My useEffect
is running as it should be, but once it is complete, it doesn't update the state of my Component. It always shows the previous render. I thought I have added all the appropriate dependencies, but it still gives me stale data. Here is my hook:
export default function useSort(items, sortKey, sortAscending) {
let [sorted, setSorted] = useState(items);
useEffect(() => {
let sortedItems = sortKey ? items.sort((a, b) => {
if (!a[sortKey] && b[sortKey]) return sortAscending ? -1 : 1;
if (a[sortKey] && !b[sortKey]) return sortAscending ? 1 : -1;
if (a[sortKey] > b[sortKey]) return sortAscending ? 1 : -1;
if (a[sortKey] < b[sortKey]) return sortAscending ? -1 : 1;
return 0;
}) : items;
setSorted(sortedItems);
}, [items, sortKey, sortAscending]);
return sorted;
}
Here is the component I am using it in:
const SearchResults = ({ columns, searchResults, sortAscending, sortKey }) => {
const dispatch = useDispatch();
let sorted = useSort(searchResults, sortKey, sortAscending);
return sorted.map((searchResult, index) => {
return ( ... )
}
SearchResult
renders, but when I try to sort the results (depending on the column header I click), the useEffect
code runs. After it is sorted, SearchResult
never gets re-rendered to show the change.
How am I using the hook incorrectly? What is the proper usage?
The stale closure problem occurs when a closure captures outdated variables. An efficient way to solve stale closures is to correctly set the dependencies of React hooks. Or, in the case of a stale state, use a functional way to update the state.
The useEffect callback runs twice for initial render, probably because the component renders twice. After state change the component renders twice but the effect runs once.
useEffect(fn, deps); fn is the effectful function, and deps is an array of values it depends on. Every time the component renders, React checks if all the values in the deps array are still the same. If any of them has changed since the last render, fn is run again.
Stale closure is the referencing towards an outdated variable in between renders (from 'React' perspective). That means even when the state/props are updated and component re-renders, some callbacks will still be referencing old variables. Let's see this with an example.
Arrays in JavaScript are reference
type; and setState
will cause re-render of the component when current value and previous values being different; unlike primitive types, reference types will only marked as changed when their reference change (i.e. shallow comparison). In this scenario when you mutate the array in-place, previous value and next value would be the same from shallow comparison perspective; it's like calling setState(2)
when the state
is already 2
; reconciliation will ignore that. people already mention that in the comments if you return new array (new reference), that would fix it; by the way check out the snippet below for a specific show-case of the problem happening here:
ReactDOM.render(<Test/>, document.getElementById("root"))
function Test(){
const [array, setArray] = React.useState([2,3,4,5]);
const renderCounter = React.useRef(0);
function mutateInPlace(e){
renderCounter.current++;
// some random in-place mutations
array[1] = 4;
array[2] = 11;
array.sort((a,b)=>a-b);
setArray(array);
}
function mutateOutOfPlace(e) {
renderCounter.current++;
// creating a new array
let arrayCopy = array.slice(0);
setArray(arrayCopy);
}
console.log("re-rendered");
return (
<div>
{renderCounter.current}
<br/>
<button onClick={mutateInPlace}>mutate in-place</button>
<button onClick={mutateOutOfPlace}>mutate out-of-place</button>
</div>
);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With