Does React make any guarantees that the `props` object reference stays stable? - javascript

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.

Related

setting state with useState affects all useState hooks

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!

Hook state one behind prop state

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.

React.useMemo does not work to prevent re-renders

I have the following simple component:
const Dashboard = () => {
const [{ data, loading, hasError, errors }] = useApiCall(true)
if (hasError) {
return null
}
return (
<Fragment>
<ActivityFeedTitle>
<ActivityFeed data={data} isLoading={loading} />
</Fragment>
)
}
export default Dashboard
I would like to prevent ALL re-renders of the ActivityFeedTitle component, so that it only renders once, on load. My understanding is that I should be able to use the React.useMemo hook with an empty dependencies array to achieve this. I changed by return to be:
return (
<Fragment>
{React.useMemo(() => <ActivityFeedTitle>, [])}
<ActivityFeed data={data} isLoading={loading} />
</Fragment>
)
As far as I'm concerned, this should prevent all re-renders of that component? However, the ActivityFeedTitle component still re-renders on every render of the Dashboard component.
What am I missing?
EDIT:
Using React.memo still causes the same issue. I tried memoizing my ActivityFeedTitle component as follows:
const Memo = React.memo(() => (
<ActivityFeedTitle />
))
And then used it like this in my return:
return (
<Fragment>
{<Memo />}
<ActivityFeed data={data} isLoading={loading} />
</Fragment>
)
Same problem occurs. I also tried passing in () => false the following as the second argument of React.memo, but that also didn't work.
Use React.memo() instead to memoized components based on props.
React.memo(function ActivityFeedTitle(props) {
return <span>{props.title}</span>
})
Take note:
This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs.
The second argument passed to React.memo would need to return true in order to prevent a re-render. Rather than computing whether the component should update, it's determining whether the props being passed are equal.
Your usage of useMemo is incorrect.
From react hooks doc:
Pass a “create” function and an array of dependencies. useMemo will
only recompute the memoized value when one of the dependencies has
changed. This optimization helps to avoid expensive calculations on
every render.
If no array is provided, a new value will be computed on every render.
You need to use useMemo like useEffect here for computation of value rather than rendering the component.
React.memo
React.memo() is the one you are looking for. It prevents re-rendering unless the props change.
You can use React.memo and use it where your define your component not where you are making an instance of the component, You can do this in your ActivityFeedTitle component as
const ActivityFeedTitle = React.memo(() => {
return (
//your return
)
})
Hope it helps
It's because rendering a parent causes it's children to re-render for the most part. The better optimization here would be to place your data fetching logic either in the ActivityFeed component or into a HOC that you wrap ActivityFeed in.

is it bad to use props value on react hook?

I'm new in react hooks and I just don't see this on docs:
const MyComponent = ({myProp}) => {
const [myPropHook, setPropHook] = useState(myProp)
...
}
I'm wondering if this is a good practice?
The value you pass to useState is used as a starting value for the state variable. So, when your component props change, they will not affect the state variable you are using. The initial value would be the first props sent to the component and after that can be modified only using the setPropHook function.
So, in short, it is definitely a code smell to use props as initializers for useState because reading the code does not correctly convey what will actually happen.
You don't see it much because it doesn't make a lot of sense in terms of how a React app should distribute its state.
If a prop value is set higher up the tree, it shouldn't be used as part of the separate state within a component. It makes sense to use prop values to determine the state of a component indirectly as in 'if the prop is this, then set the state to that', but not to directly copy the prop in to the initial value.
In other words, the internal state of a component (accessed via the useState and useReducer Hooks) should be determined by the component, not directly by the parent(s).
Yes, this is bad. What you're doing is passing a prop to the state, and it is discouraged by many.
The React docs says that "using props to generate state often leads to duplication of “source of truth”, i.e. where the real data is.". The danger is that if the props is changed without the component being refreshed, the new prop value will never be displayed, because the initialization of state from props only runs when the component is first created.
The only exception would be to use the prop as a seed for an internally-controlled state. After several years of react development, I've never encountered such a case.
Further reading:
React component initialize state from props (SO question)
React Anti-Patterns: Props in Initial State (medium.com article)
Why Setting Props as State in React.js is Blasphemy (blog post)
If you are trying to receive a prop to that functional component, then yes, but not exactly like you have it written. So in the parent component you will have something like this:
const App = () => {
const [resource, setResource] = useState("posts");
and then there is a component inside the JSX like so:
const App = () => {
const [resource, setResource] = useState("posts");
return (
<div>
<div>
<button onClick={() => setResource("posts")}>Posts</button>
<button onClick={() => setResource("todos")}>Todos</button>
</div>
<ResourceList resource={resource} />
</div>
);
};
That ResourceList component has to be able to receive the props that the App component is passing to it. Inside a class-based component you would do {this.props.resource}, but in our case, where its a functional component using React hooks you want to write it like so:
const ResourceList = (props) => {
const [resources, setResources] = useState([]);
or via ES6 destructuring like so:
const ResourceList = ({ resource }) => {
const [resources, setResources] = useState([]);

Set a child array state object from props value

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

Categories