React.JS - Child Componenet Not Updating When Recieving Props (Function Components) - javascript

I have a higher order functional component in my app that passes JSON to it's child component. I am using react Hooks to manage state. I can't use componentDidUpdate since this is not a class component.
I'm using useEffect() to get it to process the JSON on initial render, but after that I can't seem to get it to update. I've confirmed the PROPS are indeed changing and the JSON is unique (it changes on the click of a button).
Here is my code:
function FormSection(props) {
const [questions, updateQuestions] = useState([]);
const [sectionJSON, updateSectionJSON] = useState(props.json);
const fetchQuestionsData = json => {
/* API CALL TRANSFORM JSON INTO HTML FOR USE ELSEWHERE IN THE APP */
};
useEffect(() => {
fetchQuestionsData(sectionJSON);
}, [sectionJSON]);
...
}
I've also tried changing the useEffect() hook to use props directly:
useEffect(() => {
fetchQuestionsData(props.json);
}, [props.json]);
The parent componenet is calling it as follows:
<FormSection key={i} json={newJSON} />
Is there something obvious I am missing?

When you set,
const [sectionJSON, updateSectionJSON] = useState(props.json);
is equivalent to,
constructor(props){
super(props);
this.state = {
sectionJSON: props.json
}
}
So your sectionJSON will set only once and any subsequent change in props.json will not change sectionJSON.
So this will execute only once, and not when props.json changes
useEffect(() => {
fetchQuestionsData(sectionJSON);
}, [sectionJSON]);
To make this work you need to pass props.json to your useEffect hook as,
useEffect(() => {
fetchQuestionsData(props.json);
}, [props.json]);
Doing this will not re-render your component, because your component's state is not changing, like we do using componentDidUpdate in class based component, so you should set your state in fetchQuestionsData like,
const fetchQuestionsData = json => {
/* API CALL TRANSFORM JSON INTO HTML FOR USE ELSEWHERE IN THE APP */
updateSectionJSON(json); //This will re-render your component.
};

Related

Is there any way I can update a mounted React node's props in my current setup?

So I have a mounted React application that is rendered like this (it's via another framework on top):
const componentRender = (entryComponent, initialProps, targetNode) => {
root.render(wrapper);
}
On top level, I call a 3rd party library and wrap my React application inside of it.
What's the best way to integrate React inside of a 3rd party library that its supposed to render into?
The best practice is to update props within a component, not outside of the render function. Otherwise, as you've said, it will cause a full remount.
So the solution would be to call your Ext.extend function from within the App component.
function App() {
const [ready, setReady] = useState(false);
const [props, setProps] = useState({});
// Calls once on first mount.
useEffect(() => {
Ext.extend(Ext.Container, {
listeners: {
afterrender: function () {
setProps(this.myProps);
setReady(true);
},
},
constructor: function (...args) {
Object.assign(this, args);
},
onServerSuccess: function(data) {
// here is where I receive new data
setProps(data);
}
});
}, [])
return (
ready ? <Component {...props} /> : null
);
}
ReactDOM.render(App, rootNode);
If there is more interaction between Ext and the react component, for example you need to pass this.root into a function, you can still do that with the normal DOM APIs like document.getElementById. Or you can add it like this:
// use this reference to do stuff with the react object
this.root = <Component {...props} />
return this.root;
but changing the react component object directly would be an anti-pattern. All state/props updates should happen using APIs provided by the react library.
Also if for some reason you need to render the react component directly inside the Ext.Container node, then you should use ReactDOM.createPortal instead of render.
Here's how that would look.
import { createPortal } from 'react-dom';
const Portal = memo(({ children, domNode }) => createPortal(
children,
domNode,
));
function App() {
const [ready, setReady] = useState(false);
const [props, setProps] = useState({});
// Calls once on first mount.
useEffect(() => {
Ext.extend(Ext.Container, {
listeners: {
afterrender: function () {
setProps(this.myProps);
setReady(true);
},
},
constructor: function (...args) {
Object.assign(this, args);
},
onServerSuccess: function(data) {
// here is where I receive new data
setProps(data);
}
});
}, [])
return (
ready ?
<Portal domNode={extComponentNode}>
<Component {...props} />
</Portal> : null
);
}
Where extComponentNode is the node you want it rendered in. createPortal allows the react component to share state with your react App even though it's rendered in a different place, without the issue of calling render again.
I suppose you want to re-render the component with updated states without unmounting-mouting it whenever the prop(s) passed to the component changes.
Create a useEffect inside this component to re-render the component whenever the required prop(s) changes. To do this, add the prop(s) for which you want to re-render the component in the dependency array of useEffect and pass a function that will update the states of the component.
useEffect(
<function which will update the states, causing re-rendering of the component>,
[ <all props for which you want to run the function separated by a comma> ]
)

useState and changes in the props

I'm trying to understand what happens when you have both props and useState in one component.
I wrote little example of it which has one parent component that prints its numbers with another child component -
const MyNumbers = (props) => {
const [numbers, setNumbers] = useState([...props.arr]);
function changeNumbers() {
setNumbers((nums) => [...nums.map(() => Math.floor(Math.random() * 10))]);
}
return (
<div className="MyNumbers">
<div>
<button onClick={changeNumbers}>Chane numbers</button>
</div>
<div>
{numbers.map((num, idx) => (
<SingleNumber key={idx} num={num}></SingleNumber>
))}
</div>
</div>
);
};
const SingleNumber = (props) => {
const [num] = useState(props.num);
useEffect(() => {
console.log("useEffect called");
});
return <h3>The number is {num}</h3>;
};
Here is the above demo
The SingleNumber component uses useState and as you can see clicking on the "Change numbers" action doesn't change the values in the children component.
But when I wrote almost the same code but now SingleNumber doesn't use useState then clicking on the "Change numbers" changes all the values in the children component (like in this demo).
Is it correct to say that a function component with a useState renders once and then only changed if the state changed but not if the props changed ?
OFC the component "rerenders" when the props change, the useEffect hook in SingleNumber is showing you that the "render phase" is run each time the props change.... effects are run each time the component is rendered.
const SingleNumber = (props) => {
const [num] = useState(props.num);
useEffect(() => {
console.log("useEffect called"); // <-- logged each time the component renders
});
return <h3>The number is {num}</h3>;
};
If you added a dependency on props.num and updated the local state (don't actually do this, it's an anti-pattern in react!), you'll see the UI again update each time the props update.
To answer your queston:
Is it correct to say that a function component with a useState renders
once and then only changed if the state changed but not if the props
changed?
No, this is not technically correct to say if "render" to you means strictly react rendered the component to compute a diff, react components rerender when state or props update. Yes, if "render" more generally means you visually see the UI update.
When you call useState it returns an array with two values in it:
The current value of that bit of the state
A function to update the state
If there is no current value when it sets the state to the default value and returns that.
(The default value is the argument you pass to useState).
If you change the values of props in your example, then the component rerenders.
useState returns the current value of that bit of the state. The state has a value, so it doesn't do anything with the argument you pass to useState. It doesn't matter that that value has changed.
Since nothing else has changed in the output, the rerendered component doesn't update the DOM.
Is it correct to say that a function component with a useState renders once and then only changed if the state changed but not if the props changed?
No, it does rerender but doesn't commit the changes.
When parent component MyNumbers re-renders by clicking changeNumbers, by default (unless React.memo used) all its children components (like SingleNumber) will be re-render.
Now when SingleNumber rerenders, notice useState docs.
During the initial render, the returned state (state) is the same as the value passed as the first argument (initialState).
You initial the state useState(props.num) but it can only be changed by calling the setter function, therefore the state num won't change because you never calling the setter.
But it will rerender on parent render as mentioned above (notice the useEffect logs).
You don't need to use useState in SingleNumber.
because useState called only once when it rendered.
const SingleNumber = (props) => {
// const [num] = useState(props.num);
// useEffect(() => {
// console.log("useEffect called");
// });
return <h3>The number is {props.num}</h3>;
};
if you want to use useState, you can use like this.
const SingleNumber = (props) => {
const [num, setNum] = useState(props.num);
useEffect(() => {
console.log("useEffect called");
setNum(props.num);
}, [props.num]);
return <h3>The number is {num}</h3>;
};

cant pass react state to a child after setState()?

I'm working on reactjs project
where I fetching data from firestore and set the app.js state to the fetched data, and I pass this state to a child of app.js to display it but it's undefined at first then it consoles the right state.
How can I make the child component render only after its props is correct?!
fetchDataFromFirestore = async () => {
let dataRefFromFirestore = database.doc('items/fruitsDataJsonFile');
(await dataRefFromFirestore).onSnapshot((snapshot) => {
let fetchedItems = snapshot.data();
this.setState({
fetchedItems: fetchedItems.data
},
console.log('DONEEE ADIING'))
})
}
You can use a concept called "Conditional Rendering".
It will be like
{!!this.state.fetchedItems?.length &&
<YourChildComponent fetchedItems={this.state.fetchedItems}>
Then, your child component will be rendered only when the state has array data.
Similarly, your child component will have props called fetchedItems with full data.
Reference: https://reactjs.org/docs/conditional-rendering.html

What gets updated in React?

I am new to React and I am a little confused about what gets updated every time the state or props change. For instance:
const Foo = props => {
const [someState, setSomeState] = useState(null)
let a
useEffect(() => {
a = 'Some fetched data'
}, [])
}
Now if the state (i.e. someState) or props get updated, does it run through the function again, making a undefined? Does only JSX elements that depend on the state/props and the hooks that use them get affected? What changes exactly?
To understand how the state affect your component, you could check this example on Stackblitz. It shows you how the local variable of your component act when a state changes. This behavior is exactly the same when the component receive a new prop. Here is the code just in case :
import React, { Component } from "react";
import { render } from "react-dom";
const App = () => {
const [state, setState] = React.useState('default');
// see how the displayed value in the viewchanges after the setTimeout
let a = Math.floor(Math.random() * 1000);
React.useEffect(() => {
a = "new a"; // Does nothing in the view
setTimeout(() => setState("hello state"), 2000);
}, []);
return (
<div>
{a} - {state}
</div>
);
};
render(<App />, document.getElementById("root"));
What you can see is :
a is initialized with a random value and it is displayed in the view
useEffect is triggered. a is edited but does not re-render your component
After 2 seconds, setState is called, updating the state state
At this point, a value has changed for a new one because your component is re-rendered after a state update. state also changed for what you gave to it
So for your question :
if the state (i.e. someState) or props get updated, does it run through the function again, making a undefined? Does only JSX elements that depend on the state/props and the hooks that use them get affected? What changes exactly?
The answer is yes, it does run through the function again, and the state/props that has been updated are updated in the JSX. Local variable like a will be set to their default value.

React - functions as props causing extra renders

I have some heavy forms that I'm dealing with. Thus, I'm trying to squeeze performance wherever I can find it. Recently I added the Why-did-you-render addon to get more insight on what might be slowing down my pages. I noticed that, for example, when I click on a checkbox component about all of my other components re-render. The justification is always the same. WDYR says
Re-rendered because of props changes: different functions with the
same name {prev onChangeHandler: ƒ} "!==" {next onChangeHandler: ƒ}
As much as possible, I try to respect best the best practices indications that I find. The callback functions that my component passes follow this pattern
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
export function TopLevelComponent({props}){
const defaultData = {name: '', useMale: false, useFemale: false}
const [data, setData] = useState(defData);
const { t } = useTranslation();
const updateState = (_attr, _val) => {
const update = {};
update[_attr] = _val;
setData({ ...data, ...update });
}
const updateName = (_v) => updateState('name', _v);//Text input
const updateUseMale = (_v) => updateState('useMale', _v);//checkbox
const updateUseFemale = (_v) => updateState('useFemale', _v);//checkbox
...
return <div>
...
<SomeInputComponent value={data.name} text={t('fullName')} onChangeHandler={updateName} />
<SomeCheckboxComponent value={data.useMale} onChangeHandler={updateUseMale} text={t('useMale')}/>
<SomeCheckboxComponent value={data.useFemale} onChangeHandler={updateUseFemale} text={t('useFemale')}/>
...
</div>
}
In an example like this one, altering any of the inputs (eg: Writing text in the text input or clicking one of the checkboxes) would cause the other 2 components to re-render with the justification presented above.
I guess that I could stop using functional components and utilize the shouldComponentUpdate() function, but functional components do present some advantages that I'd rather keep. How should I write my functions in such a way that interacting with one input does not force an update on another input?
The problem stems from the way you define your change handlers:
const updateName = (_v) => updateState('name', _v)
This line is called on each render and thus, every time your component is rendered, the prop has a new (albeit functionality-wise identical) value. The same holds for every other handler as well.
As an easy solution you can either upgrade your functional component to a fully fledged component and cache the handlers outside of the render function, or you can implement shouldComponentUpdate() in your child components.
You need to use memo for your child components to reduce renders
const SomeInputComponent = props => {
};
export default memo(SomeInputComponent);
// if it still causes rerender witout any prop change then you can use callback to allow or block render
e.f.
function arePropsEqual(prevProps, nextProps) {
return prevProps.name === nextProps.name; // use your logic to determine if props are same or not
}
export default memo(SomeInputComponent, arePropsEqual);
/* One reason for re-render is that `onChange` callback passed to child components is new on each parent render which causes child components to re-render even if you use `momo` because function is updated on each render so in order to fix this, you can use React hook `useCallback` to get the same function reference on each render.
So in you parent component, you need to do something like
*/
import { useCallback } from 'react';
const updateName = useCallback((_v) => updateState('name', _v), [])
You have to memoize parent function before pass to children, using useCallback for functional component or converting to class property if you use class.
export default class Parent extends React.PureComponent {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
}
onClick() {
console.log("click");
}
render() {
return (
<ChildComponent
onClick={ this.onClick }
/>
);
}
}
with useCallback:
Parent = () => {
const onClick = useCallback(
() => console.log('click'),
[]
);
return (
<ChildComponent
onClick={onClick}
/>
);
}

Categories