React useMemo firing when no change to Redux state detected - reactjs

I have a state object in Redux and am using useSelector and deepEqual (from fast-deep-equal) to detect changes. Then useMemo on the current state. What's puzzling is that useMemo is being invoked even though the state seemingly hasn't changed.
Here is the structure of my code:
import deepEqual from "fast-deep-equal"
export function useMyState() {
const values = useSelector(
(store) => store.prop1,
(a, b) => { // comparison function arg to useSelector
const equal = deepEqual(a, b)
if (!equal) {
console.log("COMPARE FALSE")
}
return equal
}
)
useEffect(() => {
console.log("MOUNTED")
}, [])
return useMemo(() => {
console.log("VALUES CHANGED", JSON.stringify(values))
return {
values,
method1() {
// do something
}
}
}, [values])
}
I confirm that MOUNTED is only output once, and that VALUES CHANGED is output even when COMPARE FALSE is NOT output. How can this be...?
Maybe if component happens to be rendered as part of parent render it always gets the current object from store (which is not === the previous value) and useSelector compare fn is only used to prevent re-render of the component (in isolation) when state has not changed? If this is the case, what is the best-practice way to prevent useMemo being run when values are deep-equal?
codesandbox: https://codesandbox.io/s/redux-state-change-zdxzs
The issue occurs when there are two useSelector and one is triggered, causing a re-render. The component then thinks the result of the other useSelector has changed even though it has not according to the test in the selector itself (deepEqual).
To reproduce follow above link, click the increment button a few times - component is rendered but no change to todo items. Then click "re-create" button and increment again. Even though items have not changed according to deepEqual the text TODOS CHANGED! is output.
Thanks

Related

How to memoize useSelector() with createSelector()?

I wish to explain the memoization on a Redux store on my programming website.
What I have now on CodeSandbox:
import { useCallback, useMemo, memo } from "react";
import { useSelector, useDispatch } from "react-redux";
import { createSelector } from "reselect";
const Plus = memo(({ onIncrement }) => {
console.log("rendering <Plus>...");
return <button onClick={onIncrement}>+</button>;
});
export default () => {
console.log("rendering <App>...");
const v = useSelector((state) => {
console.log("useSelector() invoked");
return state;
});
const dispatch = useDispatch();
const increment = useCallback(() => dispatch({ type: "INCREMENT" }), [
dispatch
]);
return (
<div>
<span>{v}</span>
<Plus onIncrement={increment} />
</div>
);
};
As you can see, I have successfully memorized the dispatch() function with useCallback(). However, every time the button is clicked, useSelector() is called twice.
Is there a way to call it once only? I am thinking about memorization with createSelector() from the 'reselect' library. I don't understand that library. Could someone provide some guidance?
References:
Using memorizing selectors 2) The 'reselect' library
They are working correctly by having running the selectors 2 times. It would be easier and more obvious if you also print out the state (the counter) inside the selector.
Reference: https://react-redux.js.org/api/hooks#equality-comparisons-and-updates
Things will be this way:
A button and 0 are rendered on the UI.
You click the button, which dispatches an action.
The selector runs for the first time as it sees an action dispatched, it detects a change in the returned value, so it forces the component to re-render. Log is printed out with the NEW value (not the OLD one).
However, when an action is dispatched to the Redux store, useSelector() only forces a re-render if the selector result appears to be different than the last result. As of v7.1.0-alpha.5, the default comparison is a strict === reference comparison. This is different than connect(), which uses shallow equality checks on the results of mapState calls to determine if re-rendering is needed. This has several implications on how you should use useSelector()
The component re-renders, which triggers the the second log ("rendering ...").
That also leads to a re-run of the selector, which explains the third log.
When the function component renders, the provided selector function will be called and its result will be returned from the useSelector() hook. (A cached result may be returned by the hook without re-running the selector if it's the same function reference as on a previous render of the component.)

React Re-Render-Loop

I am currently trying to learn about the inner workings of React in context of when a component is re-rendered or especially when (callback-)functions are recreated.
In doing so, I have come across a phenomenon which I just cannot get my head around. It (only) happens when having a state comprising an array. Here is a minimal code that shows the "problem":
import { useEffect, useState } from "react";
export function Child({ value, onChange }) {
const [internalValue, setInternalValue] = useState(value);
// ... stuff interacting with internalValue
useEffect(() => {
onChange(internalValue);
}, [onChange, internalValue]);
return <div>{value}</div>;
}
export default function App() {
const [state, setState] = useState([9.0]);
return <Child value={state[0]} onChange={(v) => setState([v])} />;
}
The example comprises a Parent (App) Component with a state, being an array of a single number, which is given to the Child component. The Child may do some inner workings and set the internal state with setInternalValue, which in turn will trigger the effect. This effect will raise the onChange function, updating a value of the state array of the parent. (Note that this example is minimized to show the effect. The array would have multiple values, where for each a Child component is shown) However this example results in an endless re-rendering of the Child with the following console warning being raised throughout:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
Debugging shows, that the re-rendering occurs due to onChange being changed. However, I do not understand this. Why is onChange being changed? Neither internalState nor state is changed anywhere.
There are two workarounds I found:
Remove onChange from the dependencies of the effect in the Child. This "solves" the re-rendering and would be absolutely acceptable for my use case. However, it is bad practice as far as I know, since onChange is used inside the effect. Also, ESLint is indicating this as a warning.
Using a "raw" number in the state, instead of an array. This will also get rid of the re-rendering. However this is only acceptable in this minimal example, as there is only one number used. For a dynamic count of numbers, this workaround is not viable.
useCallback is also not helping and just "bubbling up" the re-recreation of the onChange function.
So my question is: Do React state (comprising arrays) updates are being handled differently and is omitting a dependency valid here? What is the correct way to do this?
Why is onChange being changed?
On every render, you create a new anonymous function (v) => setState([v]).
Since React makes a shallow comparison with the previous props before rendering, it always results in a render, since in Javascript:
const y = () => {}
const x = () => {}
x !== y // always true
// In your case
const onChangeFromPreviousRender = (v) => setState([v])
const onChangeInCurrentRender = (v) => setState([v])
onChangeFromPreviousRender !== onChangeInCurrentRender
What is the correct way to do this?
There are two ways to correct it, since setState is guaranteed to be stable, you can just pass the setter and use your logic in the component itself:
// state[0] is primitive
// setState stable
<Child value={state[0]} onChange={setState} />
useEffect(() => {
// set as array
onChange([internalValue]);
}, [onChange, internalValue]);
Or, Memoizing the function will guarantee the same identity.
const onChange = useCallback(v => setState([v]), []);
Notice that we memoize the function only because of its nontrivial use case (beware of premature optimization).

Prevent `useSelector` from re-rendering component?

I have a table with thousands of rows.
import React from "react"
import { useSelector } from "react-redux";
import { useEffect } from "react";
import Source from "../components/TranslationTable/Source";
const SourceContainer = React.memo((props) => {
const suggestionArray = useSelector((state) => state.suggestions.suggestionArray );
const isFetching = useSelector((state) => state.suggestions.isFetching);
let source = props.source;
if (props.shouldHighlight && !isFetching) {
//I will have code here to highlight text at some point
}
return (
<Source source={source}>{console.log("rendered")}</Source>
)
}, (prevProps, nextProps) => {
return true;
});
export default SourceContainer;
The above component is located inside the first column of each row. When the row is selected, props.shouldHighlight becomes true.
The problem that I have is that every single SourceContainer component re-renders every time a row is focused. When a row gets focused, isFetching becomes true in the redux state, and suggestionArray will become equal to an array of suggestions based upon the data in the selected row.
If I remove the if statement and the suggestionArray variable, the component STILL re-renders. Meaning, the only thing required to re-produce this issue is having an unused useSelector variable (ifFetching).
Can anyone explain how I can use useSelector without re-rendering the component when the state value of that selector changes?
Using useSelector, you add a dependency to your Component and it is normal that it re-renders if the content of useSelector changes.
The trick here is to make a less varying content for your useSelector function. Or at least something that changes only if the component needs to re-render
For exemple, from what I understand of what you are trying to achieve, your component should only change if "props.shouldHighlight && !isFetching" changes value.
If that's the case, then what you want to do is this :
const isFetching = useSelector((state) => (!state.suggestions.isFetching && props.shouldHighlight));

useEffect simulating componentWillUnmount does not return updated state

I have a functional component that initializes a state with useState, then this state is changed via an input field.
I then have a useEffect hook simulating componentWillUnmount so that, before the component unmounts, the current, updated state is logged to the console. However, the initial state is logged instead of the current one.
Here is a simple representation of what I am trying to do (this is not my actual component):
import React, { useEffect, useState } from 'react';
const Input = () => {
const [text, setText] = useState('aaa');
useEffect(() => {
return () => {
console.log(text);
}
}, [])
const onChange = (e) => {
setText(e.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
</div>
)
}
export default Input;
I initialize the state as "initial." Then I use the input field to change the state, say I type in "new text." However, when the component in unmounted, "initial" is logged to the console instead of "new text."
Why does this happen? How can I access the current updated state on unmount?
Many thanks!
Edit:
Adding text to useEffect dependency array doesn’t solve my problem because in my real-world scenario, what I want to do is to fire an asynchronous action based on the current state, and it wouldn’t be efficient to do so everytime the “text” state changes.
I’m looking for a way to get the current state only right before the component unmounts.
You've effectively memoized the initial state value, so when the component unmounts that value is what the returned function has enclosed in its scope.
Cleaning up an effect
The clean-up function runs before the component is removed from the UI
to prevent memory leaks. Additionally, if a component renders multiple
times (as they typically do), the previous effect is cleaned up before
executing the next effect. In our example, this means a new
subscription is created on every update. To avoid firing an effect on
every update, refer to the next section.
In order to get the latest state when the cleanup function is called then you need to include text in the dependency array so the function is updated.
Effect hook docs
If you pass an empty array ([]), the props and state inside the effect
will always have their initial values. While passing [] as the second
argument is closer to the familiar componentDidMount and
componentWillUnmount mental model, there are usually better solutions
to avoid re-running effects too often.
This means the returned "cleanup" function still only accesses the previous render cycle's state and props.
EDIT
useRef
useRef returns a mutable ref object whose .current property is
initialized to the passed argument (initialValue). The returned object
will persist for the full lifetime of the component.
...
It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.
Using a ref will allow you to cache the current text reference that can be accessed within the cleanup function.
/EDIT
Component
import React, { useEffect, useRef, useState } from 'react';
const Input = () => {
const [text, setText] = useState('aaa');
// #1 ref to cache current text value
const textRef = useRef(null);
// #2 cache current text value
textRef.current = text;
useEffect(() => {
console.log("Mounted", text);
// #3 access ref to get current text value in cleanup
return () => console.log("Unmounted", text, "textRef", textRef.current);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
console.log("current text", text);
return () => {
console.log("previous text", text);
}
}, [text])
const onChange = (e) => {
setText(e.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
</div>
)
}
export default Input;
With the console.log in the returned cleanup function you'll notice upon each change in the input the previous state is logged to console.
In this demo I've logged current state in the effect and previous state in the cleanup function. Note that the cleanup function logs first before the current log of the next render cycle.

useState vs useReducer

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.
(quote from https://reactjs.org/docs/hooks-reference.html#usereducer)
I'm interested in the bold part, which states that useReducer should be used instead of useState when being used in contexts.
I tried both variants, but they don't appear to differ.
The way I compared both approaches was as follows:
const [state, updateState] = useState();
const [reducerState, dispatch] = useReducer(myReducerFunction);
I passed each of them once to a context object, which was being consumed in a deeper child (I just ran separate tests, replacing the value by the function that I wanted to test).
<ContextObject.Provider value={updateState // dispatch}>
The child contained these functions
const updateFunction = useContext(ContextObject);
useEffect(
() => {
console.log('effect triggered');
console.log(updateFunction);
},
[updateFunction]
);
In both cases, when the parent rerendered (because of another local state change), the effect never ran, indicating that the update function isn't changed between renders.
Am I reading the bold sentence in the quote wrong? Or is there something I'm overlooking?
useReducer also lets you optimize performance for components that
trigger deep updates because you can pass dispatch down instead of
callbacks.
The above statement is not trying to indicate that the setter returned by useState is being created newly on each update or render. What it means is that when you have a complex logic to update state you simply won't use the setter directly to update state, instead you will write a complex function which in turn would call the setter with updated state something like
const handleStateChange = () => {
// lots of logic to derive updated state
updateState(newState);
}
ContextObject.Provider value={{state, handleStateChange}}>
Now in the above case everytime the parent is re-rendered a new instance of handleStateChange is created causing the Context Consumer to also re-render.
A solution to the above case is to use useCallback and memoize the state updater method and use it. However for this you would need to take care of closure issues associated with using the values within the method.
Hence it is recommended to use useReducer which returns a dispatch method that doesn't change between re-renders and you can have the manipulation logic in the reducers.
Practical observation on useReducer and useState -
UseState:
In my React Native project I've 1 screen containing 25+ different states created using useState.
I'm calling an api in useEffect (componentDidMount) and on getting the response based on some conditions, I'm setting up these 25 states, calling 25 state setter function for each function.
I've put a re-rendering counter and checked my screen is re-rendered 14 times.
re-rendering count likewise :
let count = 0;
export default function Home(props) {
count++;
console.log({count});
//...
// Rest of the code
}
UseReducer :
Then I've moved these 25 states in useReducer states, And used only single action to update these states on API response.
I've observed there is only 2 re-render.
//API calling method:
fetchData()
{
const response = await AuthAxios.getHomeData();
dispatch({type: 'SET_HOME_DATA', data: response.data});
}
//useReducer Code:
const initialStaes = {
state1: null,
state2: null,
//.....More States
state27: null,
state28: null
}
const HomeReducer = (state, action) => {
switch (action.type) {
case 'SET_HOME_DATA': {
return {
...state,
state1: (Data based on conditions),
state2: !(some Conditions ),
//....More states
state27: false
}
}
}
}
Advantage of useReducer in this case :
Using useReducer I've reduced number of re-renders on the screen, hence better performance and smoothness of the App.
Number of lines is reduced in my screen itself. It improved code readablity.
When you need to care about it
If you create a callback on render and pass it to a child component, the props of that child will change. However, when the parent renders, a regular component will rerender (to the virtual dom), even props remain the same. The exception is a classComponent that implements shouldComponentUpdate, and compares props (such as a PureComponent).
This is an optimization, and you should only care about it if rerendering the child component requires significant computation (If you render it to the same screen multiple times, or if it will require a deep or significant rerender).
If this is the case, you should make sure:
Your child is a class component that extends PureComponent
Avoid passing a newly created function as a prop. Instead, pass
dispatch, the setter returned from React.useState or a memoized
customized setter.
Using a memoized customized setter
While I would not recommend building a unique memoized setter for a specific component (there are a few things you need to look out for), you could use a general hook that takes care of implementation for you.
Here is an example of a useObjState hook, which provides an easy API, and which will not cause additional rerenders.
const useObjState = initialObj => {
const [obj, setObj] = React.useState(initialObj);
const memoizedSetObj = React.useMemo(() => {
const helper = {};
Object.keys(initialObj).forEach(key => {
helper[key] = newVal =>
setObj(prevObj => ({ ...prevObj, [key]: newVal }));
});
return helper;
}, []);
return [obj, memoizedSetObj];
};
function App() {
const [user, memoizedSetUser] = useObjState({
id: 1,
name: "ed",
age: null,
});
return (
<NameComp
setter={memoizedSetUser.name}
name={user.name}
/>
);
}
const NameComp = ({name, setter}) => (
<div>
<h1>{name}</h1>
<input
value={name}
onChange={e => setter(e.target.value)}
/>
</div>
)
Demo

Resources