Update state from children component - reactjs

Take a look at the app. There is a list of buttons. It's really hard for me to explain the issue to you guys so take a look at the app yourself and consider this as a challenge. It's really a small app to demonstrate an issue that I am facing while working with react. This is not a basic stuff...
https://codesandbox.io/s/cranky-faraday-bxufk?file=/src/App.js
When clicking on the button, it is expected that the boolean next to it turn to false. It seems basic to you at first but when you take a look at the code, this is not easy to achieve. My real application is more complicated so I make a simple version of it just to demonstrate the issue
Expect:
Click on button 1 => boolean next to it turns to false
Click on button 2 => boolean next to it turns to false
Click on button 3 => boolean next to it turns to false
Reality:
Click on button 1 => screen goes blank

So, This is another way to solve this problem.
But first of all I will pin-down the problem in existing code, and try to explain why it is not working.
I had placed two console.log in App.js one is in useEffect() and another just before the return statement:
export default function App() {
const [state, setState] = React.useState([]);
// Set pannels state
React.useEffect(() => {
console.log("useEffect executing");
setState(
data.map((item) => ({
id: item.id,
isSaved: true,
content: <Button id={item.id} handleButton={handleButton} />
}))
);
}, []);
const handleButton = (id) => {
....
....
}
console.log("App Rendering");
return (
<div className="App">
<button onClick={handleClick}>Try this</button>
{state ? <Tab pannels={state} /> : null}
</div>
);
}
And below is the console after execution:
Now it is clearly can be seen that when you are executing setState inside useEffect the state value is an empty array []. Because first render cycle of App is already completed and call to useState initialized state to empty array.
In useEffect you are assigning property content as:
...
content: <Button id={item.id} handleButton={handleButton} />
...
Now if you closely observe {handleButton} is an function bound to the first render of 'App' (which is of course a function in itself) and {handleButton} has access to const state in it's parent scope, which is an empty array [] in first render cycle of 'App';
Thus you are in the middle of pushing the {handleButton} in state which has still a value [] (empty array), while useEffect is executing.
As soon as setState completes inside useEffect another render cycle for App is executed (due to local state update react re-renders the component). A new instances of App and all of it's nested members are created, except the state and setState which are managed by the react.
After this second render of App your state now contains the reference of old instance of {handleButton} (where you assign property content to Button) of previous render of App thus these {handleButton} still have state value as [] (empty array closures).
Now when you click any of the Button it's onClick is bound to the old reference of handleButton which has state value as empty array, and thus when {handleButton} finishes execution it calls setState (function provided by react framework, not affected by re-render) it once again sets the state to an empty array. And you eventually get you buttons disappeared.
Now to solve this, you need to rebind new instance of {handleButton} each time a re-render happens for App. You can not memoize {handleButton} because it depends on the state. Thus you can delegate rendering of Button component to Tab which re-renders itself after every state update.
So you can do something like this:
App.js
export default function App() {
const [state, setState] = React.useState([]);
// Set pannels state
React.useEffect(() => {
setState(
data.map((item) => ({
id: item.id,
isSaved: true
}))
);
}, []);
const handleButton = (id) => {
console.log("On button click: ", id);
console.log(state);
//Update isSaved of a corresponding button to false
const updatedState = [...state];
const updatedIndex = updatedState.findIndex((item) => item.id === id);
updatedState[updatedIndex] = {
...updatedState[updatedIndex],
isSaved: false
};
setState(updatedState);
};
return (
<div className="App">
<button onClick={() => handleButton(2)}>Try this</button>
{state ? <Tab pannels={state} handleButton={handleButton} /> : null}
</div>
);
}
Note: I have even used the same event handler handleButton for try this button too, to show that the same function instance works well if used on the component itself or passed to children.
Tab.js
const Tab = ({ pannels, handleButton }) => {
return (
<div>
List of pannels
{pannels.map((item) => (
<div key={item.id}>
<Button id={item.id} handleButton={handleButton} />
<span>{item.isSaved ? "true" : "false"}</span>
</div>
))}
</div>
);
};
codesandbox reference: https://codesandbox.io/s/zen-pascal-o6ify
Thanks

The problem in your code is related to state update. The handle button need to handle state as:
const handleButton = (id) => {
console.log("On button click: ", id);
setState((state) => {
// Update isSaved of a corresponding button to false
const updatedState = [...state];
const updatedIndex = updatedState.findIndex((item) => item.id === id);
updatedState[updatedIndex] = {
...updatedState[updatedIndex],
isSaved: false
};
return updatedState;
});
};
The solution is actually in the setState variant which uses a callback like setState((latestState) => {// do something }) and guarantees to give latest state for every execution.
Thanks for giving a beautiful problem to solve.

Related

onCallback React Hook - best practice query

This is more of a best practice question than anything. But I'm wondering if any inline functions within a React component should typically be wrapped with an onCallback for performance? Under what circumstances would I not wrap a function like this with onCallback?
For example:
const ToolSearch = (props: ToolsSearchProps) => {
const handleOnClick = () => {
alert('do something');
}
return (
<NoCardOverflow>
<ToolSearchCollapse openState={filtersOpen} onClick={handleOnClick} />
</NoCardOverflow>
);
};
In this example should I be doing this:
const ToolSearch = (props: ToolsSearchProps) => {
const handleOnClick = useCallback(() => {
alert('do something');
},[]);
return (
<NoCardOverflow>
<ToolSearchCollapse openState={filtersOpen} onClick={handleOnClick} />
</NoCardOverflow>
);
};
I will try to explain the useCallBack with an example. We all know the definition of useCallBack, but when to use is the trick part here. So, let me take an example.
const RenderText = React.memo(({ text }) => {
console.log(`Render ${text}`);
return (<div>{text}</div>);
});
const App = () => {
const [count, updateCount] = React.useState(0);
const [lists, updateLists] = React.useState(["list 1"]);
const addListItem = () => {
updateLists([...lists, "random"]);
};
return (
<div>
{`Current Count is ${count}`}
<button onClick={() => updateCount((prev) => prev + 1)}>Update Count</button>
{lists.map((item: string, index: number) => (
<RenderText key={index} text={item} />
))}
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#app"));
jsFiddle: https://jsfiddle.net/enyctuLp/1/
In the above, there are two states.
count - Number
lists - Array
The RenderText just renders the text passed to it. (which is the item in the list). If you click on the Update Count button, the RenderText will not re-render because it is independent from the main component (App) and during the updateCount, only the App component will re-render, since it needs to update the count value.
Now, pass the addListItem into RenderText component and click on the Update Count button and see what happens.
jsFiddle: https://jsfiddle.net/enyctuLp/2/
You can see that the RenderText will re-render even though there is no change in the list array and this is BECAUSE :
when the count is updated, the App will re-render
which, will re-render the addListItem
which causes the RenderText to re-render.
To avoid this, we should use useCallBack hook.
jsFiddle: https://jsfiddle.net/enyctuLp/4/
Now, the addListItem function has been memoized and it will only change when the dependency are changed.
ToolSearchCollapse is your child component, and so wrapping your handleClick function makes sense. Coz every time the parent component re-renders a new reference of handleclick will be passed to ToolSearchCollapse, and your child will re-render every time a parent state changes, (from states that aren't passed to the child also). Using useCallback will not allow creating new references, and thus you can control when your child should render.
If you don't pass this fn to the child, I don't see any reason to use useCallback.

How to pass a method into a child element if the method relies on state variables in React

I've been learning react over the last few days and for the most part it makes sense, however there is one thing that is stumping me.
If I have
A Parent element with some state variables and a callback method
A child element that takes a callback method as a prop
The callback method relies on some piece of state that is in the parent element
I don't want to re-create the view object every time any state changes
Every time I try to do this, it seems like the child element is calling some older version of the parent element (presumably the instance of the parent that actually created the child) instead of the current version.
I'm getting the feeling that what I want is just wrong on a fundamental level and isnt The React Way
The reason that I am trying to do this is that my main parent contains 17 divs, each of which represent a key on a musical instrument, and each of which contains at least 20-30 divs. The lowest div (of which there are at least a few hundred) has an onClick event that I want to modify the functionality of based on whether modifier keys are held down (shift, control etc).
Currently I have Raised the state of the shiftPressed to be on the single parent element then passed down the value of that into each child through props, however re-rendering hundreds of divs whenever a user pushes shift takes quite a while.
I've made a code sandbox to show the current problem sandbox
Sandbox code:
import "./styles.css";
import { useState, useEffect, useRef } from "react";
export default function App() {
//Our state holding data
const [state, setState] = useState(false);
//Our state holding the view
const [view, setView] = useState(<div></div>);
const printState = useRef(null);
//Component did mount hook
useEffect(() => {
reGenerate();
}, []);
//state update hook
useEffect(() => {
printState.current();
}, [state]);
//function to flip the state
const flipState = () => {
setState(!state);
};
//The method that updates the view
//(The idea being that I don't want to update the view on every state change)
const reGenerate = () => {
setView(
<>
<p>
State: {state && "true"} {state || "false"}
</p>
<Child callback={printState} />
</>
);
};
//Method for validation
printState.current = () => {
console.log("Printed state: " + state);
};
return (
<div className="App">
<h1>Parent-child-prop-problem (prop-lem)</h1>
<ol>
<li>click "force regeneration"</li>
<li>
click "flip state" and the value of state after the flip will be
printed in console, but it won't show up on the HTML element
</li>
<li>
Click "print state (from child)" and observe that the console is
printing the old version of the state
</li>
</ol>
<button onClick={flipState}>Flip State</button>
<button onClick={reGenerate}>Force Regeneration</button>
{view}
</div>
);
}
function Child(props) {
return (
<div>
<button onClick={props.callback.current}>Print State (from child)</button>
</div>
);
}
Taking a quick peek at your sandbox code and I see that you are storing JSX in state, which is anti-pattern and often leads to stale enclosures like you describe.
I don't want to re-create the view object every time any state changes
"Recreating" the view is a necessary step in rendering UI in React as a result of state or props updating. State should only ever store data and the UI should be rendered from the state. In other words, treat your UI like a function of state and props. Toggle the state state value and render the UI from state.
Example:
export default function App() {
//Our state holding data
const [state, setState] = useState(false);
const printState = useRef(null);
//state update hook
useEffect(() => {
printState.current();
}, [state]);
//function to flip the state
const flipState = () => {
setState(!state);
};
//Method for validation
printState.current = () => {
console.log("Printed state: " + state);
};
return (
<div className="App">
<h1>Parent-child-prop-problem (prop-lem)</h1>
<ol>
<li>
click "flip state" and the value of state after the flip will be
printed in console, but it won't show up on the HTML element
</li>
<li>
Click "print state (from child)" and observe that the console is
printing the old version of the state
</li>
</ol>
<button onClick={flipState}>Flip State</button>
<p>State: {state ? "true" : "false"}</p>
<Child callback={printState} />
</div>
);
}
function Child(props) {
return (
<div>
<button onClick={props.callback.current}>Print State (from child)</button>
</div>
);
}
It's also generally considered anti-pattern to use any sort of "forceUpdate" function and is a code smell. In almost all circumstances if you hit a point where you need to force React to rerender you are doing something incorrect. This is the time you step back and trace through your code to find where a certain piece of state or some prop isn't updated correctly to trigger a rerender naturally.

Prevent useState value from being reset when props change

I have a component that looks something like this:
//#flow
import React, { useState } from "react";
type Props = {
likes: int,
toggleLike: () => void,
};
const Foo = (props: Props) => {
const [open, setOpen] = useState(false);
const style = `item${open ? " open": ""}`;
return (
<div className={style} onMouseOver={() => setOpen(true)} onFocus={() => setOpen(true)} onMouseOut={() => setOpen(false)} onBlur={() => setOpen(false)}>
<button onClick={props.toggleLike}>Toggle like</button>
</div>
);
};
export default Foo;
The open state is used to apply the "open" class when moused over. The problem comes if I call the toggleLike() prop function, since this updates the props and the component is rerendered with open reset to false. As the style uses a transition, this results in the animation rerunning as it changes back to false, then to true due to the mouse being over it.
So, how can I prevent open being reset back to false on each subsequent render? It seems like it should be straightforward, but after going through https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state I can't seem to apply it in my case.
State does not reset when props change. State is on a per component basis and is preserved throughout re-renders, hence being called "state".
As Dennis Vash already mentioned, the problem is most likely caused by the component being unmounted or replaced by an identical component. You can verify this easily by adding this to your component:
useEffect(() => {
console.log("Mounted")
}, [])
You should see multiple "Mounted" in the console.
If there's no way to prevent the component from being replaced or unmounted, consider putting the state into a context and consume that context inside your component, as you can also wrap each of your components into its own context to give it a unique, non-global, state.

Why isn't my state hook updating correctly?

I've created a minimal cutting of my code to show the issue, seen below.
const PlayArea = (props) => {
const [itemsInPlay, setItemsInPlay] = useState([
{id: 'a'},
{id: 'b'}
]);
const onItemDrop = (droppedItem) => {
setItemsInPlay([...itemsInPlay, droppedItem]);
};
return (
<>
<Dropzone onDrop={onItemDrop} />
<div>
{itemsInPlay.map(item => (
<span
key={item.id}
/>
))}
</div>
</>
);
};
The dropzone detects a drop event and calls onItemDrop. However, for reasons I don't understand, I can only drop in one item. The first item I drop is correctly appended to itemsInPlay and it re-renders correctly with a third span in addition to the starting two.
However, any subsequent item I drop replaces the third item rather than being appended. It's as though onItemDrop had a stored reference to itemsInPlay which was frozen with the initial value. Why would that be? It should be getting updated on re-render with the new value, no?
The Dropzone sets its subscription token only once, when the component is initially rendered. When that occurs, the callback passed to setSubscriptionToken contains a stale value of the onCardDrop prop - it will not automatically update when the component re-renders, since the subscription was added only once.
You could either unsubscribe and resubscribe every time onCardDrop changes, using useEffect, or use the callback form of setItemsInPlay instead:
const onItemDrop = (droppedItem) => {
setItemsInPlay(items => [...items, droppedItem]);
};
This way, even if an old version of onItemDrop gets passed around, the function won't depend on the current binding of itemsInPlay being in the closure.
Another way to solve it would be to change Dropzone so that it subscribes not just once, but every time the onCardDrop changes (and unsubscribing at the end of a render), with useEffect and a dependency array.
Regardless of what you do, it would also be a good idea to unsubscribe from subscriptions when the PlayArea component dismounts, something like:
const [subscriptionToken, setSubscriptionToken] = useState<string | null>(null);
useEffect(
() => {
const callback = (topic: string, dropData: DropEventData) => {
if (wasEventInsideRect(dropData.mouseUpEvent, dropZoneRef.current)) {
onCardDrop(dropData.card);
setDroppedCard(dropData.card);
}
};
setSubscriptionToken(PubSub.subscribe('CARD_DROP', callback));
return () => {
// Here, unsubscribe from the CARD_DROP somehow,
// perhaps using `callback` or the subscription token
};
},
[] // run main function once, on mount. run returned function on unmount.
);

React useState vs raw variable

For example i have a some data which need to be rendered. Items will be always the same but they are coming from props.
const items = props.data.values.map(value => ({
id: value.id
name: value.name,
rating: Number(value.rating)
}));
return (
{items.map(item => (
<div key={item.id}....
)}
);
May i use useState for items variable like that:
const [items] = useState(data.values.map(value => ({
id: value.id
name: value.name,
rating: Number(value.rating)
})));
Does it's help me to get rid of redundant "mapping" during next rerender or not?
No, it doesn't help.
I think it's better to completely get rid of the first map and do whatever you want in the second one. but if you think that is necessary for your app, you could use useMemo hook.
This hook provides you a memoized value that re-calculates only if some parameters change.
For example:
const items = useMemo(() => {
return data.values.map(...)
}, [data])
This example calculates items only if the value of data changes. otherwise, it returns the memoized version and doesn't re-calculate anything.
But what about useState? it used whenever we have some variable that whenever it changes, we want to re-render our component and show some new contents. for example, we have a Counter that whenever its value changes, we want to re-render component and show the new Value. so we use something like this:
const Counter = (props) => {
const [value, setValue] = useState(0)
return (
<div>
<p>{value}</p>
<button onClick={() => setValue(value + 1)}>Increment</button>
<button onClick={() => setValue(value - 1)}>Decrement</button>
</div>
)
}
So whenever we call setValue, the component re-renders and the new value will be shown to the user.
I think what you're looking for is Pure Components. They use shouldComponentUpdate to determine if the component needs to re-render. In your case if the props are the same, the component won't re-render if you use a Pure Component.

Resources