Do you want to request a feature or report a bug?
More of a question / discussion as I could not find it in the documentation.
What is the current behavior?
Currently when using hooks like useState
and useReducer
, any updates to props used by their initial state has no effect on the state managed by these hooks. For instance, consider this example from the Hooks API Reference:
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
</>
);
}
Now let's suppose that a parent component initially renders a Counter
with initialCount
set to 0
. But later on the initialCount
prop passed by the parent component is updated to let's say 11
. I would expect that a change in the initialCount
prop, would then trigger a change in the count
state but that doesn't happen.
At first I wasn't sure why the count
was not updating as I expect any data changes to flow through where they are used. It seems like that the initial state is only used initially as the name suggests in useState
or useReducer
and any changes to it are ignored by these hooks.
However, we are still able to address the problem by using effects:
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
useEffect(() => setCount(initialCount), [initialCount]);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
</>
);
}
The need to use effects wasn't immediate to me but it makes sense as we would generally hook such behavior into component lifecycle methods like componentDidUpdate
. I am curious as to why this doesn't already happen automatically through useState
and useReducer
, and wondering if this is the correct way to listen to such prop changes and accordingly update state, or is there some better way?
We could also extract this logic into a custom hook:
function useStateWithEffect(initialState) {
const [state, setState] = useState(initialState);
useEffect(() => setState(initialState), [initialState]);
return [state, setState]
}
// Haven't tested the reducer but as an idea
function useReducerWithEffect(reducer, initialArg, init = x => x) {
const reducerWithStateUpdate = (state, action) => action.type === 'updateInitialArg' ? init(action.initialArg) : reducer(state, action);
const [state, dispatch] = useReducer(reducerWithStateUpdate, initialArg, init);
useEffect(() => dispatch({ type: 'updateInitialArg', initialArg }), [initialArg]);
return [state, dispatch]
}
I spent some time trying to figure this out so if someone has reference to material already discussing this problem, please do share as I couldn't find it while going through the docs / posts about hooks.
I am curious as to why this doesn't already happen automatically through
useState
anduseReducer
, and wondering if this is the correct way to listen to such prop changes and accordingly update state, or is there some better way?
Using an effect to update the state when the prop changes is the correct approach if that is your desired behavior. The reason that it doesn't do this by default is because this usually _isn't_ what you want.
When a component takes a prop that initializes some state, it's assumed that the component now "owns" that state and can update it without worrying about the parent component throwing away its changes. If you try syncing the prop to state you risk end up losing local changes. Syncing state to props usually means you have more than one source of truth for that value, which can be confusing.
If you want that parent component to control that value then you typically lift the state up so that it lives in the parent and then just pass down a way for the child to update the parent's state, like a controlled component.
The reason that it doesn't do this by default is because this usually isn't what you want
Hi @aweary, do you mind to justify this? I think that is is very unintuitive, because the primary reason I wanted to use hooks is because I want to get rid of those syncing state with props when using class components with componentWillReceiveProps
, componentShouldUpdate
etc, those are nightmares to maintain.
But then now you are telling us that we should update the state from props manually? I think this defeats the purpose of hooks, they should simplify development, but it seems like remnants of the past are still haunting us.
Anyway, how should I update the state if I pass in props to the component that uses useReducer
?
For example, here's my code:
const MyComponent = (props) => {
const [state, dispatch] = React.useReducer(myReducer, props.initialState)
// If we are using React.useState, we can update the state from props using effects
// But how about when we are using React.useReducer?
}
The reason that it doesn't do this by default is because this usually isn't what you want
Hi @aweary, do you mind to justify this? I think that is is very unintuitive, because the primary reason I wanted to use hooks is because I want to get rid of those syncing state with props when using class components with
componentWillReceiveProps
,componentShouldUpdate
etc, those are nightmares to maintain.But then now you are telling us that we should update the state from props manually? I think this defeats the purpose of hooks, they should simplify development, but it seems like remnants of the past are still haunting us.
Anyway, how should I update the state if I pass in props to the component that uses
useReducer
?For example, here's my code:
const MyComponent = (props) => { const [state, dispatch] = React.useReducer(myReducer, props.initialState) // If we are using React.useState, we can update the state from props using effects // But how about when we are using React.useReducer? }
Hello, did you find a solution for this?
@anatoliikarpiuk I did as what @aweary suggested, I lift the state up.
There's a third solution that might be simpler depending on your case. Add a key
prop to Counter
in the parent component so that any changes will cause a re-mount.
<Counter key={initialCount} />
Not sure lifting state up is always the best way, but it might just be my bad understanding.. Let's imagine I want to have a list of selectable items loaded by AJAX. I'd separate this into two components: ListLoader
, which would load the list items via fetch
or something similar, and pass the items as a prop to List
, which would render the list and keep track of the IDs of the selected items. If I now change a prop of ListLoader
to e.g. load a different category of items, ListLoader
will rerender (and reload the items), but the selection state within List
still contains IDs from the previous category. Should I lift the state of List
into ListLoader
? But what if ListLoader
actually loads n
lists, and each list should be rendered by its individual List
? The selection state is something internal to List
, and it's irrelevant outside its scope; it's also the only source of truth regarding selected items in that particular list, it's just not an "independent" truth.. does that make sense? :-D
just listen to the props' unique value change, then update your local state!
import React, {
useState,
useEffect,
} from 'react';
import ExportableTable from '@/components/ExportableTable';
import { generateFilename } from '@/utils/exportUtils';
const TrendTable = ({
startDate,
endDate,
dataSource,
moduleName,
analysisName,
units,
initCurrent,
}) => {
const [current, setCurrent] = useState(initCurrent);
const [tableName, setTableName] = useState(analysisName);
// const [unmounted, setUnmounted] = useState(false);
useEffect(() => {
console.log(`did mount`);
// props change
if(tableName !== analysisName) {
console.log(`tableName`, tableName, analysisName);
setCurrent(1);
setTableName(analysisName);
}
let unmounted = false;
if(!unmounted) {
// cancel update state
}
return () => unmounted = true;
}, [analysisName, tableName]);
const columns = [
{
title: 'date',
dataIndex: 'date',
key: 'date',
align: 'center',
width: 150,
},
{
title: analysisName,
dataIndex: 'value',
key: 'value',
align: 'center',
render: text => `${text} ${units}`,
width: 150,
},
];
const total = dataSource ? dataSource.length : 0;
console.log(`total`, total, current);
const filename = generateFilename({
moduleName,
analysisName,
startDate,
endDate,
});
return (
<ExportableTable
filename={filename}
size="small"
bordered={false}
rowKey="name"
columns={columns}
pagination={{
current,
pageSize: 10,
showSizeChanger: true,
pageSizeOptions: ['5', '10', '20'],
showQuickJumper: true,
showTotal: total => <span>{total} items</span>,
}}
dataSource={dataSource}
defaultCurrent={1}
onChange={(p) => {
setCurrent(p.current);
}}
/>
);
};
export {
TrendTable,
};
export default TrendTable;
I've faced the same problem, I would like to have getDerivedStateFromProps
functionality:
static getDerivedStateFromProps(props, state) {
let newState = null;
if (props.value !== state._prevPropsValue) {
newState = { _prevPropsValue: props.value };
if (props.value !== state.value) {
newState.value = props.value;
}
}
return newState;
}
Using hooks it will be accomplished with extra rerender:
function useControlableState(initialValue) {
const [value, setValue] = useState(initialValue);
const [prevValue, setPrevValue] = useState(initialValue);
if (prevValue !== initialValue) {
setPrevValue(initialValue);
if (initialValue !== value) {
setValue(initialValue);
}
}
return [value, setValue];
}
As @arvinio suggested, the key prop may be a simple solution in this case.
I want to add a note: It may be a good idea to prefix props you want derive state from with initial and save them with by using useState.
This will save you from an inconsistent state when in case you forget to add the key prop.
import { computeProp } from '../'
const Component = ({ initialProp }) => {
const [computedProp] = useState(() => computeProp(initialProp))
return computedProp
}
<Component key={value} initialProp={value} />
Most helpful comment
Using an effect to update the state when the prop changes is the correct approach if that is your desired behavior. The reason that it doesn't do this by default is because this usually _isn't_ what you want.
When a component takes a prop that initializes some state, it's assumed that the component now "owns" that state and can update it without worrying about the parent component throwing away its changes. If you try syncing the prop to state you risk end up losing local changes. Syncing state to props usually means you have more than one source of truth for that value, which can be confusing.
If you want that parent component to control that value then you typically lift the state up so that it lives in the parent and then just pass down a way for the child to update the parent's state, like a controlled component.