Reputation: 6467
I'm having an issue with how props interact with state as created by hooks.
In the example below, the state as held in the hook is always one behind that as defined in the props.
I think I understand why this is happening. The lifecycle seems to be something like this:
handleClick
is defined with access to the scope based on this renderhandleClick
handleClick
still has access to only the scope as it was defined in the render, and as such, items
is still emptyadd
is called; the state in the parent component is updated, but setHookItems
will use the state which was defined in the props when handleClick
was definedhookItems
gets set to items
, which is emptyThis is for the first render, but I think the principle applies for all others; the click handler only has access to the scope before the new item is added, and as such, sets the hook state based on what it has as the state for items
.
I may be wrong in my assumptions about how this transpires but it seems to be consistent with what I'm seeing.
So, to the actual question; how can I update an item in hook state based on the latest props, from some kind of handler, rather than that of the props at the time of the last render?
const { useState } = React;
const App = ({items, add}) => {
const [hookItems, setHookItems] = React.useState([]);
const handleClick = () => {
add(Math.floor(Math.random() * 10));
setHookItems(items);
}
return (
<div>
<p><span class="title">All Items: </span>{items.map(x => (<span>{x}</span>))}</p>
<p><span class="title">Hook Items: </span>{hookItems.map(x => (<span>{x}</span>))}</p>
<button onClick={handleClick}>Add</button>
</div>
);
}
const Root = () => {
const [items, setItems] = useState([]);
return (
<div>
<App items={items} add={item => setItems([...items, item])} />
</div>
)
}
ReactDOM.render(
<Root />,
document.getElementById('app')
);
.title { display: inline-block; width: 100px; text-align: right; margin-right: 20px;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Upvotes: 0
Views: 65
Reputation: 33399
The issue is that every time a component is rendered it gets a set of values for its state and props - but those values don't change until the next render.
So if I have state like:
const [count, setCount] = useState(0);
If I do setCount(count + 1)
, it queues up a new render where the count
variable will be 1
, but it doesn't immediately cause the count
variable to become 1
. (It can't - const
variables can't be changed) So if I logged count
immediately after setting, it'd still be zero.
In your example there's a level of indirection, where the setter is in the parent component, triggered by the add
function, and the value is passed as a prop to the child, but that doesn't change the behavior.
In terms of design, the problem here is that you're using the "props to state" pattern, which is usually an anti-pattern. Your components generally shouldn't save props as state, you should use the props directly. Otherwise you run into issues where the props and state get out of sync.
The react blog has an article You Probably Don't Need Derived State which goes into this in more detail.
Upvotes: 1