Child component not updating with updates state via Parent props - javascript

I have a parent component from where I am passing a callback function handleProjectStagesSave and some data as props to child component. In my child component, I am accessing the props and storing the props object as a whole to the state using useState.
Parent component:
function ProjectStages(props) {
const [projectStages, setProjectStages] = React.useState(null);
useEffect(() => {
ProjectAPI.getProjectStages().then((response) => {
setProjectStages(response);
});
}, []);
//this function is passed as prop to child component. child component send data through parameters back to parent
function handleProjectStagesSave(val){
projectStages.someProperty = val; //modifying state property here and updating the state below
setProjectStages(projectStages);
}
if(projectStages === null){
return (<Loader/>);
}
return (
<div>
{(() => {
//loop through state data and pass props to child component
return projectStages.phases.map((stage) => {
return (
<ProjectStageTile criterias={stage.criterias} stagesSave={handleProjectStagesSave}/>
);
});
})()}
</div>
);
}
Now, the child component passes the callback function to it's own child component CriteriaScores which basically passes some data and sends it back all the way up to the parent.
Child component:
function ProjectStageTile(props){
const [projectStageTileState, setProjectStageTileState] = React.useState(props);
return projectStageTileState.criterias.map((criteria) => {
return(
<div>
<div>
{criteria.selectedScore}
</div>
<CriteriaScores stagesSave={projectStageTileState.stagesSave} />
</div>
);
});
}
It works fine and I get the data and store update the state setProjectStages(projectStages); in parent component. Now, I expect the child component to have the updated state data immediately. However, what I saw in React dev tools is that, after updating the state, I update the props in the child component manually, then the state changes are reflected in the Child component. Any idea what is wrong? Please let me know if I can clarify more on this.
Update-1:
Instead of mutating the state directly I tried something like below but got same result.
projectStages.phases.filter((phase) => (phase.gdfatStage === "bid"))[0].criterias
.filter((criteria) => (criteria.criteriaId === val.split(' ')[1]))[0].selectedScore = val.split(' ')[2];
let newProjectStages = projectStages;
setProjectStages(newProjectStages);
Update-2:
Also tried below approach but it did not work.
let newProjectStages = projectStages;
newProjectStages.phases.filter((phase) => (phase.gdfatStage === "bid"))[0].criterias
.filter((criteria) => (criteria.criteriaId === val.split(' ')[1]))[0].selectedScore = val.split(' ')[2];
setProjectStages(newProjectStages);

projectStages is a state variable, and you are mutating it directly.
projectStages.someProperty = val;
The recommended way of doing this is as follows:
setProjectStages(...projectStages, someProperty : val)
When you use setProjectStages react will re-render, and the most recent state variable will be passed to the child component.

This worked:
const newProjectStages = Object.assign({}, projectStages); //this is important
newProjectStages.phases.filter((phase) => (phase.gdfatStage === "bid"))[0].criterias
.filter((criteria) => (criteria.criteriaId === val.split(' ')[1]))[0].selectedScore = val.split(' ')[2];
setProjectStages(newProjectStages);

In addition to the answer provided by #user9408899, I would add that the child component does not need to store its props in state. I would change it to:
function ProjectStageTile(props){
const {criteria, stagesSave} = props;
return criterias.map((criteria) => {
return(
<div>
<div>
{criteria.selectedScore}
</div>
<CriteriaScores stagesSave={stagesSave} />
</div>
);
});
}

Related

Children props event doesn't hold parent's current state

I have a state variable which holds components created dynamically, however, when I access the state from a function passed to the child as props, I get the state status from back when it was created. Not so when I log useEffect.
For example: I add 3 children, and in the function logMyChildren I get the state previous to the creation of the last Child element.
First Child mychildren is []
Second Child myChildren is [{Child with id 0}]
Third Child myChildren is [{Child with id 0}, {Child with id 1}]
It gives me the same state with each Child every time I call that function.
Is there a way to get the current state(not a state from the past) regardless of the children?
const Parent = () => {
const [myChildren, setMyChildren] = useState([])
const addChild = () => {
let id = myChildren.length + 1
setMyChildren([
...myChildren,
<Child key={id} id={id} logMyChildren={logMyChildren} />,
])
}
const logMyChildren = (id) => {
console.log(id, myChildren)
}
useEffect(() => {
console.log(myChildren)
}, [myChildren])
return (
<>
<button onClick={addChild}>Add a child</button>
{myChildren && myChildren.map((child) => child)}
</>
)
}
const Child = ({ id, logMyChildren }) => {
return (
<>
<p>A child with id {id}!</p>
<button onClick={() => logMyChildren(id)}>X</button>
</>
)
}
Every time useEffect() runs, it has the updated state.
Thanks.
The problem for you is that you are creating logMyChildren that encloses state variable (in your case mychildren).
What you could do is to use useRef
Something like this:
const stateRef = useRef();
stateRef.current = myChildren;
And then in logMyChildren you use ref - stateRef:
console.log(id,stateRef.current);

Get Elements, (children) of React.element

So I'm having an issue I would like to resolve, Maybe someone has an answer for it.
My problem is that I have a Component that has its own Views and Components, at the same time I have a Parent Component thats using the this specific Component.
I want to check if the child of the Child Component has some props.
Child Component
const Child = () => {
return (
<View wantedArgument={true}>
<View anotherWantedArgument={false}>
</View>
</View>
)
}
Parent Component
const Parent = () => {
return (
<Child>
</Child>
)
}
So I want to get the props values of the child views.
I can use useRef for those Views, but it's not that generic and dynamic.
My question is, is there a way I can get those elements of the child?
Thanks ahead
Eden.
You can check props of Parent's children using React.Children API.
In this example, we checking the props of every Parent's child and logging them.
If you want to go to a deeper level (Child of Child of Child etc.), do it with recursion with inductive step child.props.children (if available).
const Child = ({ prop }) => {
return <>{prop}</>;
};
const Parent = ({ children }) => {
useEffect(() => {
React.Children.forEach(children, child => {
console.log(child.props);
});
}, [children]);
return <div>{children}</div>;
};
const App = () => {
return (
<Parent>
<Child prop={1} />
</Parent>
);
};

React Hook - I always get stale values from useState just because child component never updates

TL;DR This is my Parent component:
const Parent = () => {
const [open, setOpen] = useState([]);
const handleExpand = panelIndex => {
if (open.includes(panelIndex)) {
// remove panelIndex from [...open]
// asign new array to variable: newOpen
// set the state
setOpen(newOpen);
} else {
setOpen([...open, panelIndex]);
}
}
return (
<div>
<Child expand={handleExpand} /> // No need to update
<Other isExpanded={open} /> // needs to update if open changed
</div>
)
}
And this is my Child component:
const Child = (props) => (
<button
type="button"
onClick={() => props.expand(1)}
>
EXPAND PANEL 1
</button>
);
export default React.memo(Child, () => true); // true means don't re-render
Those code are just an example. The main point is I don't need to update or re-render Child component because it just a button. But the second time I click the button it's not triggering Parent to re-render.
If I put console.log(open) inside handleExpand like so:
const handleExpand = panelIndex => {
console.log(open);
if (open.includes(panelIndex)) {
// remove panelIndex from [...open]
// asign new array to variable: newOpen
// set the state
setOpen(newOpen);
} else {
setOpen([...open, panelIndex]);
}
}
it printed out the same array everytime I clicked the button as if the value of open which is array never updated.
But if I let <Child /> component re-render when open changed, it works. Why is that? is this something as expected?
This is indeed expected behavior.
What you are experiencing here are function closures. When you pass handleExpand to Child all referenced variables are 'saved' with their current value. open = []. Since your component does not re-render it will not receive a 'new version' of your handleExpand callback. Every call will have the same result.
There are several ways of bypassing this. First obviously being letting your Child component re-render.
However if you strictly do not want to rerender you could use useRefwhich creates an object and access it's current property:
const openRef = useRef([])
const [open, setOpen] = useState(openRef.current);
// We keep our ref value synced with our state value
useEffect(() => {
openRef.current = open;
}, [open])
const handleExpand = panelIndex => {
if (openRef.current.includes(panelIndex)) {
setOpen(newOpen);
} else {
// Notice we use the callback version to get the current state
// and not a referenced state from the closure
setOpen(open => [...open, panelIndex]);
}
}

React Native - Update Parent State in Child with Dynamic Key

I am very new to both Javascript and React Native, and I am trying update a parent's state by using a callback function using a dynamic key to avoid writing multiple functions.
In the parent component, I pass this function to the child to child to update the parent's state based on user text input. This code achieves the desired result.
In Parent:
_setAge = (value) => {
this.setState({age: value})}
<ChildComponent name = 'Update Age' _setAge = { this._setAge.bind(this) } />
In Child:
//Other child code
<TextInput onChangeText = { (input) => {this.props._setAge(input)} }
//Etc.
However, I am looking for a way to pass a desired state key from the parent to the child to update the state dynamically. I have tried the following:
In Parent:
const ageKey = 'age'
_setAge = (value, stateKey) => {
this.setState({ [stateKey]: value })}
<ChildComponent name = 'Update Age' _setAge = { this._setAge.bind(this) } stateKey = ageKey } />
In Child:
//Other child code
<TextInput onChangeText = { (input) => this.props._setAge(input, this.props.stateKey)
//Etc.
However this doesn't work. My current work around is writing 6 functions for my 6 child components, each updating the desire state. However, while this would work for my basic app, I am looking for a way that is more scalable for future projects. Thank you!
In Parent
Instead of passing stateKey in props on child key directly pass the state key in onChageText method in child. the code would look like this->>>>
_setAge = (value, stateKey) => {
this.setState({ [stateKey]: value })}
<ChildComponent name = 'Update Age' _setAge = {this._setAge} /> // providing no statekey here
Inside the child
<TextInput onChangeText = { (input) => this.props._setAge(input, 'age') }
// here i know statekey for each child so i directly pass the key from child so i dont have to pass it as a props and then access it

ComponentDidUpdate doesn't work

I'm trying to render dynamically a collection of component using componentDidUpdate.
This is my scenario:
var index = 0;
class myComponent extends Component {
constructor(props) {
super(props);
this.state = {
componentList: [<ComponentToRender key={index} id={index} />]
};
this.addPeriodHandler = this.addPeriodHandler.bind(this);
}
componentDidUpdate = () => {
var container = document.getElementById("container");
this.state.componentList.length !== 0
? ReactDOM.render(this.state.componentList, container)
: ReactDOM.unmountComponentAtNode(container);
};
addHandler = () => {
var array = this.state.componentList;
index++;
array.push(<ComponentToRender key={index} id={index} />);
this.setState = {
componentList: array
};
};
render() {
return (
<div id="Wrapper">
<button id="addPeriod" onClick={this.addHandler}>
Add Component
</button>
<div id="container" />
</div>
);
}
}
The problem is that componentDidUpdate work only one time, but it should work every time that component's state change.
Thank you in advance.
This is not how to use react. With ReactDOM.render() you are creating an entirely new component tree. Usually you only do that once to initially render your app. Everything else will be rendered by the render() functions of your components. If you do it with ReactDOM.render() you are basically throwing away everything react has already rendered every time you update your data and recreate it from scratch when in reality you may only need to add a single node somewhere.
Also what you actually store in the component state should be plain data and not components. Then use this data to render your components in the render() function.
Example for a valid use case:
class MyComponent extends Component{
state = {
periods: []
};
handleAddPeriod = () => {
this.setState(oldState => ({
periods: [
...oldState.periods,
{/*new period data here*/}
],
});
};
render() {
return (
<div id="Wrapper">
<button id="addPeriod" onClick={this.handleAddPeriod}>
Add Component
</button>
<div id="container">
{periods.map((period, index) => (
<ComponentToRender id={index} key={index}>
{/* render period data here */}
</ComponentToRender>
))}
</div>
</div>
);
}
}
}
Also you should not work with global variables like you did with index. If you have data that changes during using your application this is an indicator that is should be component state.
try
addHandler = () =>{
var array = this.state.componentList.slice();
index++;
array.push(<ComponentToRender key={index} id={index}/>);
this.setState=({
componentList: array
});
}
if that works, this is an issue with the state holding an Array reference that isn't changing. When you're calling setState even though you've added to the Array, it still sees the same reference because push doesn't create a new Array. You might be able to get by using the same array if you also implement shouldComponentUpdate and check the array length of the new state in there to see if it's changed.

Categories