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).
Related
I've been using React for a couple years, with functional components and hooks. The most frustrating thing I've found has to do with passing callbacks. I have two concerns:
If an element receives a callback from a parent it can't really use that function in a event listener or onEvent field or in a setTimeout/setInterval, because the passed function may be redefined any time the parent rerenders.
A function that's generated at each render (like an arrow function) and passed to a child element breaks React.memo().
I've seen some limited solutions to this, such as useEventListener() but not one that addresses all the situations that concern me.
So I've written my own hook that I've called useHandler. Unlike useCallback, which generates a new function whenever the inputs change, useHandler always returns the same function. This way the function case be passed to an element using React.memo() or to a setInterval or whatever.
It call be used by the parent element (needed for the React.memo case) or by the child element (if you're worried that a passed-in function might change). Here's an example of it used in the parent:
const onClick = useHandler(event => {
//Do whatever you want. Reference useState variables or other values that might change,
})
<MyElement onClick={onClick} />
MyElement now always gets the same function, but calling that function will execute the most recent code in the parent element.
Here's an example in the child element:
const reportBack = useHandler(props.reportBack)
useEffect(() => {
setInterval(reportBack, 1000)
}, [reportBack])
The interval callback will always call the current reportBack passed from the parent.
The only thing missing for me is that I haven't modified the React eslint config to recognize that that results of useHandler cannot change and thus shouldn't generate a warning if omitted from the dependency list in useEffect or similar.
And finally, here's the source for my onHandler hook:
export function useHandler(callback) {
const callbackRef = useRef()
callbackRef.current = callback
return useRef((...args) => callbackRef.current(...args)).current
}
My questions are: Are there any flaws in this solution? Is it a good way to address my issues with React callbacks? I haven't found anything more elegant but I'm plenty willing to believe that I've missed something.
Thanks.
I can't comment on the second point as I've not used React.memo but the first point is why we have cleanup functions.
Let's say you have a function that is passed as a prop down to your component and you want to use it as an event listener.
If you are attaching this listener inside a useEffect like: window.addEventListener('someEvent', (event) => someFunctionPassedAsProp(event))
Then the useEffect can look something like:
useEffect(() => {
const handler = (event) => someFunctionPassedAsProp(event)
window.addEventListener('someEvent', handler)
return () => window.removeEventListener('someEvent', handler)
}, [someFunctionPassedAsProp])
Similarly for intervals you can do something like:
useEffect(() => {
const interval = setInterval(() => someFunctionPassedAsProp(), 1000)
return () => clearInterval(interval)
}, [someFunctionPassedAsProp])
I'm using useRef to hold the latest value of a prop so that I can access it later in an asynchronously-invoked callback (such as an onClick handler). I'm using a ref instead of putting value in the useCallback dependencies list because I expect the value will change frequently (when this component is re-rendered with a new value), but the onClick handler will be called rarely, so it's not worth assigning a new event listener to an element each time the value changes.
function MyComponent({ value }) {
const valueRef = useRef(value);
valueRef.current = value; // is this ok?
const onClick = useCallback(() => {
console.log("the latest value is", valueRef.current);
}, []);
...
}
The documentation for React Strict Mode leads me to believe that performing side effects in render() is generally unsafe.
Because the above methods [including class component render() and function component bodies] might be called more than once, it’s important that they do not contain side-effects. Ignoring this rule can lead to a variety of problems, including memory leaks and invalid application state.
And in fact I have run into problems in Strict Mode when I was using a ref to access an older value.
My question is: Is there any concern with the "side effect" of assigning valueRef.current = value from the render function? For example, is there any situation where the callback would receive a stale value (or a value from a "future" render that hasn't been committed)?
One alternative I can think of would be a useEffect to ensure the ref is updated after the component renders, but on the surface this looks unnecessary.
function MyComponent({ value }) {
const valueRef = useRef(value);
useEffect(() => {
valueRef.current = value; // is this any safer/different?
}, [value]);
const onClick = useCallback(() => {
console.log("the latest value is", valueRef.current);
}, []);
...
}
For example, is there any situation where the callback would receive a stale value (or a value from a "future" render that hasn't been committed)?
The parenthetical is the primary concern.
There's currently a one-to-one correspondence between render (and functional component) calls and actual DOM updates. (i.e. committing)
But for a long time the React team has been talking about a "Concurrent Mode" where an update might start (render gets called), but then get interrupted by a higher priority update.
In this sort of situation it's possible for the ref to end up out of sync with the actual state of the rendered component, if it's updated in a render that gets cancelled.
This has been hypothetical for a long time, but it's just been announced that some of the Concurrent Mode changes will land in React 18, in an opt-in sort of way, with the startTransition API. (And maybe some others)
Realistically, how much this is a practical concern? It's hard to say. startTransition is opt-in so if you don't use it you're probably safe. And many ref updates are going to be fairly 'safe' anyway.
But it may be best to err on the side of caution, if you can.
UPDATE: Now, the beta docs also say you should not do it:
Do not write or read ref.current during rendering, except for
initialization. This makes your component’s behavior unpredictable.
By initialization above they mean such pattern:
function Video() {
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
....
To the best of my knowledge, it is safe, but you just need to be aware that changes to the ref-boxed value may occur when React "feels like" rendering your component and not necessarily deterministically.
This looks a lot like react-use's useLatest hook (docs), reproduced here since it's trivial:
import { useRef } from 'react';
const useLatest = <T>(value: T): { readonly current: T } => {
const ref = useRef(value);
ref.current = value;
return ref;
};
export default useLatest;
If it works for react-use, I think it's fine for you too.
function MyComponent({ value }) {
const valueRef = useRef(value);
valueRef.current = value; // is this ok?
const onClick = useCallback(() => {
console.log("the latest value is", valueRef.current);
}, []);
...
}
I don't really see an issue here as valueRef.current = value will occur every render cycle. It's not expensive, but it will happen every render cycle.
If you use an useEffect hook then you at least minify the number of times you set the ref value to only when the prop actually changes.
function MyComponent({ value }) {
const valueRef = useRef(value);
useEffect(() => {
valueRef.current = value;
}, [value]);
const onClick = useCallback(() => {
console.log("the latest value is", valueRef.current);
}, []);
...
}
Because of the way useEffect works with the component lifecycle I'd recommend sticking to using the useEffect hook and keeping normal React patterns. Using the useEffect hook also provides a more deterministic value per real render cycle, i.e. the "commit phase" versus the "render phase" that can be cancelled, aborted, recalled, etc...
Curious though, if you just want the latest value prop value, just reference the value prop directly, it will always be the current latest value. Add it to the useCallback hook's dependency. This is essentially what you are accomplishing with the useEffect to update the ref, but in a clearer manner.
function MyComponent({ value }) {
...
const onClick = useCallback(() => {
console.log("the latest value is", value);
}, [value]);
...
}
If you really just always want the latest mutated value then yeah, skip the useCallback dependencies, and skip the useEffect, and mutate the ref all you want/need and just reference whatever the current ref value is at the time the callback is invoked.
What's the different between useEffect when you pass it dependencies as the second parameter and useCallback?
Don't both essentially run the function/code passed as the first parameter whenever the dependencies passed as the second parameter change?
From what I've read the two hooks are intended to serve different purposes, but my question is whether they in actuality could be used interchangeably because they functionally do the same thing
They're too different.
useEffect will run the function inside when the dependency array changes.
useCallback will create a new function when the dependency array changes.
You can't switch useEffect with useCallback alone because you also need the logic to run the newly created function. (I suppose you could implement this if you used a ref as well, but that'd be quite strange.)
You can't switch useCallback with useEffect because you very often don't want to run the newly created function immediately - rather, you usually want to pass it as a prop to some other component.
useCallback primarily exists for optimization purposes, to reduce re-renders of a child component.
No, They are not same.
useEffect - is used to run side effects in the component when something changes. useEffect does
not return you anything. It just runs a piece of code in the component.
useCallback - Whereas useCallback returns a function, it does not execute the code actually. It is important to understand that
functions are objects in Javascript. If you don't use useCallback, the function you define inside the component is
re-created whenever the component rebuilds.
Example
Consider this example, this component will go in a infinite loop. Think Why?
const TestComponent = props => {
const testFunction = () => {
// does something.
};
useEffect(() => {
testFunction();
// The effect calls testFunction, hence it should declare it as a dependency
// Otherwise, if something about testFunction changes (e.g. the data it uses), the effect would run the outdated version of testFunction
}, [testFunction]);
};
Because on each render the testFunction
would be re-created and we already know that ueEffect will run the code when ever the testFunction changes. And since testFunction changes on each render, the useEffect will keep on running, and hence an infinite loop.
To fix this, we have to tell react, hey please don't re-create the testFunction on each render, create it only on first render (or when something changes on which it depends).
const TestComponent = props => {
const testFunction = useCallback(() => {
// does something.
}, []);
useEffect(() => {
testFunction();
// The effect calls testFunction, hence it should declare it as a dependency
// Otherwise, if something about testFunction changes (e.g. the data it uses), the effect would run the outdated version of testFunction
}, [testFunction]);
};
This won't be a infinite loop, since instance of testFunction will change only on first render and hence useEffect will run only once.
useEffect will run the function inside when the dependency array changes.
useCallback will create a new function when the dependency array changes.
Let's take an example, If I run the below code and click the first button it'll always rerender MemoComponent as well. Why because every time
we are passing new onClick function to this. To avoid re-rendering of MemoComponent what we can do is wrap onClick to useCallback. Whenever you want to create a new function pass state to the dependence array.
If you want to perform some action on state change you can write inside useEffect.
const Button = ({ onClick }) => {
console.log("Render");
return <button onClick={onClick}>Click</button>;
};
const MemoComponent = React.memo(Button);
export default function Home() {
const [state, setState] = useState(1);
useEffect(() => {
console.log(state); // this will execute when state changes
}, [state]);
const onClick = () => {};
// const onClick = useCallback(() => {},[])
return (
<main>
<button onClick={() => setState(1 + state)}>{state}</button>
<MemoComponent onClick={onClick} />
</main>
);
}
useEffect
It's the alternative for the class component lifecycle methods componentDidMount, componentWillUnmount, componentDidUpdate, etc. You can also use it to create a side effect when dependencies change, i.e. "If some variable changes, do this".
Whenever you have some logic that is executed as reaction to a state change or before a change is about to happen.
useEffect(() => {
// execute when state changed
() => {
// execute before state is changed
}
}, [state]);
OR
useEffect(() => {
// execute when state changed
() => {
// execute before state is changed
}
}, []);
useCallback
On every render, everything that's inside a functional component will run again. If a child component has a dependency on a function from the parent component, the child will re-render every time the parent re-renders even if that function "doesn't change" (the reference changes, but what the function does won't).
It's used for optimization by avoiding unnecessary renders from the child, making the function change the reference only when dependencies change. You should use it when a function is a dependency of a side effect e.g. useEffect.
Whenever you have a function that is depending on certain states. This hook is for performance optimization and prevents a function inside your component to be reassigned unless the depending state is changed.
const myFunction = useCallback(() => {
// execute your logic for myFunction
}, [state]);
Without useCallback, myFunction will be reassigned on every render. Therefore it uses more compute time as it would with useCallback.
I have a component that's supposed to read a property from the component (which is either string "fill" or string "stroke") and pull the according key from an object to read it's context.
This gets mounted as soon as an object is selected, accesses the active object and pulls out it's color either as a fill color or a stroke color.
useEffect(() => {
setButtonColor(context.objects.getActiveObject()[props.type]);
}, []); //on mount
Mounting it like this:
<ColorPicker type="fill" />
<ColorPicker type="stroke" />
This supposed to run only once on mount. I thought when the dep array is empty, it runs on any case once when it's mounted.
So how do I run something once on mount utilizing props and context?
And why does it need a dependency at all when I want it to ALWAYS run only ONCE on mount, no matter what?
It's best to move away from the thinking that effects run at certain points in the lifecycle of a component. While that is true, a model that might help you better get to grips with hooks is that the dependency array is a list of things that the effect synchronizes with: That is, the effect should be run each time those things change.
When you get a linter error indicating your dependency array is missing props, what the linter is trying to tell you is that your effect (or callback, or memoization function) rely on values that are not stable. It does this because more often than not, this is a mistake. Consider the following:
function C({ onSignedOut }) {
const onSubmit = React.useCallback(() => {
const response = await fetch('/api/session', { method: 'DELETE' })
if (response.ok) {
onSignedOut()
}
}, [])
return <form onSubmit={onSubmit}>
<button type="submit">Sign Out</button>
</form>
}
The linter will issue a warning for the dependency array in onSubmit because onSubmit depends on the value of onSignedOut. If you were to leave this code as-is, then onSubmit will only be created once with the first value of onSignedOut. If the onSignedOut prop changes, onSubmit won't reflect this change, and you'll end up with a stale reference to onSignedOut. This is best demonstrated here:
import { render } from "#testing-library/react"
it("should respond to onSignedOut changes correctly", () => {
const onSignedOut1 = () => console.log("Hello, 1!")
const onSignedOut2 = () => console.log("Hello, 2!")
const { getByText, rerender } = render(<C onSignedOut={onSignedOut1} />)
getByText("Sign Out").click()
// stdout: Hello, 1!
rerender(<C onSignedOut={onSignedOut2} />)
getByText("Sign Out").click()
// stdout: Hello, 1!
})
The console.log() statement does not update. For this specific example that would probably violate your expectations as a consumer of the component.
Let's take a look at your code now.
As you can see, this warning is essentially stating that your code might not be doing what you think it is doing. The easiest way to dismiss the warning if you're sure you know what you're doing is to disable the warning for that specific line.
useEffect(() => {
setButtonColor(context.objects.getActiveObject()[props.type]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); //on mount
The correct way to do this would be to place your dependencies inside of the array.
const { type } = props
useEffect(() => {
setButtonColor(context.objects.getActiveObject()[type]);
}, [context, type]);
This would, however, change the button colour every time type changed. There's something to note here: You're setting state in response to props changing. That's called derived state.
You only want that state to be set on the initial mount. Since you only want to set this on the initial mount, you could simply pass your value to React.useState(initialState), which would accomplish exactly what you want:
function C({ type }) {
const initialColor = context.objects.getActiveObject()[type];
const [color, setButtonColor] = React.useState(initialColor);
...
}
This still leaves the problem that the consumer might be confused as to why the view does update when you change the props. The convention that was common before functional components took off (and one I still use) is to prefix props that are not monitored for changes with the word initial:
function C({ initialType }) {
const initialColor = context.objects.getActiveObject()[initialType];
const [color, setButtonColor] = React.useState(initialColor);
}
You should still be careful here, though: It does mean that, for the lifetime of C, it will only ever read from context or initialType once. What if the value of the context changes? You might end up with stale data inside of <C />. That might be acceptable to you, but it's worth calling out.
React.useRef() is indeed a good solution to stabilize values by only capturing the initial version of it, but it's not necessary for this use-case.
This is my workaround for the issue:
Set the color to a variable and then use that variable to set the button color on mount of the component.
const oldColor = useRef(context.objects.getActiveObject()[props.type]);
useEffect(() => {
setButtonColor(oldColor.current);
}, []); //on mount
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.
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