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:
Component renders
handleClick is defined with access to the scope based on this render
Button is clicked and calls handleClick
handleClick still has access to only the scope as it was defined in the render, and as such, items is still empty
add 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 defined
hookItems gets set to items, which is empty
This 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>
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.
Related
When a component re-renders as a result of calling the state setter function returned by useState, does the entire function component get called again or only parts of the function component get called?
function MyComponent() {
let [count, setCount] = useState(0);
const handleChange = () {
setCount((prevCount) => prevCount+1)
}
return (
<p onClick={handleChange}>
Count = {count}
</p>
)
}
In the above example, when we click the paragraph element, handleChange() gets called which in turn calls the state setter function setCount(). This causes our component to re-render. But when the component re-renders( or the function MyComponent gets called), the useState(0) function gets called again. This should set the initial value of count to 0 again. So shouldn't the counter be stuck at 0 instead of progressing to 1, 2, 3 and so on?
Whole functional component will be rerendered. If the props value which you are passing to the child component is not changing then you can prevent the rerendering of child component by using React.memo
Let say your component tree has parent and has children called childOne and childtwo.
if you change the state of the ChildOne;Only the ChildOne get rendered, Not Parent and childtwo get rendered. But if you change the state of the parent, whole component tree get retendered
This is the idea of hooks. They use closures.
If it was let count = 0, then yes, if would reset to 0, this is why it is let [count] = useState(0)
The whole thing gets re-render and everything is getting re-calculated.
The argument you pass in useState / useRef is used only once, when the component mounts. If it is expensive operation, it recalculate on each render (there are ways to improve this), but the value of it is used only once. Then when you call setState it updates the value.
They work like this:
let ref = {};
const useRef = (defaultValue) => {
if (!ref.current) {
ref.current = defaultValue
}
return ref
}
The hook argument is used only once.
I am trying to build an ecommerce website, and I hit a problem I cannot seem to resolve. I am very new to react and JS so have some patience please :)
I declared 4 useStates in my app.js:
const [elementeDinState, setElementeDinState] = useState([]);
const [currentCategorie, setCurrentCategorie] = useState("Acasa");
const [subCategorie, setSubcategorie] = useState([]);
const [cartContents, setCartContents] = useState([]);
const fetchData = useCallback(async () => {
const data = await getCategories();
setElementeDinState(data);
}, []);
useEffect(() => {
fetchData().catch(console.error);
}, [fetchData]);
const changeHeader = (dataFromMenuItem) => {
setCurrentCategorie(dataFromMenuItem);
};
const changeCopiiContent = (data1FromThere) => {
setSubcategorie(data1FromThere);
};
const changeCart = (dataFromCart) => {
setCartContents(dataFromCart);
};
I am passing the functions to change those states to different child components as props. my problem is, when I add items to cart it triggers a re render of my component (products listing component) that should not be affected by cartContents and that resets the state of said component to the initial value that changes the items being shown. does useState hook create a single global state comprised of all those states?
If these useState are defined in the app.js and then passed down, when a child will use them chasing the state will happen in the app.js so all the children of <App /> will be re-rendered.
I guess that your app.js looks similar:
function App() {
const [elementeDinState, setElementeDinState] = useState([]);
// ...and the other hooks and methods
return (
<cartContents setElementDinState={setElementeDinState} />
<ProductList />
)
}
In this case the state is in the component so when <CartContents /> changes it, it will trigger a re-render of the and all its children <ProductList /> included.
To avoid this problem think better when each piece of state needs to be and put the state as near as possibile to that component. For example, if the state of the cart does not influence the Product list. Move the useState in the <Cart /> component.
From what I understand, your problem is that you're simply resetting the cartContents state every time you call the changeCart function, correct?
What you probably want, is to add (or remove ?) the item to the cart, like this?
const changeCart = (dataFromCart) => {
setCartContents(oldContents => [...oldContents, dataFromCart]);
};
Here is a description of useState from the oficial site:
useState is a Hook (...). We call it inside a function component to add some local state to it
So it creates just a local state.
About your problem, We need more information, but I believe that some parent component of that widget is trying to render other component instead of your the component that you wanted (let's call it "ProblemComponent") and rendering you ProblemComponent from scratch again, before you can see it.
it's something like that:
function ParentComponent(props: any) {
const isLoading = useState(false);
// Some logic...
if(isLoading) {
return <LoadingComponent/>;
}
return <ProblemComponent/>;
}
If that doesn't work you can also try to use React.memo() to prevent the ProblemComponent to update when it props change.
well, seems like I wanted to change the way react works so I figured out a work around, based on what you guys told me. I declared the state of the productsComponent in the parent component and adding to cart now doesn't force a refresh of the items being shown. thank you!
I have a functional component managing several states, amongst other things a state, which stores the index, with which I am rendering a type of table, when clicking a suitable button. OnClick that button calls a callback function, which runs a click handler. That click handler changes the index state, to the same 'index' as the array entry, in which I store an object with information for the rendering of a child component.
I would expect, that onClick the state would change before the rendering happens, so the component could render correctly. Yet it only happens a render later.
I already tried calling the useEffect-hook, to re-render, when that index state changes, but that didn't help neither.
Here is a shortened version of the code:
export const someComponent = () => {
[index, setIndex] = useState(-1);
const handleClick = (id) => {
setIndex(id);
// This is a function, I use to render the table
buildNewComponent(index);
}
}
Further 'down' in the code, I got the function, which is rendering the table entries. There I pass the onClick prop in the child component of the table as following:
<SomeEntryComponent
onClick={() => handleClick(arrayEntry.id)}
>
// some code which is not affecting my problem
</SomeEntryComponent>
So as told: when that onClick fires, it first renders the component when one presses it the second time, because first then the state changes.
Could anyone tell me why that happens like that and how I could fix it to work properly?
As other have stated, it is a synchronicity issue, where index is being updated after buildComponent has been invoked.
But from a design standpoint, it would be better to assert index existence by its value, as opposed to flagging it in a handler. I don't know the details behind buildComponent, but you can turn it into a conditional render of the component.
Your component rendering becomes derived from its data, as opposed to manual creation.
export const someComponent = () => {
[index, setIndex] = useState(-1);
const handleClick = (id) => setIndex(id);
const indexHasBeenSelected = index !== -1
return (
<div>
{indexHasBeenSelected && <NewComponent index={index} />}
</div>
)
}
When calling buildNewComponent the index is not yet updated. You just called setState, there is no guarantee that the value is updated immediately after that. You could use id here or call buildNewComponent within a useEffect that has index as its dependency.
I believe that you can have the correct behavior if you use useEffect and monitor index changes.
export const someComponent = () => {
[index, setIndex] = useState(-1);
useEffect(() => {
// This is a function, I use to render the table
buildNewComponent(index);
}, [buildNewComponent, index])
const handleClick = (id) => {
setIndex(id);
}
}
The process will be:
When the use clicks and call handleClick it will dispatch setIndex with a new id
The index will change to the same id
The useEffect will see the change in index and will call buildNewComponent.
One important thing is to wrap buildNewComponent with useCallback to avoid unexpected behavior.
Recently I saw code akin to the following contrived example:
const MyComp = props => {
const [prevProps, setPrevProps] = useState(props)
if (props !== prevProps) {
setPrevProps(props);
// do something else...
}
}
The component uses some kind of derived state to remember the previous props. If the memory location of props has changed (props !== prevProps), it will sync the new props to useState.
Yeah, it doesn't make that much sense, but my point is: Does React make any guarantees, that the object reference of props stays the same, given no contained property of props has changed?
At least, this seems to be the case for the tests I have done. If a parent component changes any of the props, there will be a new props object created.
I would appreciate any official documentation link proving this.
Does React make any guarantees, that the object reference of props stays the same, given no contained property of props has changed?
No, it doesn't.
If a parent component changes any of the props, there will be a new props object created.
If the parent re-renders, the child will always receive its props in a new object, even if nothing in it has changed.
If you want to prevent that, you will have to use
PureComponent or React.memo which will perform a shallow equal comparison of all the properties of the props object and prevent re-rendering if none of them changed.
const { memo, useState, useEffect } = React;
const Child = props => {
// only gets called when `props` changes
useEffect(() => console.log(props.name, ": props changed"), [props]);
return <p>{props.name}: {props.text}</p>;
};
const MemoChild = memo(Child);
const Parent = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState("foo");
const handleTextChange = event => setText(event.target.value);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Click to force Re-Render
</button>
<input id="text" value={text} onChange={handleTextChange} />
<Child name="Child without memo()" text={text} />
<MemoChild name="Child with memo()" text={text} />
</div>
);
};
ReactDOM.render(<Parent />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Have a look at this example. You will see that when you press the button, the child not using memo will re-render, even if the props name or text have not changed. It will still receive a new props object.
But when you enter something into the input, both components re-render, because the text property of the props changed.
It should also be noted that this is only a performance optimization. You should not rely on memo preventing a re-render at all times.
This method only exists as a performance optimization. Do not rely on
it to “prevent” a render, as this can lead to bugs.
I am creating a react application using hooks but I'm getting a weird behavior on my code, I'm not going to put all my code here but I will give you an example what it's going on.
const Child = props => {
const {data} = props;
const [myData, setMyData] = useState(data);
const items => myData.map( r => <li> r </li> );
return ( <ul> { items } </ul> );
}
const Parent = () => {
return (<div>
<Child data={ [1, 2, 3] }
</div> );
}
I am making changes to the array that the Parent component sends to the Child component.
When I add a new element to the array, the Child component re-render so MyData is equals to the array ( this makes sense because the Child components is re-render by the props change ).
If I delete an element from the array the Child component is re-render but myData doesn't change and the element I deleted from the array is still there in myData.
Why the useState sets the array to myData when I add elements to the array but when I delete elements it seems like this doesn't work,even though the Child component is re-render.
I know it seems a little dumb, you can ask me, why you don't just use the props value on the Child component instead of a state value, well the idea is on the Child component there is a search control so I can make some kind of searching over MyData and not over the props value ( but maybe there is another way ).
I think the issue is how you're using props.
When Child renders, it gets props.data from Parent. It then copies data to its component state as the variable myData.
From here on, changes to props can trigger the Child to re-render, but myData won't be redefined again via useState. That happens only once.
If you're writing code that does hot-reloading (you save the file and the application reloads in the browser), it might seem like changing the props sent to Child updates myData, but that's not happening in a production environment.
My suggestion: if you're going to fork props into the state of Child, think of props as an initial value, and not something that Parent can update.
The React docs explain why this is an anti-pattern:
The problem is that it’s both unnecessary (you can use this.props.color directly instead), and creates bugs (updates to the color prop won’t be reflected in the state).
Only use this pattern if you intentionally want to ignore prop updates.
React constructor docs
useEffect(() => {
setMyData(data)
},[data])
your data will be load to state only once. but you can handle changes on data by parent component with useEffect, useEffect will updates when data will updated
there is full code
const Child = props => {
const {data} = props;
const [myData, setMyData] = useState(data);
useEffect(() => {
setMyData(data)
},[data])
const items => myData.map( r => <li> r </li> );
return ( <ul> { items } </ul> );
}
const Parent = () => {
return (<div>
<Child data={ [1, 2, 3] }
</div> );
}
you can read about React use effect