I'm refactoring to use Hooks and I've hit a very confusing wall
I have a basic functional component like so:
export const MakeComponent = props => {
const { path, value, info, update } = props;
const [val, setVal] = useState(value);
console.log(value, val); // abc undefined
return (...)
}
The log returns abc undefined - i.e. value in props is definitely defined, but the first argument returned from useState(value) is undefined
Just to test that hooks were working at all, I tried useState("abc") and that logs abc as expected.
I have no idea what I'm doing wrong - any ideas?
React version: 16.8.6
EDIT here is the parent component - nothing fancy going on here as far as I can see!
<MakeComponent
path={key}
value={item[key]}
info={value}
update={updateDiffs}
/>
As it is alluded to in the comments, useState(initialState) (or whatever you call it) will only use initialState on the first render.
During the initial render, the returned state (state) is the same as the value passed as the first argument (initialState).
(React Docs, emphasis mine)
After this, on re-renders the useState function does not change the state based on new changes to the props passed in.
To make changes reflected everytime value changes, register an effect on the input prop like so
export const MakeComponent = props => {
const { path, value, info, update } = props;
const [val, setVal] = useState(value);
useEffect(() => { setVal(value)}, [value] )
return (...)
}
Note that just setting state based on the props changing is a bit of an anti-pattern, as MonsterBucket notices you could just rely directly on the props changing to trigger a re-render:
export const MakeComponent = props => {
const { path, value, info, update } = props;
const [val, setVal] = useState(value);
if (val !== value) { // don't update unnecessarily
setVal(value);
}
return (...)
}
And instead reserve useEffect for side effects, mostly those outside of the React render cycle.
To see examples of these, have a look as these ReactJs docs - You might not need an effect, which covers lots of other examples.
Here you have to add useEffect if you want to update the state on props change, which will listen to prop change & will update the state value accordingly
here is the updated snippet
export const MakeComponent = props => {
const { path, value, info, update } = props;
const [val, setVal] = useState(value);
useEffect(() => { setVal(value)}, [value] )
return (<div>{val}</div>)
}
Attching sandbox link
https://codesandbox.io/s/confident-agnesi-ohkq7?file=/src/MakeComponent.js
By the time you pass the prop value to useState the value of it can be yet to set. value itself might have been undefined yet.
Also setState is not truly sync so if the useState uses same mechanism as setState your state val might not be set to value yet as the initial value.
In such cases, using props as the initial values to states, you should use a side effect with no dependency. Once the first render achieved the effect will run and set your state with prop. You can let the initial value of the component be undefined passing nothing with no problems.
export const MakeComponent = props => {
const { path, value, info, update } = props;
const [val, setVal] = useState();
// get initial state after initial render
useEffect(() => {
setVal(value)
}, [])
console.log(value, val); // abc undefined then will log abc abc
return (...)
}
Just keep in mind that props in React are meant to be read-only, state is for read and write. But it is perfectly fine, and no not an anti pattern, if you use a prop just as an initial value for a state and use the state you have set with the prop after that instead of the prop. That is for consistency since you might have two different values at a time from a prop and a state in circumstances.
Your case might need to care for the value of the prop at an arbitrary time depending on you needs as stressed in one of the above answers. You question does not state that. Still, if so, you can add the prop to the dependency array of the effect that sets the state with that prop, and yes write separate effects for other props you want the same, well, effect.
If you don't need writing for that data you do not need that pattern at all, just use prop and it will be up to date and you will have consistency. But you apparently do so I hope the pattern I suggest above works for you as it does for me.
Related
I have a hook that gets data from another hook
const useIsAdult = () => {
const data = useFormData();
return data.age > 18;
}
This hook returns true or false only, however the useFormData is constantly being updated. Everytime useFormData reruns, it will rerender my component. I only want my component to rerender if the result of useIsAdult changes.
I know this can obviously be solved by implementing some logic via react-redux and using their useSelector to prevent rendering, but I'm looking for a more basic solution if there is any.
Before you say useMemo, please read question carefully. The return value is a boolean, memo here doesnt make any sense.
Even with returned useMemo in useIsAdult parent component will be rerendered. This problem is reason why I rarely create custom hooks with other hooks dependency. It will be rerender hell.
There I tried to show that useMemo doesnt work. And using useMemo for components its wrong way. For components we have React.memo.
const MyComponent = memo(({ isAdult }: { isAdult: boolean }) => {
console.log("renderMy");
return <h1>Hello CodeSandbox</h1>;
});
And memo will help to prevent rendering of MyComponent. But parent component still render.
https://codesandbox.io/s/interesting-lumiere-xbt4gg?file=/src/App.tsx
If you can modify useFormData maybe useRef can help. You can store data in useRef but it doesnt trigger render.
Something like this but in useFormData:
onst useIsAdult = () => {
const data = useFormData();
const prevIsAdultRef = useRef();
const isAdult = useMemo(() => data.age > 18, [data]);
if (prevIsAdultRef.current === isAdult) {
return prevIsAdultRef.current;
}
prevIsAdultRef.current = isAdult;
return isAdult;
};
I dont know how useFormData works, then you can try it self with useRef.
And if useFormData gets data very often and you cant manage this, you can use debounce or throttle to reduce number of updates
Memoize hook
const useIsAdult = () => {
const data = useFormData();
return useMemo(() => data.age > 18, [data.age]);
}
Here, useMemo will let you cache the calculation or multiple renderes, by "remembering" previous computation. So, if the dependency (data.age) doesn't change then it will use simply reuse the last value it returned, the cached one.
Memoize component expression
useMemo can also be used to memoize component expressions like this: -
const renderAge = useMemo(() => <MyAge age={data.age} />, [data.age]);
return {renderAge}
Here, MyAge will only re-render if the value of data.age changes.
I'm refactoring to use Hooks and I've hit a very confusing wall
I have a basic functional component like so:
export const MakeComponent = props => {
const { path, value, info, update } = props;
const [val, setVal] = useState(value);
console.log(value, val); // abc undefined
return (...)
}
The log returns abc undefined - i.e. value in props is definitely defined, but the first argument returned from useState(value) is undefined
Just to test that hooks were working at all, I tried useState("abc") and that logs abc as expected.
I have no idea what I'm doing wrong - any ideas?
React version: 16.8.6
EDIT here is the parent component - nothing fancy going on here as far as I can see!
<MakeComponent
path={key}
value={item[key]}
info={value}
update={updateDiffs}
/>
As it is alluded to in the comments, useState(initialState) (or whatever you call it) will only use initialState on the first render.
During the initial render, the returned state (state) is the same as the value passed as the first argument (initialState).
(React Docs, emphasis mine)
After this, on re-renders the useState function does not change the state based on new changes to the props passed in.
To make changes reflected everytime value changes, register an effect on the input prop like so
export const MakeComponent = props => {
const { path, value, info, update } = props;
const [val, setVal] = useState(value);
useEffect(() => { setVal(value)}, [value] )
return (...)
}
Note that just setting state based on the props changing is a bit of an anti-pattern, as MonsterBucket notices you could just rely directly on the props changing to trigger a re-render:
export const MakeComponent = props => {
const { path, value, info, update } = props;
const [val, setVal] = useState(value);
if (val !== value) { // don't update unnecessarily
setVal(value);
}
return (...)
}
And instead reserve useEffect for side effects, mostly those outside of the React render cycle.
To see examples of these, have a look as these ReactJs docs - You might not need an effect, which covers lots of other examples.
Here you have to add useEffect if you want to update the state on props change, which will listen to prop change & will update the state value accordingly
here is the updated snippet
export const MakeComponent = props => {
const { path, value, info, update } = props;
const [val, setVal] = useState(value);
useEffect(() => { setVal(value)}, [value] )
return (<div>{val}</div>)
}
Attching sandbox link
https://codesandbox.io/s/confident-agnesi-ohkq7?file=/src/MakeComponent.js
By the time you pass the prop value to useState the value of it can be yet to set. value itself might have been undefined yet.
Also setState is not truly sync so if the useState uses same mechanism as setState your state val might not be set to value yet as the initial value.
In such cases, using props as the initial values to states, you should use a side effect with no dependency. Once the first render achieved the effect will run and set your state with prop. You can let the initial value of the component be undefined passing nothing with no problems.
export const MakeComponent = props => {
const { path, value, info, update } = props;
const [val, setVal] = useState();
// get initial state after initial render
useEffect(() => {
setVal(value)
}, [])
console.log(value, val); // abc undefined then will log abc abc
return (...)
}
Just keep in mind that props in React are meant to be read-only, state is for read and write. But it is perfectly fine, and no not an anti pattern, if you use a prop just as an initial value for a state and use the state you have set with the prop after that instead of the prop. That is for consistency since you might have two different values at a time from a prop and a state in circumstances.
Your case might need to care for the value of the prop at an arbitrary time depending on you needs as stressed in one of the above answers. You question does not state that. Still, if so, you can add the prop to the dependency array of the effect that sets the state with that prop, and yes write separate effects for other props you want the same, well, effect.
If you don't need writing for that data you do not need that pattern at all, just use prop and it will be up to date and you will have consistency. But you apparently do so I hope the pattern I suggest above works for you as it does for me.
What is the best way to update single state value, if specific two props changes, without useEffect, since performance point of view we should not be setting state inside useEffect.
For Example:
const NestedList = ({childItems, parentItems, selected, ...rest}) => {
const [currentItems, setCurrentItems] = useState({}); //assume this could be hash object
// Wrong X
useEffect(() => setCurrentItems(parentItems), [parentItems])
// Wrong X
useEffect(() => setCurrentItems(childItems), [childItems])
return ...
React component updates on both State change and Prop change. So you don't have to do anything additonal
Try below in single use effect. You can also compare previous props with new props.
useEffect(() => {
setCurrentItems(whateverthevalueis)
}, [prop1,prop2]);
What i try to figure out is the why in this case.
Why do you need a state if you always want to change it?
Why do the two props replace each other in the state?
There is nothing wrong with setting the state in a useEffect as alot of people already have said. However useEffect should be used for side effects.
There is no golden solution for all cases.
Use a state if you set the intial state with props and then the component update the state itself.
const processItems = (childItems, parentItems, ) => {
// Do what you want to create one state
return childItems
}
const NestedList = ({childItems, parentItems, selected, ...rest}) => {
// use props to set the inital state
const [currentItems, setCurrentItems] = useState(processItems(childItems, parentItems))
// We can later mutate the state, however if the props change we do not care
If the parent always pass the state as a prop, you do not need a state since the state is handled higher up.
const NestedList = ({childItems, parentItems, selected, ...rest}) => {
// No need for a state here, we can just use the props directly and
// the parent will pass the updated props when the state changes
If the parent pass several props and you need to calculate stuff based on the props, use memo hook.
const NestedList = ({childItems, parentItems, selected, ...rest}) => {
const currentItems = useMemo(() => {
// Add the condition of what items to show here
if(parentItems.length > 0) return parentItems
return childItems
}, [childItems,parentItems])
return ...
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.
I am looking for a way to perform more advanced comparison instead of the second parameter of the useEffect React hook.
Specifically, I am looking for something more like this:
useEffect(
() => doSomething(),
[myInstance],
(prev, curr) => { /* compare prev[0].value with curr[0].value */ }
);
Is there anything I missed from the React docs about this or is there any way of implementing such a hook on top of what already exists, please?
If there is a way to implement this, this is how it would work: the second parameter is an array of dependencies, just like the useEffect hook coming from React, and the third is a callback with two parameters: the array of dependencies at the previous render, and the array of dependencies at the current render.
you could use React.memo function:
const areEqual = (prevProps, nextProps) => {
return (prevProps.title === nextProps.title)
};
export default React.memo(Component, areEqual);
or use custom hooks for that:
How to compare oldValues and newValues on React Hooks useEffect?
In class based components was easy to perform a deep comparison. componentDidUpdate provides a snapshot of previous props and previous state
componentDidUpdate(prevProps, prevState, snapshot){
if(prevProps.foo !== props.foo){ /* ... */ }
}
The problem is useEffect it's not exactly like componentDidUpdate. Consider the following
useEffect(() =>{
/* action() */
},[props])
The only thing you can assert about the current props when action() gets called is that it changed (shallow comparison asserts to false). You cannot have a snapshot of prevProps cause hooks are like regular functions, there aren't part of a lifecycle (and don't have an instance) which ensures synchronicity (and inject arguments). Actually the only thing ensuring hooks value integrity is the order of execution.
Alternatives to usePrevious
Before updating check if the values are equal
const Component = props =>{
const [foo, setFoo] = useState('bar')
const updateFoo = val => foo === val ? null : setFoo(val)
}
This can be useful in some situations when you need to ensure an effect to run only once(not useful in your use case).
useMemo:
If you want to perform comparison to prevent unecessary render calls (shoudComponentUpdate), then useMemo is the way to go
export React.useMemo(Component, (prev, next) => true)
But when you need to get access to the previous value inside an already running effect there is no alternatives left. Cause if you already are inside useEffect it means that the dependency it's already updated (current render).
Why usePrevious works
useRef isn't just for refs, it's a very straightforward way to mutate values without triggering a re render. So the cycle is the following
Component gets mounted
usePrevious stores the inital value inside current
props changes triggering a re render inside Component
useEffect is called
usePrevious is called
Notice that the usePrevious is always called after the useEffect (remember, order matters!). So everytime you are inside an useEffect the current value of useRef will always be one render behind.
const usePrevious = value =>{
const ref = useRef()
useEffect(() => ref.current = value,[value])
}
const Component = props =>{
const { A } = props
useEffect(() =>{
console.log('runs first')
},[A])
//updates after the effect to store the current value (which will be the previous on next render
const previous = usePrevious(props)
}
I hit the same problem recently and a solution that worked for me is to create a custom useStateWithCustomComparator hook.
In your the case of your example that would mean to replace
const [myInstance, setMyInstance] = useState(..)
with
const [myInstance, setMyInstance] = useStateWithCustomComparator(..)
The code for my custom hook in Typescript looks like that:
const useStateWithCustomComparator = <T>(initialState: T, customEqualsComparator: (obj1: T, obj2: T) => boolean) => {
const [state, setState] = useState(initialState);
const changeStateIfNotEqual = (newState: any) => {
if (!customEqualsComparator(state, newState)) {
setState(newState);
}
};
return [state, changeStateIfNotEqual] as const;
};