Prevent re-rendering in ReactJS of all childs on onChange event - javascript

I am setupping a simple dashboard to challeging my self with ReactJS, but I have some issues preventing useless re-rendering.
I have a root component called App where I fetch some data.
const App = () => {
const [data, setData] = useState(null);
const [list1, setList1] = useState(null);
const [list2, setList2] = useState(null);
const [list3, setList3] = useState(null);
const [list4, setList4] = useState(null);
useEffect(() => {
const fetchData = fetchDataInSomeWay();
const fetchedData = getData(fetchData);
const list1Data = getList1(fetchData);
setList1(list1Data);
setData(fetchedData);
});
...
{ data !== null
&& (
<Parent
data={data}
list1={list1}
list2={list2}
list3={list3}
list4={list4}
/>
};
Then I setup a Parent component where I created some Select component and other elements which depend on the values ​​selected by select.
I have a Select element for each list state created with useState();
const Google = ({
data,
list1,
list2,
list3,
list4,
}) => {
const [typeValue, setTypeValue] = useState('someValue');
const [list1Value, setList1Value] = useState(list1[0]);
const [list2Value, setList2Value] = useState(list2[0]);
const [list3Value, setList3Value] = useState(list3[0]);
const [list4Value, setList4Value] = useState(list4[0]);
const onChangeSelectTypeValue = (value) => {
setTypeValue(value);
};
...
const selectTypeValueElement = (
<SelectElement
select={selectType}
value={[typeValue]}
onChangeValue={onChangeSelectTypeValue}
values={list1Value}
/>
);
...
<div className="interactionHeaderChart">
{ selectTypeValueElement }
...
</div>
};
Then I have a Select element where I do not store a state, but where option selected is passed to Parent compoment.
const SelectElement = ({
select, value, values, onChangeValue,
}) => {
...
<Select
...
value={value[0]}
onChange={onChangeValue}
>
...
};
Now when I select some option from one Select, state of Parent change and all Childs re-render, all Selects components and also other components which depend on the values ​​selected by select.
Can I prevent all Select components from re-rendering? Can I avoid to re-render all other components which does not depend on the values of option selected?
The fact that the state has changed from the onChange function and not from useEffect() is confusing me and I can not understand how to solve it.
Thanks.

You should look into shouldComponentUpdate:
https://reactjs.org/docs/react-component.html#shouldcomponentupdate
Usually, in order to use this with your SelectElement component you will first have to convert it into a Class. You can then add the shouldComponentUpdate function to it and check the previous and next props are the same or not. If they are the same, don't update.
However, if your props are not complex objects, you can actually just recreate your SelectElement as a PureComponent. This will automatically check the props and will not re-render if they're the same.
e.g.
class SelectElement extends React.PureComponet {...

you can use memo to avoid re rendering.
Way 1:
const NestedComponent = () => {
return (
<div>
ContainerComponent
</div>
);
};
export default React.memo(NestedComponent);
Way 2:
function ParentComponent(a, b) {
const childComponent = React.useMemo(() => <ChildComponent posts={a} />, [a]);
return (
<>
{childComponent}
</>
)
}

Related

Wait for change of prop from parent component after changing it from a child in React

I have rewritten a Child class component in React to a functional component. Here is the simplified code example.
For sure, as so often, this is a simplified code and more things are done with the value in the parent component. That's why we have and need it there.
const Parent = (props) => {
const [value, setValue] = useState(null);
const handleChange = (newValue) => {
// do something with newValue and probably change it
// store the result in `newChangedValue`
setValue(newChangedValue);
}
return (
<Child value={value} onChange={handleChange}/>
);
}
const Child = (props) => {
const {value} = props;
// This solution does not work for me,
// because it's always triggered, when
// `value` changes. I only want to trigger
// `logValueFromProp` after clicking the
// Button.
useEffect(() => {
logValueFromProp();
}, [value]);
const handleClick = () => {
// some calculations to get `newValue`
// are happening here
props.onChange(newValue);
logValueFromProp();
}
const logValueFromProp = () {
console.log(prop.value);
}
return (
<Button onClick={handleClick} />
);
}
What I want to do is to log a properties value, but only if it got changed by clicking the button. So just using a useEffect does not work for me.
Before changing the child component to a functional component, the property had its new value before I was calling logValueFromProp(). Afterwards it doesn't. I guess that's cause of some timing, and I was just lucky that the property was updated before the function was called.
So the question is: How would you solve this situation? One solution I thought of was a state in the child component which I set when the button is clicked and in the useEffect I only call the function when the state is set and then reset the state. But that doesn't feel like the optimal solution to me...
Three possible solutions for you
Pass logValueFromProp the value directly — but in a comment you've explained that the value might be modified slightly by the parent component before being set on the child, which would make this not applicable.
Use a flag in a ref. But if the parent doesn't always change the prop, that would be unreliable.
Have the parent accept a callback in its handleChange.
#1
If possible, I'd pass the value directly to logValueFromProp when you want to log it. That's the simple, direct solution:
const Child = (props) => {
const {value} = props;
const handleClick = () => {
props.onChange(newValue);
logValueFromProp(newValue);
};
const logValueFromProp = (newValue = prop.value) {
console.log(newValue);
};
return (
<Button onClick={handleClick} />
);
};
But in a comment you've said the new value may not be exactly the same as what you called props.onChange with.
#2
You could use a ref to remember whether you want to log it when the component function is next called (which will presumably be after it changes):
const Child = (props) => {
const {value} = props;
const logValueRef = useRef(false);
if (logValueRef.current) {
logValueFromProp();
logValueRef.current = false;
}
const handleClick = () => {
props.onChange(newValue);
logValueRef.current = true;
};
const logValueFromProp = () {
console.log(prop.value);
};
return (
<Button onClick={handleClick} />
);
};
Using a ref instead of a state member means that when you clear the flag, it doesn't cause a re-render. (Your component function is only called after handleClick because the parent changes the value prop.)
Beware that if the parent component doesn't change the value when you call prop.onChange, the ref flag will remain set and then your component will mistakenly log the next changed value even if it isn't from the button. For that reason, it might make sense to try to move the logging to the parent, which knows how it responds to onChange.
#3
Given the issues with both of the above, the most robust solution would seem to be to modify Parent's handleChange so that it calls a callback with the possibly-modified value:
const Parent = (props) => {
const [value, setValue] = useState(null);
const handleChange = (newValue, callback) => {
// ^^^^^^^^^^−−−−−−−−−−−−−−−−− ***
// do something with newValue and probably change it
// store the result in `newChangedValue`
setValue(newChangedValue);
if (callback) { // ***
callback(newChangedValue); // ***
} // ***
};
return (
<Child value={value} onChange={handleChange}/>
);
};
const Child = (props) => {
const {value} = props;
const handleClick = () => {
props.onChange(newValue, logValueFromProp);
// ^^^^^^^^^^^^^^^^^^−−−−−−−−−−−−−− ***
}
const logValueFromProp = () {
console.log(prop.value);
};
return (
<Button onClick={handleClick} />
);
};
This answer is based upon the answer of T.J. Crowder (#2).
You can create a custom hook that accepts a callback and dependencies. And returns a function that will trigger a re-render (by using useState instead of useContext) calling the callback in the process.
I've enhanced his answer by allowing you to pass a dependency array which will be used to determine if the callback is called. If the dependency array is omitted, the callback is always called. When passed, the callback is only called if there was a change in the dependency array.
I went for the name useTrigger in the example below, but depending on preference you might like another name better. For example useChange.
const { useState, useCallback } = React;
const useTrigger = (function () {
function zip(a1, a2) {
return a1.map((_, i) => [a1[i], a2[i]]);
}
// compares 2 arrays assuming the length is the same
function equals(a1, a2) {
return zip(a1, a2).every(([e1, e2]) => Object.is(e1, e2));
}
return function (callback, deps) {
const [trigger, setTrigger] = useState(null);
if (trigger) {
if (!deps || !equals(deps, trigger.deps)) {
callback(...trigger.args);
}
setTrigger(null);
}
return useCallback((...args) => {
setTrigger({ args, deps });
}, deps);
}
})();
function Parent() {
const [value, setValue] = useState(null);
function handleChange(newValue) {
// Sometimes the value is changed, triggering `logValueFromProp()`.
// Sometimes it isn't.
if (Math.random() < 0.66) newValue++;
setValue(newValue);
}
return <Child value={value} onChange={handleChange} />;
}
function Child({ value, onChange }) {
const logValueFromProp = useTrigger(() => {
console.log(value);
}, [value]);
function handleClick() {
onChange(value || 0);
logValueFromProp();
}
return (
<button onClick={handleClick}>
Click Me!
</button>
);
}
ReactDOM.render(<Parent />, document.querySelector("#demo"));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="demo"></div>

Change child's state in react

function App(){
const [selectedLang, setSelectedLang] = useState(0);
const [langList, setLangList] = useState([]);
useEffect(()=>{
const list = [];
/* i18n.options.supportedLngs is ['en', 'ko'] */
i18n.options.supportedLngs.map((item, i)=>{
list.push(item);
/* Set selectedLang to i18n's default language.(Default is 'ko')*/
if(item === i18n.language){
setSelectedLang(i);
}
})
setLangList(list);
}, [])
useEffect(()=>{
console.debug("langList :", langList, ",", selectedLang); // <- It print correctly ['en', 'ko'], 1
}, [langList, selectedLang])
return(
<Child defaultIndex={selectedLang} childList={langList}/>
)
}
function Child(props){
const {defaultIndex, childList} = props;
const [selected, setSelected] = useState(0);
useState(()=>{
setSelected(defaultIndex);
},[])
return(
<div>
{childList[selected]} //<- But it print 'en'.
</div>
)
}
The code above is a simplified version of my code.
Currently i18n defaults to 'ko'. I want to display 'ko' in Child by setting selectedLang to 'ko's index in App, and giving the index of 'ko' and the entire language array as props to Child.
However, Child's selected and defaultIndex doesn't seem to change from a state initialized with useState(0).
Can someone help me?
setSelected need to be called after changing defaultIndex in Child component.
And you didn't use proper hook.
useEffect(()=>{
setSelected(defaultIndex);
},[defaultIndex])
You can use react context to achieve this:
First make a Lang provider in app.js
const LangContext = React.createContext('ko');
function App() {
const [lang, setLang] = useState("ko");
return (
<LangContext.Provider value={[lang, setLang]}>
<Toolbar />
</LangContext.Provider>
);
}
Then in any child component you can use :
function Button() {
const [lang, setLang]= useContext(LangContext);
return (
<button onClick={()=>{setLang('en')}}>
{lang}
</button>
);
}
Any change in context will propagate in all components using useContext.
Read more here : https://reactjs.org/docs/hooks-reference.html#usecontext

Re-render a sibling component using hooks

I am new to react and I'm trying to get the one component to re-render from another component.
Here's my code:
const Parent = () => {
return (
<div>
<Child1 />
<Child2 />
</div>
)
}
What I intend to do is update Child1 when there is some trigger from Child2.
One way I can think of is to get the parent component to re-render so both Child1 and Child2 will be updated. I tried to do this by lifting the state but it doesn't seem to re-render each of the child components. Here's the code
const Parent = () => {
const [value, setValue] = useState('')
const handlePost = (newValue) => {
setValue(newValue)
}
return (
<div>
<Child1 />
<Child2 onPost={handlePost} />
</div>
)
}
const Child2 = (props) => {
// This function is executed when there is a trigger.
// In this case, when a post request is made to the server
const onPost() => {
props.handlePost('new value')
}
}
Edit:
The reason why the component(s) needs to be re-rendered is because they are making changes to the API and these changes need to be reflected on the screen. It has nothing to do with any state variables.
Your question is an XY problem. In the example given it does not make sense that Child1 rerenders cause there is no need for it. From the comments your real problem is that you update one API, which is supposed to change the response of another API. If you however already know how the response will change, and that it will change, this can be reflected in one state that changes for both API calls:
function useEntries() {
const [entries, setEntries] = useState([]);
useEffect(() => {
setEntries(getEntries());
}, []);
function addEntry(entry) {
postEntry(entry);
setEntries(prev => [...prev, entry]);
}
return { entries, addEntry };
}
function Parent() {
const { entries, addEntry } = useEntries();
return <>
<Entries entries={entries} />
<AddEntry addEntry={addEntry} />
</>;
}
From the comments in the post, it sounds like you have Child1 presenting results of a GET request (being done in Child1). Child2 can add or modify that state on the server with some kind of request and you want to trigger a re-render in order to make Child1 refresh the state.
The general problem is, that children should only re-render if props or their used contexts change. I see two options how to approach this:
Lift the handling of the requests up into the parent. Put the results of the request as props into the child component you want to refresh.
Make the sibling aware of the request having to reload by setting it to "dirty" in some way. Either through context or routing state around through the parent.
Usually it's best to go with option 1 if the components are not too deeply nested. It could look like this:
const Parent = () => {
const [posts, setPosts] = useState([]);
const fetchNewestPosts = useCallback(async () => {
const fetched = await fetchPosts();
setPosts(fetched);
}, [fetchPosts, setPosts]);
const handleSubmit = useCallback(async (event) => {
const newPost = getValuesFromSubmitEvent(event);
await sendNewPost(newPost);
// you could even set the posts here to what you think the
// send request will result in (see Jonas Wilms answer), like
// setPosts(posts => [newPost, ...posts]);
await fetchNewestPosts();
}, [fetchNewestPosts, getValuesFromSubmitEvent, sendNewPost]);
useEffect(() => {
fetchNewestPosts();
}, [fetchNewestPosts]);
return (
<div>
<Child1 posts={posts} />
<Child2 submitNewPost={submitNewPost} />
</div>
);
);
const Child1 = ({posts}) => {
return (
<ul>{posts.map(post => <li key={post.id}>post.title</li>)}</ul>
);
);
const Child2 = ({submitNewPost}) => {
return (
<form onSubmit={submitNewPost}>...</form>
);
);
As a nice side-effect, Child1 and Child2 now need a lot less logic and can be styled independently of the fetchPosts and sendNewPost functions.
Ciao, lets say that Child1 must be re-rendered on handlePost. Your parent component will be:
const Parent= () => {
const [value, setValue] = useState('')
const [rerender, setrerender] = useState(false)
const handlePost = (newValue) => {
setValue(newValue);
let setrerender_temp = rerender;
setrerender(!setrerender_temp);
}
return (
<div>
<Child1 rerender={rerender} />
<Child2 onPost={handlePost} />
</div>
)
}
Then, in your Child1 component:
import React, { useReducer, useEffect } from 'react';
...
export default function Child1(props) {
const [,forceRender] = useReducer((s) => s+1, 0);
useEffect(() => forceRender(), [props.rerender]);
...
}

Replace parent state with state of child. Button in parent

EDIT: See the comment of O.o for the explanation of the answer and the variant in case you are using classes.
I've come across to something and I can't find the solution.
I have 4 components in my web app:
Parent
child_1
child_2
child_3
I have a button on the Parent, and different forms (with inputs, checkboxes and radiobuttons) at the children.
Each child has his own button that executes several functions, some calculations, and updates the corresponding states. (No states are passed through parent and child).
I need to replace the three buttons of the children with the parent button.
Is there a way that I can execute the functions at the three children from the parent button and retrieve the results? (the results are one state:value per child.)
function Child1(props) {
const [value, setValue] = useState("");
useEffect(() => {
calculate();
}, [props.flag]);
calculate() {
//blah blah
}
onChange(e) {
setValue(e.target.value);
props.onChange(e.target.value); // update the state in the parent component
}
return (
<input value={value} onChange={(e) => onChange(e)} />
);
}
function Parent(props) {
const [flag, setFlag] = useState(false);
const [child1Value, setChild1Value] = useState("");
return (
<div>
<Child1 flag={flag} onChange={(value) => setChild1Value(value)}/>
<button onClick={() => setFlag(!flag)} />
</div>
);
}
I didn't test this but hope this helps you. And lemme know if there is an issue.
Try the following:
create refs using useRef for child form components.
for functional components, in order for the parent to access the child's methods, you need to use forwardRef
using the ref, call child component functions on click of parent submit button (using ref.current.methodName)
See the example code. I have tested it on my local, it is working ok.
Parent
import React, { Fragment, useState, useRef } from "react";
import ChildForm1 from "./ChildForm1";
const Parent = props => {
const [form1Data, setFormData] = useState({});//use your own data structure..
const child1Ref = useRef();
// const child2Ref = useRef(); // for 2nd Child Form...
const submitHandler = e => {
e.preventDefault();
// execute childForm1's function
child1Ref.current.someCalculations();
// execute childForm2's function
// finally do whatever you want with formData
console.log("form submitted");
};
const notifyCalcResult = (calcResult) => {
// update state based on calcResult
console.log('calcResult', calcResult);
};
const handleChildFormChange = data => {
setFormData(prev => ({ ...prev, ...data }));
};
return (
<Fragment>
<h1 className="large text-primary">Parent Child demo</h1>
<div>
<ChildForm1
notifyCalcResult={notifyCalcResult}
ref={child1Ref}
handleChange={handleChildFormChange} />
{/*{do the same for ChildForm2 and so on...}*/}
<button onClick={submitHandler}>Final Submit</button>
</div>
</Fragment>
);
};
export default Parent;
ChildFormComponent
import React, { useState, useEffect, forwardRef, useImperativeHandle } from "react";
const ChildForm1 = ({ handleChange, notifyCalcResult }, ref) => {
const [name, setName] = useState("");
const [calcResult, setCalcResult] = useState([]);
const someCalculations = () => {
let result = ["lot_of_data"];
// major calculations goes here..
// result = doMajorCalc();
setCalcResult(result);
};
useImperativeHandle(ref, () => ({ someCalculations }));
useEffect(() => {
// notifiy parent
notifyCalcResult(calcResult);
}, [calcResult]);
return (
<form className="form">
<div className="form-group">
<input
value={name}// //TODO: handle this...
onChange={() => handleChange(name)}//TODO: notify the value back to parent
type="text"
placeholder="Enter Name"
/>
</div>
</form>
);
};
export default forwardRef(ChildForm1);
Also as a best practice, consider to maintain state and functions in the parent component as much as possible and pass the required values/methods to the child as props.

React w/hooks: prevent re-rendering component with a function as prop

Let's say I have:
const AddItemButton = React.memo(({ onClick }) => {
// Goal is to make sure this gets printed only once
console.error('Button Rendered!');
return <button onClick={onClick}>Add Item</button>;
});
const App = () => {
const [items, setItems] = useState([]);
const addItem = () => {
setItems(items.concat(Math.random()));
}
return (
<div>
<AddItemButton onClick={addItem} />
<ul>
{items.map(item => <li key={item}>{item}</li>)}
</ul>
</div>
);
};
Any time I add an item, the <AddItemButton /> gets re-rendered because addItem is a new instance. I tried memoizing addItem:
const addItemMemoized = React.memo(() => addItem, [setItems])
But this is reusing the setItems from the first render, while
const addItemMemoized = React.memo(() => addItem, [items])
Doesn't memoize since items reference changes.
I can'd do
const addItem = () => {
items.push(Math.random());
setItems(items);
}
Since that doesn't change the reference of items and nothing gets updated.
One weird way to do it is:
const [, frobState] = useState();
const addItemMemoized = useMemo(() => () => {
items.push(Math.random());
frobState(Symbol())
}, [items]);
But I'm wondering if there's a better way that doesn't require extra state references.
The current preferred route is useCallback, which is the same as your useMemo solution, but with additional possible optimizations in the future. Pass an empty array [] to make sure the function will always have the same reference for the lifetime of the component.
Here, you also want to use the functional state update form, to make sure the item is always being added based on the current state.
const addItem = useCallback(() => {
setItems(items => [...items, Math.random()]);
}, []);

Categories