React useState with an empty object causes an infinite loop - javascript

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.

Related

how to stop re-rendering of child component if parent update the context or state in react js?

how to stop re-rendering of child component if parent update the context or state in react js ?
I am already using React.memo still it is re-rendering.
this is my child component
const Ab = () => {
console.log("---ff-ddd");
const pageContext = useContext(PageContext);
useEffect(() => {
setTimeout(() => {
pageContext.updateGlobalMenu({});
}, 5000);
}, []);
return <div>ddd</div>;
};
export default React.memo(Ab);
I am updating the context. I want it update the context value but not re-render the child component
export default function IndexPage() {
const [globalMenu, setGlobalMenu] = useState("");
const updateGlobalMenu = (menuContent) => {
setGlobalMenu(menuContent);
};
return (
<PageContext.Provider
value={{
updateGlobalMenu
}}
>
<Ab />
</PageContext.Provider>
);
}
here is my code
https://codesandbox.io/s/friendly-bartik-3cnqvf?file=/pages/index.js:156-470
if you see it print two times console. it means it is re-rendering two times
If PageContext's value changes, then any component consuming that context (including Ab) will render. React.memo cannot stop this. In your case you're changing the value on every render, so you have room to improve it by memoizing the context value. That way, the value won't change unless it needs to:
export default function IndexPage() {
const [globalMenu, setGlobalMenu] = useState("");
const updateGlobalMenu = useCallback((menuContent) => {
setGlobalMenu(menuContent);
}, []);
const value = useMemo(() => ({
updateGlobalMenu
}), [updateGlobalMenu]);
return (
<PageContext.Provider value={value}>
<Ab />
</PageContext.Provider>
);
}
You can also, in addition to memoisation from the previous answer, split your "api" and "data" portion of the state into two different providers.
updateGlobalMenu will be in PageContextAPI provider, globalMenu will be in PageContextData provider. That way when you update the data, only the provider with the data will be re-rendered.
Take a look at this article, I covered this technique in detail here:https://www.developerway.com/posts/how-to-write-performant-react-apps-with-context

Cannot useRef() in functional component. null is not an object

I have a functional component which causes the following error:
null is not an object (evaluating 'textInput.current.blur'
I wonder why
import React, { useState, useRef } from "react";
import { TextInput } from "react-native";
const UselessTextInput = ({ hide }) => {
const [text, onChangeText] = React.useState("Useless Text");
const textInput = useRef(null);
const _renderInput = () => {
if (hide) textInput.current.blur();
<TextInput
ref={textInput}
onFocus={() => ...}
onChangeText={onChangeText}
value={text} />
...
return __renderInput();
}
Can anyone explain this behaviour?
If you set a ref to a DOM node using the ref attribute, the ref's current property will have that node only after it's set i.e. in useEffect callbacks or from next renders.
function Counter() {
const [count, setCount] = React.useState(0);
const divRef = React.useRef("initval");
console.log("During Render", divRef.current);
React.useEffect(() => {
console.log("Inside useEffect CB", divRef.current);
}, []);
return <button ref={divRef} onClick={() => setCount(count + 1)}>{count}</button>;
}
ReactDOM.render(<Counter />, document.getElementById("root"));
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
Understanding the flow:
When useRef is called React returns an object whose current property is set to the initial value provided ("initval" in our case).
So, the console log during first render prints the initial value.
Then the JSX is returned from the component, which updates the ref and sets it's current property to the DOM node.
Then the callback passed useEffect runs and since the ref has already been updated it prints the DOM node.
Then in next render (which can be triggered by clicking on the button), again we call useRef and React returns the same object and therefore this time the log during the render prints the DOM node.
use optional chaining to your textInput value
like this : textInput?.current?.blur();

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>

Why ref value is correct in useEffect but nullish in component body

This is probably very trivial but Im looking for a proper answer:
Why is someRef available in useEffect, but its null in Child body?
How is it possible?
Especially when the component do not re-render. It looks like useEffect is kind of an abstract layer over component? It sounds ridiculous
const { useState, useRef } = React;
const Child = ({ someRef }) => {
console.log('body', someRef);
React.useEffect(() => {
console.log('hiya', someRef);
}, [someRef]);
console.log('body', someRef);
return null;
}
const App = () => {
const someRef = useRef(null);
return (
<div>
<div ref={someRef}>text1</div>
<Child someRef={someRef} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
The ref doesn't get assigned to until the App fully renders for the first time and the HTMLElements are created. When components are first mounting in an app, any refs tied to elements will initially not be defined until the initial mount cycle is completed. That's why the useEffect in Child properly displays the ref as populated with the <div>.
You don't need to wait for the someRef to change in Child (and you shouldn't, since the ref object doesn't change anyway) - all you need to do is wait for the initial mount to complete. This would work too:
const Child = ({ someRef }) => {
React.useEffect(() => {
console.log('hiya', someRef);
}, []);
You can think of
<div ref={someRef}>
a bit like
<div onThisElementInsertedIntoDOM={function() { someRef.current = this; }}
Not exactly, but you get the idea. The element hasn't been inserted yet when you log them in the function body for the first time. The elements may be in the process of being created, but they haven't made it into the DOM yet.

How correctly pass a node from a ref to a context?

I'm trying to pass a node from a ref to a context. But because I have no re-render after the first render, the passed node is null. I thought about two variants (but I think they aren't the best):
To pass ref instead of ref.current. But then in use cases, I'll be forced to use something like contextRef.current instead of contextNode.
Use a state (something like firstRender) to re-render a component after getting a ref.current. This seems not optimal.
What is a correct (the best?) way to do it?
CodeSandbox
import React, { createContext, createRef, useContext, useEffect } from "react";
import ReactDOM from "react-dom";
const Context = createContext(null);
const App = ({ children }) => {
const ref = createRef();
return (
<div ref={ref}>
<Context.Provider value={ref.current}>{children}</Context.Provider>
</div>
);
};
const Child = () => {
const contextNode = useContext(Context);
useEffect(() => {
console.log(contextNode);
});
return <div />;
};
const rootElement = document.getElementById("root");
ReactDOM.render(
<App>
<Child />
</App>,
rootElement
);
Instead of passing the ref which doesn't trigger a render when changed, use a state that holds the ref. This way you can change the Context from a child if needed, and at the same time you get the value updated correctly.
const App = ({ children }) => {
const ref = useRef(null);
const [ref_state, setRefState] = useState(null);
useEffect(() => {
if (!ref.current) {
return;
}
setRefState(ref.current)
}, []);
return (
<div ref={ref_state}>
<Context.Provider value={ref.current}>{children}</Context.Provider>
</div>
);
};
If you need the initial render to point to the element, you could (in a non-optimal way) set the initial value to the HTML element:
const App = ({ children }) => {
const ref = useRef(document.querySelector("#app"));
return (
<div id="app" ref={ref}>
<Context.Provider value={ref.current}>{children}</Context.Provider>
</div>
);
};
I didn't know about that, but passing ref.current doesn't work in the first render, but if you only pass ref, it will work in the first render.
Where is the working codesandbox.
I don't think that this
then is use cases I'll be forced to use something like contextRef.current instead of contextNode.
Will be a issue, it will be good, because when using it, you will know that what you are getting is a ref.
Also,
Do this
Use a state (something like firstRender) to rerender a component after getting a ref.current. This seems not optimal.
Only for not using ref.current, doesn't look like a good practice.

Categories

Resources