How to prevent expanded React-Table row from collapsing on re-render? - reactjs

I have a simple react-table with an expanded sub component whose data is tied to a Redux state object.
When the sub component gets expanded, I trigger an api call to get some additional data to be lazy-loaded onto the redux store. The redux store gets updated with the new data and the sub component DOM gets successfully updated as well, but the problem is the expanded row doesn't stay expanded after the re-render. It collapses.
Is there a way to keep my expanded rows expanded even after the redux store gets updated and a re-render is triggered?

Set autoResetExpanded to false in configuration, more details - https://react-table.tanstack.com/docs/api/useExpanded

For me, setting collapseOnDataChange to false worked
You could also try freezeWhenExpanded property and set it to true.

Related

React Router prop changes causing unnecessary render

I am working on an app that has a range of routes that display essentially the same data except the route filters the list based on a status property that determines which entries we want shown in the list.
So in my App.js file I have all the routes set up and the listItems are passed into each of these route components as a property. These items are then rendered out as a table.
Now I have some logic that greys the list out and disabled the buttons on the list items when the list is refreshing using a toggle (isRefreshing) state variable.
The isRefreshing state variable is toggled off using a useEffect() that fires when listItems is updated.
This works great for the refreshing button, but not so well when the route changes.
I've figured out it's because when the route changes the existing listItems prop gets fed into the new rendered list component it see that as listItems changing, so the useEffect() fires, toggling isRefreshing off.
2 seconds later the real data for this new route turns up in the list.
So my question is how can I prevent this from happening? I feel like I might have backed myself into a corner and made a serious architectural error.
Can anyone point me in the right direction?
I've created an example here:
https://codesandbox.io/s/small-sound-rikxv?file=/src/App.js:0-2353
As you can see the refresh button works as expected, but the route change causes the listItems prop to immediately cause my refresh indicator to stop, so you can see a console.log fired immediately after hitting the "list2" link, then 5 seconds later when the real data loads it fires again. The problem is that the initial prop value causes my refresh to stop.
Regards, John.
So I found that if I lifted the "refreshing" state variable right up to the same component the data fetching was happening in then things got a lot easier for me.
Even though the property value changes twice still, because the refreshing state is being handled further up the tree, with refreshing now getting passed down, it's not an issue.
With refreshing getting managed up the tree, the refreshing toggle only gets toggled when refreshing is actually happening and not when the prop thinks it has changed.
Thanks for getting me to code out a simple example, that really helped me conceptualize what was happening free from the clutter of my app's code base.

react-window saves state of deleted row

here is my problem.
I am using react-window to render large tables. Each row has its own local state. After I delete a row, the next row moves up and gains the state of the deleted row (this how it looks in my app).
Is there a workaround for this problem? Can i have local state for each row with react-window?
codesandbox example | gif how it works
There is a fundamental issue here - react-window will dynamically render, unmount and rerender components. So if you want data persisted across re-renders - you must pass the count via props and a function to change it. Store the data with the item data itself.
I will try and show a working demo of that soon ,but look at this to understand yet another problem happening with this code: https://codesandbox.io/s/react-window-row-state-problem-mmukt
I have edited the size to be 150 instead of 50, this leads to the window getting a scrollbar. Now, try and click on "two" multiple times. This will increase the count. Next, scroll to bottom and go back up. You'll see that the count is lost.
This is because of the way react-window mounts and unmounts components.
To fix this in your original code, I made the following modifications:
1. Move count/active state to parent.
2. Pass the updateCount function to TableRow.
Essentially, the TableRow is now a pure component :)
Here is the link: https://codesandbox.io/s/react-window-row-state-problem-uzsdl
Hope this helps.
In a nutshell, you can't. The issue is that, when you delete an item, the same component that was used previously for it will be used to render the item after it (so if you delete the 2nd item, the 3rd item will use the Row from the old 2nd element). It will maintain that item's state. If you add a useEffect to detect that the item changed, then you break everything after it (since everything is bumped up one, all of the items after the deleted item will reset their state. You don't have access to the sibling component's state, so you have no way to propagate the state.
You have a few options here:
Add the selection to the properties of the item, and provide a way to update the item.
Make the selection state part of the App component's state, and pass it down to the component so it can render appropriately.
I'll also add that you probably don't want to map over your presumably large list of items every render like you are now. It looks like you are doing this to expose deleteItems. I would recommend something more like this:
<FixedSizeList
height={500}
width={300}
itemCount={items.length}
itemSize={50}
itemData={{items, deleteItem}}
>
{Row}
</FixedSizeList>
Then your Row component's data will have both the items array and the deleteItem function. If you maintain your selected rows state in App, you easily extend this to pass the selected state and modification functions down to your Row component.

Using shouldComponentUpdate in a child component appears to be breaking state in parent component?

I have adapted this example from the material-ui table component example from the docs. In the provided example, rows can be selected, in which case the checkbox for that row will be checked. The problem with the example implementation is that when a change is made to any row, all of the rows will be re-rendered, even though only one of them has changed. This works fine as long as the number of rows is quite small but quickly degrades when rendering more rows.
As an attempted fix for this, I tried to implement shouldComponentUpdate in my Row components, to check if the row data (via the 'name' property), or the isItemSelected prop has changed (I ignored the other props as the callback function props will always be considered to have changed on each render). For some reason, this totally breaks my selected state in the parent Table component.
Here is my example, highlighting the issue: table-bug-example. As is, this reflects the example provided in the docs. You can see the lag issues if you set the rows per page drop down to a higher number. You can uncomment my shouldComponentUpdate implementation in the Row Component, to see how the state breaks in the Table component, when using shouldComponentUpdate in the children.
Expected Flow:
parent state keeps track of selected rows
clicking on row updates parent state
parent passes isSelected prop to child rows
row component checks to see if isSelected has changed, and re-renders only if this prop has changed
I have been struggling with this for several hours and can't for the life of me figure out what is wrong. I've also tried using React.memo, as well as different implementations of the setSelected state in the table component (I was previously using a dictionary to keep track of the selected rows, but for now I've left the default implementation provided in the material-ui docs, to minimize any issues introduced in my code).
Might anyone happen to know what is going on here?
When you used your custom shouldComponentUpdate what happened you "saved" the old version of handleClick inside of your custom MyRow, and since handleClick is not a pure function(it is using previous selection to determine prev selection state) it was always reading the wrong state.
For example you have 2 rows (row1 and row2). On initial render selection is : [], and the both render with the version of handleClick that has captured selection as []. You click on row1, it will rerender since its isItemSelected prop has changed, but row2 will not since your shouldComponentUpdate returned false. That means when you click on row2 it will use the handleClick from the first render wich had the selection as [] and it will uncheck row1 selection. There is no magic mechanism that binds selection, it all uses javascript context capturing.
Thats why custom shouldComponent is very dangerous. I would recommend using MyRow as a PureComponent (no need to use React.memo, you can just extend PureComponent) and transform handleClick inside table so it doesnt change (doesnt depend on current selection). Something like this: https://codesandbox.io/embed/material-demo-i0yp2?fontsize=14&hidenavigation=1&theme=dark
Hope this helps.

Keep scroll-y after rerender

I have dropdown list with a lot of checkboxes, so this container has scroll. But when i click on any checkbox - it selects\deselects itself and then state changes.
So the problem is that after rerendering this container is back to the top. Is it possible to keep container's scroll after rendering without saving it to the state?
You can save the "snapshot" of the scroll position before the commit phase.
getSnapshotBeforeUpdate() shows kind of what you are looking for.
The documentation example saves the current scroll position within getSnapshotBeforeUpdate lifecycle method and then use the snapshot value passed to componentDidUpdate(prevProps, prevState, snapshot) as the last argument to restore the scroll position.
It doesn't require creating a state to save the scroll position as you requested.
It's happening because you are re-rendering the complete list of the checkboxes.
There are 2 possible approaches:
Re-render only relevant checkbox
Save the scroll position of the container and update it once component is re-rendered.
Unfortunately, you haven't added any code examples, so can't share the code changes.

How to pass filter-params with React?

I'm using Semantic-React and I have component-filter which consists of checkbox groups. Every checkbox has id and I must pass them in order to filter the results.First idea was to perform method which would be called after checkbox onCLick(which would pass checkbox id). And in this method set in state an object and after every checkbox click change it. I remember, that in Jquery exists $('form').serialize() which everytime checks form elements and gets checked values automaticaly. Exists something like this in react, or semantic-ui react? I would be simplier to use such method than to create and control object in state
You should use "controlled components":
Filter component should keep a set of checked ids as part of state.
Checkbox checked property should be set to true if the set contains box id (and to false otherwise) in Filter component render.
Checkbox onChange should be handled by filter component and modify state.
Filter can access it's state handling checkbox onChange or button "Apply" onClick (or any other event handled by Filter component).
Be careful: setState is an async function and directly reading other component state can lead to data race, so state must be read after proper event fired by React. Also there are a bunch of state-keepers like Redux.
It is described in official docs here: https://reactjs.org/docs/forms.html

Resources