React child component not updating when props change - javascript

I'm trying to build out a local search in React consisting of a parent component with a search input, and a child component containing a list of search results. I've created a React state object that contains the search query and a list of search results. When the input field is changed, the search is ran to generate a new result set, and both properties (query and results) are updated. The search input is updating as expected, but the child component doesn't re-render despite an update to its prop. I've removed some of the code for brevity, but if you need more information please let me know.
export const Search = () => {
let [searchState, setSearchState] = React.useState({});
let handleChange = (event) => {
searchState['results'] = searchProducts(event.target.value);
searchState['query'] = event.target.value;
setSearchState(searchState);
};
return (
<div>
<FormControl>
<Input
value={searchState.query}
onChange={handleChange}>
Search...
</Input>
</FormControl>
<SearchResults results={searchState.results}></SearchResults>
</div>
);
};
export const SearchResults = (props) => {
return (
<List>
{props.results?.map((product, index) => (
<ListItem key={index}>
<ListItemText primary={product.name}></ListItemText>
</ListItem>
))}
</List>
);
};
My question is: Why doesn't the SearchResults component get re-rendered when searchState.results changes?

You mutate your state object in your handleChange function. The component doesn't rerender because searchState is still the same object reference from the previous render cycle.
let handleChange = (event) => {
searchState['results'] = searchProducts(event.target.value); // mutation!
searchState['query'] = event.target.value; // mutation!
setSearchState(searchState); // safe reference back into state
};
You shouldn't mutate state object directly. Use a functional state update and shallow copy existing state into a new state object reference. Then update the properties you want to update.
let handleChange = (event) => {
const { value } = event.target;
setSearchState(searchState => ({
...searchState,
results: searchProducts(value),
query: value,
}));
};

Related

Re-rendering on key-value pair object components

I want to avoid re-render of my child component <ChildComponent/> whenever I update my state using a onClick in <ChildComponent/>.
I have my callback function in <ParentComponent/> which updates one of the values for the key-value pair object.
In the parent component
const _keyValueObject = useMemo(() => utilityFunction(array, object), [array, object])
const [keyValueObject, setKeyValueObject] = useState<SomeTransport>(_keyValueObject)
const handleStateChange = useCallback((id: number) => {
setKeyValueObject(keyValueObject => {
const temp = { ... keyValueObject }
keyValueObject[id].isChecked = ! keyValueObject[id].isChecked
return temp
})
}, [])
return(
<Container>
{!! keyValueObject &&
Object.values(keyValueObject).map(value => (
<ValueItem
key={value.id}
category={value}
handleStateChange ={handleStateChange}
/>
))}
</Container>
)
In child component ValueItem
const clickHandler = useCallback(
event => {
event.preventDefault()
event.stopPropagation()
handleStateChange(value.id)
},
[handleStateChange, value.id],
)
return (
<Container>
<CheckBox checked={value.isChecked} onClick={clickHandler}>
{value.isChecked && <Icon as={CheckboxCheckedIcon as AnyStyledComponent} />}
</CheckBox>
<CategoryItem key={value.id}>{value.title}</CategoryItem>
</Container>
)
export default ValueItem
In child component if I use export default memo(ValueItem), then the checkbox does not get updated on the click.
What I need now is to not re-render every child component, but keeping in mind that the checkbox works. Any suggestions?
Spreading (const temp = { ... keyValueObject }) doesn't deep clone the object as you might think. So while keyValueObject will have a new reference, it's object values will not be cloned, so will have the same reference, so memo will think nothing changes when comparing the category prop.
Solution: make sure you create a new value for the keyValueObject's id which you want to update. Example: setKeyValueObject(keyValueObject => ({...keyValueObject, [id]: {...keyValueObject[id], isChecked: !keyValueObject[id].isChecked})). Now keyValueObject[id] is a new object/reference, so memo will see that and render your component. It will not render the other children since their references stay the same.
Working Codesandbox
Explanation
What you need to do is wrap the child with React.memo. This way you ensure that Child is memoized and doesn't re-render unnecessarily. However, that is not enough.
In parent, handleStateChange is getting a new reference on every render, therefore it makes the parent render. If the parent renders, all the children will re-render. Wrapping the handleStateChange with useCallback makes sure react component remembers the reference to the function. And memo remembers the result for Child.
Useful resource

State is not up to date in functions of array of components React

import React, { useState } from "react";
const Person = ({ id, name, age, deleteThisPerson }) => {
return (
<div className="person">
<p>{name}</p>
<p>{age}</p>
<button onClick={() => deleteThisPerson(id)}>Delete This Person</button>
</div>
);
};
const ArrayOfComponents = () => {
const deleteThisPersonHandler = (id) => {
console.log("[ArrayOfComponents] id : ", id);
console.log("[ArrayOfComponents] personArrOfComp : ", personArrOfComp);
};
const [personArrOfComp, setPersonArrOfComp] = useState([
<Person
id="pc1"
key="pc1"
name="pc1"
age={12}
deleteThisPerson={deleteThisPersonHandler}
/>,
]);
const addNewPersonHandler = () => {
let r = new Date().getTime();
setPersonArrOfComp((prevState) => {
return prevState.concat(
<Person
id={r}
key={r}
name={"PCC" + r}
age={Math.floor(Math.random() * 100)}
deleteThisPerson={deleteThisPersonHandler}
/>
);
});
};
return (
<div>
<button onClick={addNewPersonHandler}>Add New Person</button>
{personArrOfComp}
</div>
);
};
export default ArrayOfComponents;
Add some persons by clicking Add New Person button. After that when clicking on Delete This Person button, deleteThisPersonHandler function shows previous state, not the most current state.
Can anyone explain this behavior of ReactJs. Why the state is not updating in deleteThisPersonHandler function after adding new person.
It's because you're putting components on state which reference a lambda that closes over an old version of deleteThisPersonHandler.
Components are not appropriate react state values. State should be the simplest possible data that your component needs to store to be able to render the UI. In this case that's probably a list of ids.
const [persons, setPersons] = useState(["pc1"]);
const addPerson = setPersons([...persons, Date.now()]);
return {persons.map(p => <Person
id={p}
key={p}
name={p}
age={12}
deleteThisPerson={deleteThisPersonHandler}
/>};
There, no components on state! You can swap out the scalar persons with any complex object type you need.
It's because of the deleteThisPersonHandler that you are passing as a prop. In setPersonArrOfComp you are setting a new state, but it hasn't been updated yet. Your deleteThisPersonHandler function that you are passing references the current state of personArrOfComp, and not the state that you are attempting to currently update to. It will always be the previous state.
setPersonArrOfComp((prevState) => {
return prevState.concat(
<Person
id={r}
key={r}
name={"PCC" + r}
age={Math.floor(Math.random() * 100)}
deleteThisPerson={deleteThisPersonHandler}
/>
);
});
that's because you used a console.log() which shows the previous state, not the current state, your job is already done but it's the nature of the console.log().
so you can show your result as:
const deleteThisPersonHandler = (id) => {
alert("[ArrayOfComponents] id : "+ id);
alert("[ArrayOfComponents] personArrOfComp : "+ personArrOfComp);
};

React state is updating but the component is not

There is a component that maps through an array stored in the state. A button, when it is clicked it updates the state, this action is working.
The problem is that the component is not updating too.
Here is the code:
const MyComponent = () => {
...
const [fields, setFields] = useState([{value: 'test', editable: false},
{value: 'test2', editable: false}]);
...
const toggleClass = (id) => {
const aux = fields;
aux[id].editable = true;
setFields(aux);
}
...
return (
<div>
...
{fields.map((field, id) => {
return (
<div>
<input className={field.editable ? 'class1' : 'class2'} />
<button onClick={() => toggleClass(id)}>click</button>
</div>
);
})}
</div>
);
I put logs and the state (fields) is updated after click to editable = true. But the css class is not changing.
Is there any solution to this issue?
You need to make a copy of your existing state array, otherwise you're mutating state which is a bad practice.
const toggleClass = id => {
const aux = [...fields]; //here we spread in order to take a copy
aux[id].editable = true; //mutate the copy
setFields(aux); //set the copy as the new state
};
That's happening because you are mutating the value of fields, which makes it unsure for React to decide whether to update the component or not. Ideally if you should be providing a new object to the setFields.
So, your toggleClass function should look like something below:
const toggleClass = (id) => {
const aux = [...fields]; //This gives a new array as a copy of fields state
aux[id].editable = !aux[id].editable;
setFields(aux);
}
BTW, I also noticed that you're not assigning a key prop to each div of the the map output. Its a good practice to provide key prop, and ideally keep away from using the index as the key.

Nested object stored as state variable deleting newer values when older values are modified

I have a react component which passes user input value to a props function. The parent function just appends those inputs to an object. However, when an older value is modified, all the newer values are removed. Please refer to the screenshots.
This is the parent. ExamArea.js
import McqQuestion from './McqQuestion'
import React, { useState, useEffect } from 'react';
import './ExamArea.css'
function ExamArea(props) {
const[currentQuesID, setCurrentQuesID] = useState(2);
const[mcqQuestionList, setmcqQuestionList] = useState([<McqQuestion returnfunc={returnFromMcqQuestion} id={1}/>]);
const[ques, setQues] = useState({});
function returnFromMcqQuestion(quesID, thisQuestion) {
var temp = {...ques};
temp["ques"+quesID] = thisQuestion;
console.log(temp);
setQues(temp);
}
function generateMCQ(questionid) {
return (<McqQuestion returnfunc={returnFromMcqQuestion} id={questionid}/>)
}
function addAnotherQuestion() {
setmcqQuestionList(mcqQuestionList.concat(generateMCQ(currentQuesID)));
setCurrentQuesID(currentQuesID+1);
}
return (
<div className="ExamArea">
{mcqQuestionList}
<button onClick={()=>addAnotherQuestion()} class="add_another_question_button">+ Add Another Question</button>
</div>
);
}
export default ExamArea;
This is the child.
import './McqQuestion.css'
import React, { useState, useEffect } from 'react';
import { Paper, TextField } from '#material-ui/core';
import InputBase from '#material-ui/core/InputBase';
/*
This is the component that lets the maker create the question, and then stores the question to packedQuestion.
packedQuestipn is in a format which can be directly sent to the API to be uploaded to the database.
A basic question has question_text, question.title
Props passed:
props.id = The Question ID.
props.returnfunc = The function that gets called with packedQuestion and props.id when everything is done.
props.returnfunc(props.id, packedQuestion) is the thing that is called.
*/
function McqQuestion(props) {
const [packedQuestion, setPackedQuestion] = useState({});
useEffect(()=> props.returnfunc(props.id, packedQuestion));
/*These two variables store a local copy of packedQuestion. These variables are first updated with the information from
onChange (or a variation of it), and then packedQuestion is set to an instance of this. */
let local_question_mcq = {};
let local_answerChoices_mcq = {};
function fillUpQuestionWithDefault(){
function addOption(character, value) {
local_answerChoices_mcq[character] = value;
local_question_mcq["answer_choices"] = local_answerChoices_mcq;
}
function addQuestion(title, value){
if(title){
local_question_mcq['title'] = value;
}
else {
local_question_mcq['question_text'] = value;
}
}
addQuestion(true, "Question "+props.id);
addQuestion(false, "");
addOption("a", "");
addOption("b", "");
addOption("c", "");
addOption("d", "");
local_question_mcq['title'] = "Question " + props.id;
local_question_mcq['id'] = props.id;
setPackedQuestion(local_question_mcq);
}
useEffect(() =>fillUpQuestionWithDefault(), []);
function optionOnInputFunc(character, value) {
local_question_mcq = {...packedQuestion};
local_answerChoices_mcq = {...local_question_mcq["answer_choices"]};
local_answerChoices_mcq[character] = value;
local_question_mcq["answer_choices"] = local_answerChoices_mcq;
setPackedQuestion(local_question_mcq);
}
function questionOnInputFunc(title, value) {
if(title){
local_question_mcq = {...packedQuestion};
local_question_mcq['title'] = value;
setPackedQuestion(local_question_mcq);
}
else {
local_question_mcq = {...packedQuestion};
local_question_mcq['question_text'] = value;
setPackedQuestion(local_question_mcq);
}
}
function mcqChoiceGeneratingFunc() {
return (
<div class = "Opt">
<TextField onChange = {e => optionOnInputFunc('a', e.target.value)} label="Option A" variant="filled" multiline rowsMax={4}/>
<TextField onChange = {e => optionOnInputFunc('b', e.target.value)} label="Option B" variant="filled" multiline rowsMax={4}/>
<TextField onChange = {e => optionOnInputFunc('c', e.target.value)} label="Option C" variant="filled" multiline rowsMax={4}/>
<TextField onChange = {e => optionOnInputFunc('d', e.target.value)} label="Option D" variant="filled" multiline rowsMax={4}/>
</div>
);
}
return (
<Paper class="Question">
<form class="Question-form">
<a class = "editpencil">✎</a>
<InputBase class = "questionedit"
onChange = {e => questionOnInputFunc(true, e.target.value)}
defaultValue={"Question "+props.id}
inputProps = {{"maxlength": 40}}/>
<div class="question-text">
<TextField onChange = {e => questionOnInputFunc(false, e.target.value)} variant="outlined" fullWidth="true" label="Type your question"></TextField>
</div>
{mcqChoiceGeneratingFunc()}
</form>
</Paper>
);
}
export default McqQuestion;
The behavior I am describing can be seen in these screenshots.
The first two screenshots are expected. Two new questions were added and their respective objects were in the console log.
Expected Behavior at the start of the state
Expected behavior when two new questions were added
When question 1 was edited while questions 2 and 3 were there, the objects for question 3 disappeared.
Why is this happening and how do I fix this?
Issues
ExamArea
Storing react components in state is a React anti-pattern and sure-fire way to get yourself some stale state enclosures.
Store just the data in state and render the UI from it.
Any time you are updating react state that depends on the previous state (i.e. appending an element to an array, incrementing a count/id, etc...) you don't use a functional state update.
Use a functional state update to correctly update from any previous state versus state from the previous render cycle.
McqQuestion
Once I resolved your issues in ExamArea I was a bit thrown off by the usage of local_question_mcq and local_answerChoices_mcq. At first glance they appeared to be "state" that wasn't part of component state.
Limit the scope of utility variables such as local_question_mcq and local_answerChoices_mcq
Similar issues with the functional updates, but coupled to the overscoped local_question_mcq and local_answerChoices_mcq.
Use a functional state update to directly update packedQuestion in the onChange handlers.
Solution
ExamArea
Store only data in component state.
Map state to UI in render function.
Use functional state update to map previous state to next state. Use the question ID to match the question that needs to be updated and also shallow copy it.
Pass returnFromMcqQuestion as prop directly (not stored in state either).
Code:
function ExamArea(props) {
const [currentQuesID, setCurrentQuesID] = useState(2);
const [mcqQuestionList, setmcqQuestionList] = useState([{ id: 1 }]); // <-- store data only
function returnFromMcqQuestion(quesID, thisQuestion) {
setmcqQuestionList((mcqQuestionList) => // <-- functional state update
mcqQuestionList.map((question) =>
question.id === quesID // <-- shallow copy matching question
? {
...question,
...thisQuestion
}
: question
)
);
}
function generateMCQ(questionid) {
return {
id: questionid
};
}
function addAnotherQuestion() {
setmcqQuestionList((mcqQuestionList) => // <-- functional state update
mcqQuestionList.concat(generateMCQ(currentQuesID))
);
setCurrentQuesID((c) => c + 1); // <-- functional state update
}
return (
<div className="ExamArea">
{mcqQuestionList.map(({ id }) => (
<McqQuestion
key={id}
returnfunc={returnFromMcqQuestion} // <-- pass callback directly
id={id}
/>
))}
<button
onClick={addAnotherQuestion}
className="add_another_question_button"
>
+ Add Another Question
</button>
</div>
);
}
McqQuestion
Use functional state update to map previous state to next state.
Limit the scope of local_question_mcq and local_answerChoices_mcq, move them into fillUpQuestionWithDefault and declare them const.
Make code more DRY where possible.
Fix class vs className and other various React warnings.
Code:
function McqQuestion(props) {
const [packedQuestion, setPackedQuestion] = useState({});
useEffect(() => {
props.returnfunc(props.id, packedQuestion); // <-- update state in parent
}, [packedQuestion]);
function fillUpQuestionWithDefault() {
/*These two variables store a local copy of packedQuestion. These variables are first updated with the information from
onChange (or a variation of it), and then packedQuestion is set to an instance of this. */
const local_question_mcq = { // <-- provide initial values, then override
id: props.id,
title: `Question ${props.id}`,
};
const local_answerChoices_mcq = {};
function addOption(character, value = '') {
local_answerChoices_mcq[character] = value;
local_question_mcq["answer_choices"] = local_answerChoices_mcq;
}
function addQuestion(title, value) {
local_question_mcq[title ? "title" : "question_text"] = value; // <-- DRY
}
addQuestion(true, "Question " + props.id);
addQuestion(false, "");
['a', 'b', 'c', 'd'].forEach(c => addOption(c, '')); // <-- DRY
setPackedQuestion(local_question_mcq);
}
useEffect(() => {
fillUpQuestionWithDefault();
}, []);
function optionOnInputFunc(character, value) {
setPackedQuestion((question) => ({ // <-- functional state update
...question,
answer_choices: {
...question.answer_choices,
[character]: value
}
}));
}
function questionOnInputFunc(title, value) {
setPackedQuestion((question) => ({ // <-- functional state update
...question,
[title ? 'title' : 'question_text']: value
}));
}
function mcqChoiceGeneratingFunc() {
return (
<div className="Opt">
...
</div>
);
}
return (
<Paper className="Question">
...
</Paper>
);
}
When you are calling this function from child component then ques take the value of initial state in hook that is {}. Now you are adding key quesID in temp and updating the state. So it will be an expected behavior.
function returnFromMcqQuestion(prevQues, quesID, thisQuestion) {
var temp = {...prevQues};
temp["ques"+quesID] = thisQuestion;
setQues(prevQues);
}
So you need something like this.
<McqQuestion ques={ques} returnfunc={returnFromMcqQuestion} id={questionid}/>)
useEffect(()=> props.returnfunc(props.ques, props.id, packedQuestion));

What is the reason behind react component showing the changes of state inside them?

When we have many components in react project and sometimes we use multiple pre-made components for making a page. While using onChange inside a component and showing the result of the state, in this case, what functionality of components allows the value render of state and how it works when we have multiple components inside other components.
Here is an Ex...
function Component() {
const [value, setValue] = React.useState()
const handleChange = val => {
setValue(val)
}
return (
<React.Fragment>
<Compo1 //perform adding +1
onChange={handleChange}
/>
Value: {value} // 1
{console.log("value", value)} // showing right value
<Compo2>
<Compo3>
<Compo1 //perform adding +1
onChange={handleChange}
/>
Value:{value} // undefined
{console.log("value", value)} // showing right value
</Compo3>
{console.log("value", value)} // showing right value
</Compo2>
</React.Fragment>
)
}
render(<Component />)
In this case why console is showing the right value but the state variable value is showing undefined.
The only way I can get that code to do what you say it does is when you incorrectly use React.memo on Compo3:
const Compo1 = ({ onChange }) => (
<button onClick={() => onChange(Date.now())}>+</button>
);
const Compo2 = ({ children }) => <div>{children}</div>;
const Compo3 = React.memo(
function Compo3({ children }) {
return <div>{children}</div>;
},
() => true//never re render unless you re mount
);
function Component() {
const [value, setValue] = React.useState(88);
const handleChange = React.useCallback(() => {
setValue((val) => val + 1);
}, []);
return (
<React.Fragment>
<Compo1 //perform adding +1
onChange={handleChange}
/>
works: {value}-----
<Compo2>
<Compo3>
<Compo1 //perform adding +1
onChange={handleChange}
/>
broken:{value}-----
</Compo3>
</Compo2>
</React.Fragment>
);
}
ReactDOM.render(
<Component />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Maybe you can do the same if you do some wacky stuff with shouldComponentUpdate
A component will render when:
When the parent renders the child and the child is a functional component (not wrapped in React.memo)
When the parent renders the child with different prop values than the previous render.
When value in [value,setValue]=useState() or when this.state changes (when state changes).
When someContext in value = useContext(someContext) changes (even if value doesn't change).
In most cases when value in value = useCustomHoom() changes but this is not guaranteed for every hook.
When Parent renders and passes a different key prop to Child than the previous render (see 2). This causes the Child to unmount and re mount as well.
In the example the Compo3 wants to re render because Parent is re rendered due to a state change and passes different props (props.children).
Compo3 is not a functional component because it's wrapped in React.memo. This means that Compo3 will only re render if props changed (pure component).
The function passed as the second argument to React.memo can be used to custom compare previous props to current props, if that function returns true then that tells React the props changed and if it returns false then that tells React the props didn't change.
The function always returns true so React is never told that the props changed.

Categories

Resources