I have a problem with handling state in a React application. For some background: The application mostly renders a big table with lots of data that is then editable. The data comes from a single source as a big list of objects (actually it’s a more complicated hierachy but let’s keep it simple for this purpose), and should be kept as it is. Users can then partially change the data in the big table, and ultimately save their changes.
Since the data comes from a single source, I’m thinking in React and store the data as the table state and pass everything necessary down to the individual components. So a row gets only the row data as a prop, and the cell gets only the cell data as a prop. For the update process at cell level, I then use an inverse data flow to call an update method on the table that updates the state for the updated cell:
change (rowIndex, cellIndex, value) {
this.state.data[rowIndex][cellIndex] = value;
this.forceUpdate();
}
This works pretty fine in theory. However, I have a lot data; the table easily contains about 1000 rows with multiple columns. This is not a problem directly: it takes a bit time for the browser to render the table, but once it’s there, it can work with it pretty well. Also, React doesn’t have a problem with that data amount either.
But the problem is that changing a single cell essentially triggers a rerender of the whole table. Even if the DOM is only changed for a single cell as a result, all the render methods are executed with most of them not doing anything (because the change only happened in a single cell).
My current solution is to implement shouldComponentUpdate for the row component and perform a deep check on all mutable values to avoid a rerender at row and cell level. But this feels very messy as it’s not only very verbose but also very dependent on the data structure.
So I’m not really sure how to tackle this better. I’m currently thinking about moving the state into the rows, and as such also the mutation functionality, and have the table component query the rows for changes on demand. Alternatively I could also move the whole data out of the table, and only work with identifiers the rows then use to query the data from a central store that provides the data and also offers mutation functions. This would be possible because the whole data is loaded once on page load, and then only mutated by the user.
I’m really unsure on how to handle this situation. I’m even thinking of dropping React for this, and rendering everything as static HTML with a custom JavaScript module on top that fetches the data on-demand from the actual input elements when a save is requested. Is there a way to solve this in a good way?
In case you want to play around with this situation, I have a running example on CodePen. As you type into one of the many input fields, you will notice a lag that comes from React calling all the render functions without really changing anything in the DOM.
You should take a look at PureRenderingMixin and shouldComponentUpdate documentation
I made some changes to your code so you don't modify state directly so shouldComponentUpdate can properly compare the props to determine if a rerender is required. The code here is a bit messy and I hacked it together really fast but hopefully it gives a good idea of how it could be implemented.
http://codepen.io/anon/pen/yYXbaL?editors=001
Table
change (rowIndex, cellIndex, value) {
this.state.data[rowIndex][cellIndex] = value;
var newData = this.state.data.map((row, idx) => {
if(idx != rowIndex){
return this.state.data[idx]
} else {
var newRow = this.state.data[idx].map((colVal, idx) =>{
return idx == cellIndex ? value : colVal
})
return newRow
}
});
Row
shouldComponentUpdate(nextProps){
return this.props.cells != nextProps.cells;
}
Related
I would like to use Ag Data Grid filtered data to generate plots in Vega-Lite that update automatically whenever the filter(s) are changed. I would prefer to have the implementation in an observablehq notebook. Here is a section on how to access the table data in Ag Grid documentation. I created an observablehq notebook where the chart is updated using a button. There are two issues with this implementation:
The chart is generated only after the button is clicked.
I would prefer the chart update to be automatic without the need to click a button.
Thanks for linking to the notebook! It appears that the angular table does update, but it appends to the element and gets drawn behind the code cells. This is what it looks like in my browser when I move the slider:
The problem with components like this that manipulate the DOM is that they work against Observable's way of managing the DOM. Even if the component were to update correctly, it would probably still not resize the way you'd expect.
Observable is already reactive at its core, which is something angular (and other frameworks like React) was built to add to JavaScript. Using Observable's built-in reactivity will be much easier in the long run than trying to make two different reactivity models work with each other.
You can make the AG Grid table an Observable “view” so that you can both (1) see the table and (2) refer to its value in other cells, which will re-run when you filter the table. Once you do that, it'll work naturally with anything else you want to do in the notebook. Here's a working example with Observable Plot, and here's the same example with Vega-Lite.
Assuming you've loaded AgGrid and have gridOptions in a different cell, you can wrap AgGrid as an Observable view like this:
viewof table = {
const node = htl.html`<div style="height:320px;" class="ag-theme-alpine"></div>`;
new AgGrid.Grid(node, gridOptions);
gridOptions.api.addEventListener("modelUpdated", update) // set value after filter
gridOptions.api.sizeColumnsToFit();
update(); // set initial value
function update() { // for Observable dataflow
let value = [];
gridOptions.api.forEachNodeAfterFilter(d => value.push(d.data));
node.value = value;
node.dispatchEvent(new Event("input"));
}
return node;
}
A few notes on how this is different from your version (as of when I saw it):
I put the HTML and the AgGrid initialization in the same cell, so that AgGrid has an object reference to the node and if you re-run the cell it all gets cleaned up and recreated as a whole.
I listen for when the grid rows change with gridOptions.api.addEventListener("modelUpdated", update).
In that update function, I do the same iteration over the rows that you were doing to build an array, but then I set it as the value of the node, and dispatch an input event from that node. That's what Observable looks for to tell when a view has changed its value and other cells should re-run.
I name the cell viewof table. You might've seen this pattern with sliders and other inputs on Observable. The viewof keyword means that you get the DOM node rendered, but you can also refer to its value with just table in other cells.
Thanks for the question; I’ve been meaning to check out AG Grid for a while!
I'm trying to preselect rows in my table but the table won't refresh unless there are changes to the actual data itself. Is there a method to reinit the table that doesn't involve changing the data?
It's also completely possible that my method for approaching this requirement is wrong and there may be a better way? I've created an example sandbox here:
https://codesandbox.io/s/mock-preselected-rows-data-t36nl?file=/src/App.js
In this you can see I have a mock response from my server for determining what rows should be selected. I'm then grabbing the data to compare to see if any of the items from the mock response exist in the data and if so push them to a new obj which is then fed into the intialState for selectedRowIds
Any guidance appreciated.
Seems your work is all working. The short answer to your question.
As long as you want the user see something, in a React way, it needs to be contained in a state, or state derivative. In your case, it's a cell data wrapped in row and in a table.
So you can't avoid selecting it without touching the data. Unless you don't want user see the change.
Although the checkbox doesn't seem to be part of the original data stream, when you develop on it, you have to make it part of the data. To be honest, it's easy you make it part of the data, because by the time you want to refresh the table, ex. selecting or de-selecting, or deleting a row, you want everything refreshed. Unfortunately it's very difficult to do local refresh with a table in React. It's possible, but very difficult, because most of the design is based on either prop or context.
You can also refactor your handleSelectedRows function.
// Find row ids and compare them with our 'preSelectedTheseItems' array.
const handleSelectedRows = () => {
const preIds = preSelectTheseItems.map(item => item.collectibleId)
return data?.filter((collectibleRow, index) => preIds.includes(collectibleRow.collectibleId));
};
Example : codesandbox
This is a small game that I am developing.
Screenshot
Every one of these squares are represented by an object.
{
row: i,
col: j,
isWall: false,
isVisited: false,
isPath: false,
parentRow: null,
parentCol: null,
distance: Infinity
}
isWall property corelates to the black squares.
These are stored in a 2D array in the state.
this.state = {
grid: getGrid()
}
I know we shouldn't mutate the state directly so every time I have to change a square from white to black, I copy the grid, change that square's isWall property to true and finally call setState.
getGridCopy = () => this.state.grid.map(row => row.map(square => ({...square})));
turnBlack = (row, col) => {
const grid = this.getGridCopy();
grid[row][col].isWall = true;
this.setState({
grid
})
}
(The code is stripped down to only show relevant parts)
Now imagine I have to animate the whole grid from being white to completely black one square at a time. There are hundreds of squares in the grid and I have to change every one of them to black and to change just one square, I have to copy the whole 2D array of objects.
This turns out to be very resource heavy that I can visibly see stutters in the animation. The animation is really smooth when I change the state directly without copying.
What do you suggest?
I don't have much experience developing so any suggestions are welcome. This is the first project that is actually worth something. Can you suggest other ways of storing these objects instead of 2d arrays?
EDIT:
If there's just a single component with the whole grid as its state...
I have a Board component which has this state with grid 2d array.
I mapped through the grid in board component's render method and render a Node component for each cell.
Node component has no state. It receives properties as props, applies corresponding classNames to divs and renders them.
//Board component
render() {
return(
<div className="node-group">
{
grid.map((row, i) => (
<div key={i} className="node-row">
{ row.map((node, j) => <Node {...node} key={j} ></Node> ) }
</div>
))
}
</div>
)
}
//Node component
render() {
let className = 'node';
if (this.props.isWall) className += ' node-wall';
return( <div className={className} ></div> )
}
Does this qualify as each cell being it's own react component?
No technique is bad or good on its own.
Having your state immutable allows any data-binding frameworks to detect change almost instantly, by comparing the references of old and new states, not having to go deep and do comparison prop-by-prop. Yet there's an obvious price to pay when state is updated.
Mutable state, on the other hand, is less computational-heavy, yet requires significant effort to detect the change.
The key question here is how your Views are organized. If there's just a single component with the whole grid as its state, you'll have to pay the price - either when updating the state or when trying to detect the changes.
However, if each Cell of your Grid is a separate View (React Component), it's enough to update just this object by replacing the corresponding element of your array with a new one. Something like this:
function handleGridUpdate(changedRow, changedCol, changes) {
const newGrid = grid.map(
(row, i) => rows.map(
(cell, j) => i === changedRow && j === changedCol
? { ...cell, changes }
: cell
)
);
setGrid(newGrid);
}
... then calling this function like this:
handleGridUpdate(row, col, { isWall: true } )
In this example, handleGridUpdate function takes the coords of changed grid element, as well as changes that should be applied. But the trick is, while list references are updated (as map returns a new copy of an array), their elements stay intact for each element that hasn't been changed. That saves a lot of time - both on copying and rerendering of the components.
Instead of using indexes, one can pass the original object instead (so each element of grid will be compared against it). But the important part is that only this object will be replaced with a new instance.
Check this article for more details. Even though it takes you through processing a List, and not a Grid, the key ideas demonstrated there can be applied in your case, too.
In most cases, where states are small, copying them entirely and then updating parts of it is not a problem.
However, if the state becomes bigger, or you have some deeply combined state, it can impact performance, and it becomes tedious to code. This is where persistence becomes important. Persistence ensures that immutable data structures keep references to parts of data that don't change, and gives back a copy of the object referencing the same 'old' data when possible, and new data when needed. This makes the structures both very memory efficient and performant.
There are libraries that take care of immutability and persistence for you. The most notable ones are:
Immutable.js: an immutable collections library that has persistence built in.
Immer: a smart way to update objects like you normally would, without the need to worry about modifying the original.
I myself am developing an immutable persistent collections library called Rimbu for TypeScript. It offers a plethora of immutable persistent collections that you can use in your state without being afraid to accidentally modify your data.
Since I liked you question, and need material to test my own library, I have created a small example in CodeSandbox of a basic game board using a Rimbu Table for the state.
I am using tabulator package 4.3.0 to work on a webpage. The table generated by the package is going to be the control element of a few other plots. In order to achieve this, I have been adding a dataFiltered function when defining the table variable. But instead of getting the order of the rows in my data object, I want to figure a way to get the index of the rows in the filtered table.
Currently, I searched the manual a little bit and have written the code analogue to this:
dataFiltered: function(filters,rows){
console.log(rows[0]._row.data)
console.log(rows[0].getPosition(true));
}
But the getPosition always returned -1, which refers to that the row is not found in the filtered table. I also generated a demo to show the real situ when running the function. with this link: https://jsfiddle.net/Binny92/3kbn8zet/53/.
I would really appreciate it if someone could help me explain a little bit of how could I get the real index of the row in the filtered data so that I could update the plot accordingly and why I am always getting -1 when running the code written in this way.
In addition, I wonder whether there is a way to retrieve the data also when the user is sorting the table. It's a pity that code using the following strategy is not working in the way I am expecting since it is not reacting to the sort action and will not show the information when loading the page for the first time.
$('#trialTable').on('change',function(x){console.log("Yes")})
Thank you for your help in advance.
The reason this is happening is because the dataFiltered callback is triggered after the rows are filtered but before they have been laid out on the table, so they wont necessarily be ready by the time you call the getPosition function on them.
You might do better to use the renderComplete callback, which will also handle the scenario when the table is sorted, which would change the row positions.
You could then use the getRows function passing in the value "active" as the first augment return only rows that have passed the filter:
renderComplete: function(){
var rows = table.getRows("active");
console.log(rows[0].getPosition(true));
}
On a side note i notice you are trying to access the _row property to access the row data. By convention underscore properties are considered private in JavaScript and should not be accessed as it can result in unstable system behaviour.
Tabulator has an extensive set of functions on the Row Component to allow you to access anything you need. In the case of accessing a rows data, there is the getData function
var data = row.getData();
Is it possible to force update only one td (cell) for view ?
There is a method hot.render(), but it rerenders all cells.
I want to update table content JSON data (hot.getData()) using ajax, but I can't find how to force render the table. My table is so huge to rerender each time I receive the data.
E.g.,
$.ajax(url,... ,success: function(d){
var data = hot.getData();
data[parseInt(d['row'],10)][d['col']] = d['value'];
hot.render();//please, change this function into more simple.
},
...
);
is it possible to update a TD-cell at [row,col]?
I agree complete grid rendering is good , however there is one drawback.When we render the complete grid it will scroll back to the row 1 , it will not store the last view we have before render.E.g. i am in the row number 100 as soon as i render grid will come back to row number 1 , now user has to scroll back to row number 100 again which is frustrating.
Any solution to this issue.
I found we could use hot.selectCell(100,1) to go back to row hundred , however how do i store that we were in the row number 100 programmatically, so that i could set it back to that row.
I did one customRendering to change the value of the Grid by using below code, however it has some performance issue when we have large number of row to change.
//Below code will render one row ,similarly apply the loop to modify multiple row.
data.Records[i].Values.forEach(function(value, ix) {
hot.setDataAtCell(i, ix, value);
//i is row number , ix is the column number , v is the new value.
}
However hot.setDataAtCell(i, ix, v) is a costly, so if you have a large number of row then it will hit the performance.However benefit is it will do the custom(single/mulitiple) cell/row rendering without scrolling the grid and preserving the user view.
P.S. in place of hot.setDataAtCell you could use setDataAtRowProp to set the value for a row , however i have not tried.
Re-rendering the entire table is the only way Handsontable will allow you to render and it's there for a few good reasons. First off, it doesn't matter how large your table is since there is virtual rendering in use. This means that it will only render what you can see plus a few more rows. Even if you had trillions of rows and columns, it would still just render enough for you to think it's fully rendered. This is not an intensive task assuming you're not doing something funky with a custom renderer.
The other reason why it renders everything from scratch is that it's Handsontable's way of keeping a stateless DOM object. If you started manually rendering specific cells then you could end up with an out of sync looking table. And, again, since virtual rendering restricts what gets rendered, there's no performance issue associated with a full re-rendering.
You can use setDataAtCell() method to update one or more cells.
setDataAtCell(row, column, value, source)
Set new value to a cell. To change many cells at once (recommended way), pass an array of changes in format [[row, col, value], ...] as the first argument.
Source: https://handsontable.com/docs/7.2.2/Core.html#setDataAtCell
For example:
var row = 3;
var col = 7;
var value = "Content of cell 3,7";
hot.setDataAtCell(row, col, value); // Update a single cell
Method also accepts an array of values to update multiple cells at once. For example:
var cell_3_7 = [3, 7, "Content of cell 3,7"];
var cell_5_2 = [5, 2, "Content of cell 5,2"];
hot.setDataAtCell([ cell_3_7, cell_5_2 ]); // Update a multiple cells
Note that Handsontable documentation does not recommend re-rendering the whole table manually.
render()
Calling this method manually is not recommended. Handsontable tries to render itself by choosing the most optimal moments in its lifecycle.
Source: https://handsontable.com/docs/7.2.2/Core.html#render