I noticed that if I dispatch an action that happens to not to modify the state, the component is re-rendered anyway.
Example:
// for simplicity sake, we simply pass state on (no mutation)
const someReducer = (state, action) => state
const App = () => {
const [state, dispatch] = useReducer(someReducer, 0)
// in my use case, I want to dispatch an action if some specific condition in state occurs
if(state === 0) {
dispatch({ type: 'SOME ACTION' }) // type doesn't matter
}
// return some JSX
}
I get:
Error in app.js (16929:26)
Too many re-renders. React limits the number of renders to prevent an infinite loop.
Is this by design? Should it be this way?
In terms of your example as-is, it's not immediately obvious as to what is causing the component to re-render. However, the docs seem to suggest that it's not a guarantee that a re-render won't occur when you don't mutate the state:
Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.
This is why it's generally recommended that you run code that potentially has side effects in useEffect e.g.
const [state, dispatch] = useReducer(someReducer, 0);
...
useEffect(() => {
if (state === 0) {
dispatch({ type: 'SOME ACTION' });
}
}, [state]);
Related
I've been reading the docs on createSelector and i'm under the impression that it's supposed to memoize the results to avoid expensive calculations. However, for some reason my console.logs are firing in functions that should'nt be firing. Curious why that is, am I doing something wrong? By the time I reach this specific component, the data already exists in the store in the same shape so it should be memoized.
note: useAppDispatch and useAppSelector are the same as the defualts, they're just ts-typed in a central location.
in my component:
const dispatch = useAppDispatch();
const rawTranscript = useAppSelector(selectRawTranscript);
const parsedTranscript = useAppSelector(selectParsedTranscript);
const parsedResult = useMemo(() => {
// rawTranscript should be memoized ...
return transcriptParser(rawTranscript, undefined, 0.9);
}, [rawTranscript]);
useEffect(() => {
// this also fires, I initially had the parsed result here but the same behavior happened.
// const parsedResult = transcriptParser(rawTranscript, undefined, 0.9);
dispatch(updateParsedTranscript(parsedResult));
}, [dispatch, parsedResult]);
in my redux slice:
// selectRawTranscript should be memoized (createSelector is used)
export const selectRawTranscript = createSelector(
(state: RootState) => state.transcript.rawTranscript,
rawTranscript => rawTranscript
);
export const selectParsedTranscript = createSelector(
(state: RootState) => state.transcript.parsedTranscript,
parsedTranscript => parsedTranscript
);
Your selectors really aren't "memoizing" anything. Any time you have a use of createSelector where the output selector is just x => x, there's no real memoization happening. This is exactly equivalent to a plain function that is just state => state.x - you're just returning the field as-is.
Based on the code you've shown so far, the component will re-render any time the state.transcript.rawTranscript or state.transcript.parsedTranscript fields are updated by the reducer, and the effect will re-run every time the state.transcript.rawTranscript field is updated.
I'd suggest reading through the recently updated Redux docs page on selectors to get a better idea of when, how, and why to write memoized selector functions.
Also, having the effect depend on the parsed transcript, and then also dispatch an action that looks like it's saving the parsed value, seems a bit odd and with potential to cause extra re-renders.
Beyond that
I've built a tabs component. Each tab, when clicked, changes the contents of the "main screen". Assume a tab had some back-end call to retrieve data it needs to render, then it makes no sense to have it re-run these calls every time the user clicks another tab and comes back to it. I want to retrieve what was rendered before and display it.
I looked into memo, the big warning says to "not rely on it to “prevent” a render, as this can lead to bugs.", nor does it work. Every time I wrap my component in a memo, the test:
useEffect(() => {
console.log('Rendered');
}, [])
Still runs, telling me that the component re-rendered. Then I thought about memoizing the return itself, so, like:
export const MyComponent = (context) => {
const content = useMemo(() => {
return <></>
}, [context]);
return content;
};
But quickly realized that by the time I reach this useMemo, I'm already in the re-rendering cycle, because there's no way for React to know that MyComponent's useMemo existed in the past, so, again, it re-renders the whole thing. This, in turn made me think that the memoization needs to be done at the level where MyComponent is being rendered, not inside of it but I don't know how to do it.
How can I skip re-renders if my props haven't changed?
Read all the articles, tried all the things but to no avail.
Concisely, here is my component and my latest approach:
export const MyComponent = memo(({ context, className = '', ...props }) => {
..
..
});
The interesting bit here is context. This should almost never change. Its structure is a deeply nested object, however, when I play with memo's second argument, its diff function, what ends up happening if I put a console.log in there, as follows:
const MyComponent = ({ context, className = '', ...props }) => {
};
const areEqual = (prevProps, nextProps) => {
console.log('Did equality check.');
};
export default memo(MyComponent, areEqual);
I will only see "Did equality check." once. No matter what I do, I can't seem to get a memoized component out of memo. This is how MyComponent's parent looks like:
const Parent = ({}) => {
const context = useSelector(); //context comes from the store.
const [selectedTab, setSelectedTab] = useState(false);
const [content, setContent] = useState(null);
useEffect(() => {
switch (selectedTab) {
case 'components':
setContent(<MyComponent context={context} />);
break;
}
}, [selectedTab, context]);
return(<>{content}</>);
};
#daniel-james I think the problem here is we are un-mounting the whole component when we are switching tabs. Instead try memoizing the component in the parent itself. That way your component is memoized only once.
When using useReducer to manage state, the internal state update is deferred.
I.e. if I were to initialize a reducer with something like
const [state, dispatch] = useReducer(reducer, {prop: ""})
... then later call a dispatch like so ...
dispatch({
type: 'ACTION_THAT_UPDATES_PROP'
});
console.log(state.prop)
... the value of "state.prop" wouldn't contain the new value, due to the deferred state update.
Is there a way to get the newly updated state? In other words, I'm looking for a similar mechanism provided by React's setState method, where you could do something like
this.setState({
propName: "newPropValue"
}, () => {
// state.propName would have value of "newPropValue" here
})
You can access it, but not in the body of the function nor directly after the dispatch, since the state needs to be updated first. React's state and lifecycle mandates that the new state won't be approachable in the context of the current state.
The way to do so is to use useEffect:
React.useEffect(() => {
console.log(state.prop)
}, [state.prop]);
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
I'm using the alpha version of react supporting hooks, and want to validate my approach to updating the text in a component after an interval without rendering the component more times than needed when a prop changes.
EDIT: For clarity - this component is calling moment(timepoint).fromNow() within the formatTimeString function (docs here), so the update isn't totally unneccessary, I promise!
I previously had:
const FromNowString = ({ timePoint, ...rest }) => {
const [text, setText] = useState(formatTimeString(timePoint));
useEffect(() => {
setText(formatTimeString(timePoint));
let updateInterval = setInterval(
() => setText(formatTimeString(timePoint)),
30000
);
return () => {
clearInterval(updateInterval);
};
}, [timePoint]);
// Note the console log here is so we can see when renders occur
return (
<StyledText tagName="span" {...rest}>
{console.log('render') || text}
</StyledText>
);
};
This "works" - the component correctly updates if the props change, and the component updates at each interval, however on mounting, and when a prop changes, the component will render twice.
This is because useEffect runs after the render that results when the value of timePoint changes, and inside my useEffect callback I'm immediately calling a setState method which triggers an additional render.
Obviously if I remove that call to setText, the component doesn't appear to change when the prop changes (until the interval runs) because text is still the same.
I finally realised I could trigger a render by setting a state variable that I didn't actually need, like so:
const FromNowString = ({ timePoint, ...rest }) => {
// We never actually use this state value
const [, triggerRender] = useState(null);
useEffect(() => {
let updateInterval = setInterval(() => triggerRender(), 30000);
return () => {
clearInterval(updateInterval);
};
}, [timePoint]);
return (
<StyledText tagName="span" {...rest}>
{console.log("render") || formatTimeString(timePoint)}
</StyledText>
);
};
This works perfectly, the component only renders once when it mounts, and once whenever the timePoint prop changes, but it feels hacky. Is this the right way of going about things, or is there something I'm missing?
I think this approach seems fine. The main change I would make is to actually change the value each time, so that it is instead:
const FromNowString = ({ timePoint, ...rest }) => {
const [, triggerRender] = useState(0);
useEffect(() => {
const updateInterval = setInterval(() => triggerRender(prevTriggerIndex => prevTriggerIndex + 1), 30000);
return () => {
clearInterval(updateInterval);
};
}, [timePoint]);
return (
<StyledText tagName="span" {...rest}>
{console.log("render") || formatTimeString(timePoint)}
</StyledText>
);
};
I have two reasons for suggesting this change:
I think it will help when debugging and/or verifying the exact behavior that is occurring. You can then look at this state in dev tools and see exactly how many times you have triggered the re-render in this manner.
The other reason is just to give people looking at this code more confidence that it will actually do what it is intended to do. Even though setState reliably triggers a re-render (and React is unlikely to change this since it would break too much), it would be reasonable for someone looking at this code to wonder "Does React guarantee a re-render if a setState call doesn't result in any change to the state?" The main reason setState always triggers a re-render even if unchanged is because of the possibility of calling setState after having done mutations to the existing state, but if the existing state is null and nothing is passed in to the setter, that would be a case where React could know that state has not changed since the last render and optimize for it. Rather than force someone to dig into React's exact behavior or worry about whether that behavior could change in the future, I would do an actual change to the state.