react parent component re-rendering again after all components have been rendered - javascript

Here's my code
// App.js
import { useState } from "react";
import Child from "./Child";
export default function App() {
const [value, setValue] = useState(0);
const [isInvalid, setIsInvalid] = useState(false);
const handleChange = (e) => setValue(e.currentTarget.value);
const handleInvalid = (isValue) => setIsInvalid(isValue);
console.log("parent", isInvalid);
return (
<Child
value={value}
handleInvalid={handleInvalid}
handleChange={handleChange}
/>
);
}
//Child.js
import { useEffect } from "react";
export default function Child({ value, handleInvalid, handleChange }) {
console.log("child");
useEffect(() => {
console.log("child effect");
handleInvalid(!(Number(value) >= Number(0) && Number(value) <= Number(24)));
}, [value, handleInvalid]);
return (
<input
type="number"
min={0}
max={24}
value={value}
onChange={handleChange}
/>
);
}
run => https://codesandbox.io/s/bumo-jasig-rendeoring-sunseo-isyu-forked-08gszy?from-embed
Problem
When the components are first rendered, they work as expected.
like this:
parent false
child
child effect
However, if I change the input value, the result is like this:
parent false
child
child effect
parent false
As you can see, the parent component is rendered once more at the end.
What is the problem and how do I fix it?

Update your handleInvalid function to call the setter function only if value actually changed:
const handleInvalid = (isValue) => isValue !== isInvalid && setIsInvalid(isValue);
// or, lengthier
// const handleInvalid = (isValue) => { if (isValue !== isInvalid) setIsInvalid(isValue) };
Currently, each change of input in Child component fires two setters on Parent:
handleChange, which calls setValue(...), triggering the first Parent rerender
handleInvalid (as part of Child useEffect), which always calls setIsInvalid(...). It happens while Parent component is still not 'finalized' (that's the difference with initial render, when handleInvalid is called as well as part of Child "mounting"), hence render function for Parent is called twice.

You declare states at Parent and pass them to Child via props.
so Parent will re-render whenever its states change
have no problem here

Related

Is there any way to trigger React.useEffect from different component?

Imagine two components like this in React:
import MyComponent2 from "./components/MyComponent2";
import React from "react";
export default function App() {
const [myState, setMyState] = React.useState([]);
React.useEffect(() => {
console.log("useEffect triggered");
}, [myState]);
return <MyComponent2 myState={myState} setMyState={setMyState} />;
}
import React from "react";
export default function MyComponent2(props) {
const [inputValue, setInputValue] = React.useState("");
function handleChange(e) {
setInputValue(e.target.value);
let list = props.myState;
list.push(`${e.target.value}`);
props.setMyState(list);
console.log(props.myState);
}
return (
<div>
<input
type="text"
value={inputValue}
name="text"
onChange={handleChange}
/>
</div>
);
}
As you can see I am making changes with props.setMyState line in second component. State is changing but Somehow I could not trigger React.useEffect in first component even tough It is connected with [myState]. Why ?
In short form of my question : I can not get "useEffect triggered" on my console when i make changes in input
Instead of providing myState and setMyState to MyComponent2, you should only provide setMyState and use the functional update argument in order to access the current state.
In your handleChange function you are currently mutating the React state (modifying it directly):
let list = props.myState; // This is an array that is state managed by React
list.push(`${e.target.value}`); // Here, you mutate it by appending a new element
props.setMyState(list);
// ^ You update the state with the same array here,
// and since they have the same object identity (they are the same array),
// no update occurs in the parent component
Instead, you should set the state to a new array (whose object identity differs from the current array):
props.setMyState(list => {
const newList = [...list];
newList.push(e.target.value);
return newList;
});
// A concise way to write the above is like this:
// props.setMyState(list => [...list, e.target.value]);

Return value from child component to parent component React

So I'm trying to use a react Timer in one of my class components. I figured out how to connect it, but now I need to save the time into the parent's state. How can I go upon doing that? Since the time is being measured on the child component. So essentially I need to save the time measured from the child component to the parent's state.
The phrase that comes to mind is Lifting State up. React has a 'top-down' data flow. So your state needs to be initialised in the parent component and passed down to the child components that need it. e.g. (pseudocode, may not work)
function Parent() {
const [text, setText] = useState('hello world')
return (
<>
<Child text={text} />
<OtherChild text={text} setText={setText} />
</>
)
}
function Child({ text }) {
return <p>{text}</p>
}
function OtherChild({ text, setText }) {
return (
<input
onChange={e => setText(e.target.value)}
value={text} />
)
}
You'll need to pass a function that updates the parent's state to the child component via props:
In this particular example, both the child and the parent props have a state hook that keeps track of time - the parent's updateParentTime function get's called any time the child's time value changes.
NOTE: it's probably not necessary to keep state in both places...I was just showing an example. If the parent needs has state for time, you could also just pass that down to the child component
const Parent = (props) => {
const [time, setTime] = useState(0);
const updateParentTime = (t) => {
setTime(t);
}
return (
<Child updateParentTime={updateParentTime}/>
)
}
const Child = (props) => {
const {updateParentTime} = props;
const [time, setTime] = useState(0);
useEffect(() => {
updateParentTime(time)
}, [time]);
return (
<div>
{/* something goes here */}
</div>
)
}
The following defines two React components: Parent and Child.
Parent declares a piece of state named time and a mutator method for that state named setTime.
The current value of time is passed to Child via the prop named time.
setTime is passed to Child via the prop named notify.
Child configures a timer that invokes notify (ie setTime) once a second.
Invoking notify tells React that a state change has occurred and triggers a re-rendering of both components, causing the time to be rendered by each component, once every second.
pre { border: 1px solid black; padding: 10px; }
<script src="https://unpkg.com/#babel/standalone#7/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<script type="text/babel">
const { useState, useEffect } = React
const Parent = () => {
const [ time, setTime ] = React.useState('--- --- -- ----')
return <pre>
Parent: { time }
<Child notify={setTime} time={ time }/>
</pre>
}
const Child = ({ notify, time }) => {
useEffect(()=>{
const id = setInterval(() => notify(Date()), 1000)
return () => clearInterval(id)
})
return <pre>Child: {time}</pre>
}
ReactDOM.render(<Parent/>, document.querySelector('main'))
</script>
<main></main>

How to prevent unwanted re-renders to stop another API call in react js?

Here is a parent component (Cart) and a child component (CartItem).
in the parent, there is a button that counts on the state that changes by the checkAvailability function which passed to the child via props,
import {useState} from "react";
import CartItem from "./CartItem";
const Cart = ({cart}) => {
const [available, setAvailable] = useState(true);
const checkAvailability = (check) => {
setAvailable(check)
}
return (
<>
{cart.items.map((item) => (
item.is_valid &&
<CartItem
key={item.id}
checkAvailability={checkAvailability}
/>
))}
<button disabled={available} >Click Me!</button>
</>
)
}
export default Cart;
in the child component, an API call returns true or false called by useEffect.
import {useState, useEffect} from "react";
const CartItem = ({checkAvailability}) => {
const [newData, setNewData] = useState(null);
const handleCheck = async () => {
const data = await api.call();
setNewData(data)
if(newData.available === false) {
checkAvailability(false)
} else if(newData.available === true) {
checkAvailability(true)
}
};
useEffect(() => {
handleCheck();
}, []);
return (
<div> Item </div>
)
};
export default CartItem;
issue:
every time the components mount, the API call in the child returns a value, that value gets passed to the parent by the checkAvailability function as a prop, which changes the state in the parent, when the state changes a re-render happen which restarts the circle infinitely.
the main thing is the button gets disabled when the API call returns a {false} value. if this way won't do the job, is there another way of doing it?.
what is the solution?.
Even if you resolved the current issue of infinite re-renders, you'll still have multiple API requests if there are multiple <CartItem/> components.
A better approach will be to move the API call to the parent. This will ensure it's called once regardless of the number of cart items it has. If the cart items need to know the value of available, then pass it to them.
const CartItem = ({ available }) => {
return <div> Item </div>
}

React useState with an empty object causes an infinite loop

Using React hooks with a child component that should get the initial state from the parent and update the parent on every internal state change.
I figured that since it's always the same reference the useEffect of the child should not get called infinitely.
If the initial state of the child is an empty object I get an infinite loop.
If the initial state of the child is taken from the props it works great.
Not sure what's causing it.
You can change the first useState inside the child component to an empty object to make the infinite loop start.
Please review the sandbox below:
https://codesandbox.io/s/weird-initial-state-xi5iy?fontsize=14&hidenavigation=1&theme=dark
Note: I've added a counter to the sandbox to stop the loop after 10 runs and not crash the browser.
import React, { useState, useEffect, useCallback } from "react";
const problematicInitialState = {};
/* CHILD COMPONENT */
const Child = ({ onChange, initialData }) => {
const [data, setData] = useState(initialData); // if initialData is {} (a.k.a problematicInitialState const) we have an infinite loop
useEffect(() => {
setData(initialData);
}, [initialData]);
useEffect(() => {
onChange(data);
}, [data, onChange]);
return <div>Counter is: {data.counter}</div>;
};
/* PARENT COMPONENT */
export default function App() {
const [counterData, setCounterData] = useState({ counter: 4 });
const onChildChange = useCallback(
(data) => {
setCounterData(data);
},
[setCounterData]
);
return (
<div className="App">
<Child onChange={onChildChange} initialData={counterData} />
</div>
);
}
How about putting the state only in the parent component instead, and have the child only reference the props passed down to it, without any state of its own?
const Child = ({ counterData, setCounterData }) => {
return (
<div>
<div>Counter is: {counterData.counter}</div>
<button
onClick={() => setCounterData({ counter: counterData.counter + 1 })}
>increment</button>
</div>
);
};
const App = () => {
const [counterData, setCounterData] = React.useState({ counter: 4 });
return (
<div className="App">
<Child {...{ counterData, setCounterData }} />
</div>
);
}
ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class="react"></div>
Problem is that in JS {} !== {} because objects, unlike primitive values, are compared by reference, not value.
In you useEffect you're comparing 2 objects, because they always have different reference, the'll never be the same in JS land and your useEffect will trigger, setting new object and you got yourself an infinite loop.
You shouldn't use hooks in the same way you used class components in react, meaning you should do
const [counter, setCounter] = useState(4);
This way, you'll pass primitive value down to your child component and useEffect will have much more predictable behaviour.
Also, while this is a test case, you should rarely (read: never) try to set child sate to parent state. You already pass that data from parent to child, no need to create redundant state in your child component, just use the passed in data.
Regarding solutions I propose that you don't set any initial state (or set it as empty object {}) in your child component. The first useEffect will handle the first update.
const Child = ({ onChange, initialData }) => {
const [data, setData] = useState({});
useEffect(() => {
setData(initialData);
}, [initialData]);
useEffect(() => {
onChange(data);
}, [data, onChange]);
return <div>Counter is: {data.counter}</div>;
};
as of the other comments, I agree, rather pass the state from parent to child.

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