Say I want to create the often wanted useInputState hook:
function useInputState(initialValue) {
const [value, setValue] = useState(initialValue);
const onChange = useCallback(
e => setValue(e.target.value),
[ /* ??? */ ]
);
return { value, onChange };
}
Would I need to add the setValue setter function to the callback's dependencies?
Can we count on the setter to always stay the same?
This seems to work when I try it, but is this good practice? Or should we assume ANYTHING in the callback's closure can change and affect its implementation?
(I can think of more examples that eventually raise the same question)
Yes, the setter from useState and similarly the dispatch from useReducer do not change.
This portion of the docs covers the pattern of passing the dispatch method to descendant components via context and contains the following:
If you use context to pass down the state too, use two different
context types — the dispatch context never changes, so components that
read it don’t need to rerender unless they also need the application
state.
Yes, useState state setter is the same function; even if it wouldn't, it would be expected to update the state. It is supposed to be used like the question shows.
The common usage is explained better in useReducer documentation:
useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.
And useState uses useReducer internally.
The concerns with a scope where useState was called are applicable only to useState state value which stays the same in the scope of a callback where it's used, e.g. this problem. In case current state should be used, it should be retrieved with state updater function, like setValue(currentState => currentState + 1).
Related
Similar to:
How to use `setState` callback on react hooks but I am trying to do this with functional component specifically and I am not trying to replicate the exact same scenario.
React hooks: accessing up-to-date state from within a callback but I am not passing the state from the callback
So say I have a notify() method that I want to be fired AFTER the states have changed, not during state change. Also after I click.
something like
const handleClick = useCallback(()=>{
const [state,setState] = useState("bar");
...
setState("foo", ()=> {
... at this point `state` should be "foo" ...;
notify("yo we're set. but I am not passing the current state to you");
})
});
...
a component that is child of the context
const { state } = useSomeContext();
subscribe(()=>{
console.log("I want ", state, " to be the updated value, but I get the existing value);
});
useStateRef lets the callback know but the ones listening when they query the value that is in the state may not get the updated value.
useStateCallback does not solve the problem for me either because it's the callback that has the value.
The workaround I sort of ended up with (still testing it) is to utilize a useRef to the value somewhat like useStateRef and do stateRef.current instead of just state
When you change the state via the setter, it will cause the current component to re-render. If the component you want to 'notify' is within the current component and has a dependency on the state value that changed e.g.:
return (
<>
<SomeOtherComponent something={state}/>
</>
)
... then that component will re-render as well.
If the component you want to notify isn't in the render path, you can make the state a dependency of a useEffect so your useEffect will run whenever the state change (and do your notification there). You can have multiple useEffects with with dependencies on different state. Example:
useEffect(()=> {
console.log('hello, state is',state)
}, [state])
And you can look into context and other mechanisms to communicate state. You should read this article. I think it's very inline with what you'd like to do.
Certainly don't do anything in your useState setter other than the minimal needed to return the new state.
I'm making a checkbox component for my project, and I have it set so that whenever is isChecked state changes, useEffect will run a returnFunction (defined in the props), like so:
export function JCheckbox({ returnFunction }: JCheckboxProps) {
const [isChecked, setIsChecked] = React.useState(defaultState)
React.useEffect(() => {
returnFunction(isChecked)
}, [isChecked])
When I do this, my compiler wants me to include returnFunction in the useEffect dependency array. I'm confused why I would need to track that, it's not used at all anywhere else in the function, and it doesn't change either. The code works perfectly fine without it.
You should add your function (returnFunction) to useEffect dependency because your function reference (returnFunction) may be changed, So react ask you to add your function (returnFunction) in useEffect dependency to recognize that and update them by new function reference.
returnFunction is certainly used as it is called inside the effect, and it could certainly change as the parent component could pass in different props, in which case your component would continue to call the previously passed in function. So the linter is correct, returnFunction is a dependency. However adding it makes it slightly more difficult to detect changes of isChecked, as changing returnFunction would also trigger the effect. Thus it seems way more elegant to call returnFunction from within setIsChecked:
const [isChecked, _setIsChecked] = useState(false);
function setIsChecked(value: boolean) {
returnFunction(value);
_setIsChecked(value);
}
However in the case given it seems that the state should rather be maintained by the parent component, consider lifting it up.
returnFunction is read from props so you probably must include it in the array of dependencies.
You should almost always include every variable and function you declare inside React and use in useEffect callback in the array of dependencies.
Eslint does not expect you to include setState functions and refs that you create in the same component that you execute useEffect.
Edit:
If you have a handleChange event handler and isChecked is toggled whenever the onChange method is invoked, you probably do not need useEffect. Call returnFunction function with the new isChecked value.
I am trying to understand the exact difference in terms of how the re-render of function component is caused in one case using plain setState V/s other case which uses functional state update
The relevant code snippet is as below
Case 1 : Causes re-render of the component
const onRemove = useCallback(
tickerToRemove => {
setWatchlist(watchlist.filter(ticker => ticker !== tickerToRemove));
},
[watchlist]
);
Case 2 : Does not cause re-render
const onRemove = useCallback(tickerToRemove => {
setWatchlist(watchlist =>
watchlist.filter(ticker => ticker !== tickerToRemove)
);
}, []);
Full example of both the use-cases can be seen on;
https://codesandbox.io/s/06c-usecallback-final-no-rerenders-bsm64?file=/src/watchlistComponent.js
https://codesandbox.io/s/06b-usecallback-usememo-ngwev?file=/src/watchlistComponent.js:961-970
UPDATE
Full article link
https://medium.com/#guptagaruda/react-hooks-understanding-component-re-renders-9708ddee9928#204b
I am a bit confused as to how the re-render of child components is prevented.
In the article it says
"Thankfully, setter function from useState hook supports a functional
variant which comes to our rescue. Instead of calling setWatchlist
with the updated watchlist array, we can instead send a function that
gets the current state as an argument"
However, I am a bit confused whether the re-rendering of child components is prevented because we use empty array (as [] does not changes between renders) V/s prevented because of using setter variant of useState hook ?
Using a functional state update or not is rather irrelevant to the question you are asking about. You appear to be asking why (1) a callback with dependency triggers a rerender versus (2) a callback with empty dependency.
The answer is quite literally very simple. In version (2) you are providing a stable callback reference from the time the component mounts that never changes, whereas in (1) the callback reference changes when the dependency does. Remember that React components rerender when state or props update (a new callback reference is a new prop reference) or when the parent component rerenders. Since the onRemove prop is updating in (1) it triggers a rerender.
Recently I saw a few examples of passing setter functions into hook dependency arrays in my coworkers' React code, and it doesn't look right to me. For example:
const MyComponent = () => {
const [loading, setLoading] = useState(true);
useEffect(() => {
doSomeBigLongNetworkRequest();
setLoading(false);
}, [setLoading, /* other deps */]);
// ...
}
My feeling is that they have misunderstood the purpose of the dependency array, which, as I understand it, is to indicate which pieces of state to monitor so that the hook can fire again when they change, not to simply indicate that the hook needs to use the setLoading function. And since the setLoading function never actually changes, including it in the dependencies does nothing.
Am I correct, or does including the setter in the array make sense somehow? My other thought was that maybe this was just a linter error, since the linter cannot recognize that the function is a setter, and thinks it might change.
I should also add that in the instances I've seen, they've included the setter but not the variable. So in the example above, setLoading, but not loading would be in the dependency array, and the hook does not actually need the value of loading.
Yes, you are right there is no need to include them. Here is quote from docs:
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
In general again based on docs the recommendation about dependency array is:
If you use this optimization, make sure the array includes all values
from the component scope (such as props and state) that change over
time and that are used by the effect. Otherwise, your code will
reference stale values from previous renders.
This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 4 years ago.
In the past, we've been explicitly warned that calling setState({myProperty}) is asynchronous, and the value of this.state.myProperty is not valid until the callback, or until the next render() method.
With useState, how do I get the value of the state after explicitly updating it?
How does this work with hooks? As far as I can tell, the setter function of useState doesn't take a callback, e.g.
const [value, setValue] = useState(0);
setValue(42, () => console.log('hi callback');
doesn't result in the callback being run.
My other workaround in the old world is to hang an instance variable (e.g. this.otherProperty = 42) on the class, but that doesn't work here, as there is no function instance to reuse (no this in strict mode).
You can use useEffect to access the latest state after updating it. If you have multiple state hooks and would like to track the update on only part of them, you can pass in the state in an array as the second argument of the useEffect function:
useEffect(() => { console.log(state1, state2)}, [state1])
The above useEffect will only be called when state1 is updated and you shouldn't trust the state2 value from inside this function as it may not be the latest.
If you are curious about whether the update function created by useState is synchronous, i.e. if React batches the state update when using hooks, this answer provide some insight into it.
Well, if you refer to the relevant docs you'll find...
const [state, setState] = useState(initialState);
Returns a stateful value, and a function to update it.
During the initial render, the returned state (state) is the same as the value passed as the first argument (initialState).
The setState function is used to update the state. It accepts a new state value and enqueues a re-render of the component.
setState(newState);
During subsequent re-renders, the first value returned by useState will always be the most recent state after applying updates.
So your new state, whatever it is, is what you've just set in useState, i.e. the value of initialState. It goes directly to state, which updates reactively thereafter. Quoting further from the relevant docs:
What does calling useState do? It declares a “state variable”. Our variable is called count but we could call it anything else, like banana. This is a way to “preserve” some values between the function calls — useState is a new way to use the exact same capabilities that this.state provides in a class. Normally, variables “disappear” when the function exits but state variables are preserved by React.
If you'd like to do something whenever your state updates, then just use componentDidUpdate.(docs)