Hook's useState initial value never changes in callback that is memoized - javascript

In this simple example, I am memoizing Child component using useMemo and passing callback back to parent function to add data to react hook array, which is initially set as empty.
My question is as follows: Why hook data never changes and keeps it's initial state in callback function that comes from Child component which is memoized. However, when checking data hook in useEffect method or using that hook in rendering - it always has the newest state.
It is more of a general question what happens behind the hood and what is the best way, for example, to check against duplicate values in callback functions from memoized child components if data always has initial state.
I am aware of useReducer and that I can manipulate data in hooks setter method before adding it, but maybe there are other methods?
Parent component:
import React, {useEffect, useState} from 'react';
import Child from "./Child";
function App() {
const [data, setData] = useState([]);
useEffect(()=>{
console.log("UseEffect", data)
},[data]);
return (
<div>
<Child callback={(val)=>{
console.log("Always returns initial state", data); // <----------Why?
setData(old=>{
console.log("Return previous state", old);
return [...old, val]
})
}} />
Length: {data.length /*Always gets updated*/}
</div>
);
}
export default App;
Child component: In real scenario it is a map, that I want to render only once, without causing re-renders.
import React, {useMemo} from "react"
export default function Child({callback}) {
return useMemo(()=>{
return <button onClick={()=>callback(1)}>
Press!
</button>
},[])
}

Is there a way to return new reference of callback, without adding dependencies in useMemo method
Like this
// returns *(always the same)* function that will forward the call to the latest passed `callback`
function useFunction(callback) {
const ref = React.useRef();
ref.current = callback;
return React.useCallback(function() {
const callback = ref.current;
if (typeof callback === "function") {
return callback.apply(this, arguments);
}
}, []);
}
and then:
export function Child({ callback }) {
const handler = useFunction(callback);
return useMemo(() => {
return <button onClick={() => handler(1)}>Press!</button>;
}, []);
}
or
function App() {
const [data, setData] = useState([]);
const callback = useFunction((val) => {
console.log("Always returns the latest state", data);
setData([...data, val]);
});
return (
<div>
<Child callback={callback} />
Length: {data.length /*Always gets updated*/}
</div>
);
}
function Child({ callback }) {
return useMemo(() => {
return <button onClick={() => callback(1)}>
Press!
</button>
}, [])
}

As you said, your Child component is memoized, which means it will only update if one of its dependency changes.
So at the first rendering, you create your Child component with the function you pass to the callback prop, and at this time, data is []. If you click on the button, data is updated correctly and so is the function passed to callback prop to Child, but since you did not set any dependency to useMemo, your Child component will not update and will still return its memoized version, with first callback prop it received, which indeed always log the initial value of data: [].
So all you need is to add callback to the list of dependencies of useMemo:
export function Child({ callback }) {
return useMemo(() => {
return <button onClick={() => callback(1)}>Press!</button>;
}, [callback]);
}
This way, when the callback prop changes, your Child component will also update its onClick event handler.
Also, I recommend using eslint-plugin-react npm package, which will instantly warn you about missing dependencies in React hooks, and more generally about bad practices in your code.

Related

re-render useState for each object in an array

When I click the MultipleComponent button, all logs in the function return null.
The second time I click it, it returns the previous values.
How can I get the current status in each log within the map function?
When I call the function in the useEffect hook, useEffect runs after the first render. I don't want it to run after the first render.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react-lite";
function App() {
const [component, setComponent] = useState([]);
useEffect(() => {});
const newArray = [1, 2, 3];
const Test = observer(() => {
return (
<div>
<p>Test</p>
</div>
);
});
const Test2 = observer(() => {
return (
<div>
<p>Test2</p>
</div>
);
});
const Test3 = observer(() => {
return (
<div>
<p>Test3</p>
</div>
);
});
function MultipleComponent() {
newArray.map(async (x) => {
if (x === 1) {
setComponent((ps) => [...ps, Test]);
console.log(component);
} else if (x === 2) {
setComponent((ps) => [...ps, Test2]);
console.log(component);
} else {
setComponent((ps) => [...ps, Test3]);
console.log(component);
}
});
}
return (
<div>
{component.map((Input, index) => (
<Input components={component} key={index} />
))}
<button onClick={() => setComponent([...component, Test])}>
Single Component
</button>
<button onClick={() => MultipleComponent()}>Multiple Component</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
codensadbox: https://codesandbox.io/s/react-hooks-useeffect-forked-shxvl6
When I click the MultipleComponent button, all logs in the function return null.
The second time I click it, it returns the previous values.
React state updates are asynchronous and do not update any values in the current scope. They trigger a render, and then your component executes again with that new state.
See this for more info: The useState set method is not reflecting a change immediately
How can I get the current status in each log within the map function?
You can't get the state that has been changed until the next render, but you don't need to because you have the value that you set. Just use that if you need to. For example:
setComponent((ps) => {
const newState = [...ps, Test];
console.log(newState);
return newState;
});
When I call the function in the useEffect hook, useEffect runs after the first render. I don't want it to run after the first render.
Your effect has no dependencies, which means that it will run after every render. Just pass an array of dependencies to the effect in order to only execute it when those change. If you pass an empty array, then it will only every execute once.
useEffect(() => console.log('I run only once'), []);
See the docs on useEffect for more: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
Hi as per the documentation "calling the set function does not change state in the running code". If you need to use the next state, you can save it in a variable before passing it to the set function.
Here is a link of your updated code.
https://codesandbox.io/s/react-hooks-useeffect-forked-m7ipwb?file=/src/index.js
When I call the function in the useEffect hook, useEffect runs after the first render. I don't want it to run after the first render.
By default, Effects run after every render. You can tell React to skip unnecessarily re-running the Effect by specifying an array of dependencies as the second argument to the useEffect call. Start by adding an empty [] array

Passing Async State to Next.js Component via Prop

I'm fetching WordPress posts asynchronously via getStaticProps()...
export async function getStaticProps({ params, preview = false, previewData }) {
const data = await getPostsByCategory(params.slug, preview, previewData)
return {
props: {
preview,
posts: data?.posts
},
}
}
... and passing them to useState:
const [filteredArticles, setFilteredArticles] = useState(posts?.edges)
Then, I pass the state to a component:
router.isFallback ? (
// If we're still fetching data...
<div>Loading…</div>
) : (
<ArticleGrid myArticles={filteredArticles} />
This is necessary because another component will setFilteredArticles with a filter function.
But when we are passing the state to ArticlesGrid, the data is not ready when the component loads. This is confusing to me since we passing the state within a router.isFallback condition.
Even if we set state within useEffect...
const [filteredArticles, setFilteredArticles] = useState()
useEffect(() => {
setFilteredArticles(posts)
}, [posts?.edges])
... the data arrives too late for the component.
I'm new to Next.js. I can probably hack my way through this, but I assume there's an elegant solution.
Let's look at some useEffect examples:
useEffect(() => {
console.log("Hello there");
});
This useEffect is executed after the first render and on each subsequent rerender.
useEffect(() => {
console.log("Hello there once");
}, []);
This useEffect is executed only once, after the first render.
useEffect(() => {
console.log("Hello there when id changes");
}, [props.id]);
This useEffect is executed after the first render, every time props.id changes.
Some possible solutions to your problem:
No articles case
You probably want to treat this case in your ArticleGrid component anyway, in order to prevent any potential errors.
In ArticleGrid.js:
const {myArticles} = props;
if(!myArticles) {
return (<div>Your custom no data component... </div>);
}
// normal logic
return ...
Alternatively, you could also treat this case in the parent component:
{
router.isFallback ? (
// If we're still fetching data...
<div>Loading…</div>
) : (
<>
{
filteredArticles
? <ArticleGrid myArticles={filteredArticles} />
: <div>Your custom no data component... </div>
}
</>
)
}
Use initial props
Send the initial props in case the filteres haven't been set:
const myArticles = filteredArticles || posts?.edges;
and then:
<ArticleGrid myArticles={myArticles} />

React.js infinite re-render

i got a issue with my code. My function "getNames" rerender may times, but i want it to render once? have you got any clue ?
import grounds from './../../UballersGroundsData.json';
export default function Groundlist() {
function getNames(jsonObj){
for(let item in jsonObj){
console.log("item = " + item);
for(let property in jsonObj[item] ){
console.log(jsonObj[item]);
// if (property === "groundName"){
// console.log(jsonObj[item][property]);
// }
}
}
}
return(
<div>
<h1>Hello world!</h1>
<ul>
{getNames(grounds)}
</ul>
</div>
)
}
Thank you !
You should put your function inside of a useEffect hook, then set it to a state hook with useState. Then, map out the list items for your list (assuming you are returning an array from your function). If you want it to only run getNames on the first render, you would set it up the useEffect hook with an empty dependency array. Code should look something like this:
import React, { useEffect, useState } from 'react'
import grounds from './../../UballersGroundsData.json';
export default function Groundlist() {
const [names, setNames] = useState([]) // Initial state with empty array
useEffect(() => {
function getNames(jsonObj){
// your function logic here...
}
const result = getNames(grounds) // Call your function
setNames(result) // set it to names state hook
}, []) // Empty array here means it will only use the useEffect on the first render.
return(
<div>
<h1>Hello world!</h1>
<ul>
{Array.from(names).map(name => <li>{name}</li>)}
</ul>
</div>
)
}
You can use useMemo react hook to memoize the returned value i.e. skip unnecessary / heavy calculations due to change in other state, props or context variables.
Example:
import { useMemo } from "react"
export default function Groundlist(props) {
const grounds = props.data // if grounds is passed as props from Parent component
const groundsMemo = useMemo(() => {
// do all the heavy calculations here
// (e.g. do the work of getNames function)
// and return some JSX or Array (data)
// returned value will be memoized;
// means it will be re-calculated only if "grounds" changes
// Hence, no unnecessary calls to getNames (heavy calculations)
}, [grounds])
return (
<div>
{/* Use this if groundsMemo is JSX */}
<ul>{groundsMemo}</ul>
{/* Use this if groundsMemo is an Array (data) */}
<ul>{groundsMemo.map(item => <li key={some_key}>
{item.property}
</li>)}</ul>
</div>
)
}
Try using useMemo and useCallBack you want to optimize your react app.
React Official docs clearly described how to use it: useMemo
useCallBack
You should never call a function inside return scope of render.
It's normal for a component to re-render without proper treatment.
Taking in mind the other 2 answers
You can use the full power of useEffect, useCallback and React.memo to prevent anything from re-render.
import React from 'react';
import grounds from './../../UballersGroundsData.json';
function Groundlist() {
// initiate state
const [names, setNames] = React.useState([]);
// This will prevent the Function from recalculate - useCallback
const getNames = React.useCallback(function (jsonObj) {
for(let item in jsonObj){
console.log("item = " + item);
for(let property in jsonObj[item] ){
console.log(jsonObj[item]);
// if (property === "groundName"){
// console.log(jsonObj[item][property]);
// }
}
}
}, []);
// Will make function run only once and nevermore - useEffect
React.useEffect(() => {
setNames(getNames());
}, [])
return(
<div>
<h1>Hello world!</h1>
<ul>
{names.map(a => <li>{a}</li>)}
</ul>
</div>
)
}
// Will prevent React from try to re-render without changing in props, so as your component has no props, will never re-render without yourself unmounting first
export default React.memo(Groundlist);
In another cases you can control exact when the component should recalculate your names using the last argument of functions
useCallback(() => {}, []) //<---
For example
useCallback(() => {}, [updateState]);
when updateState change the function will be recreated.

Is it a correct approach to pass useState setter to a child for updating parents state?

We can pass a callback to a child as props and update the state but I was just wondering can I pass useState setter to the child and call it directly in child to update parents state?
Something like:
function Parent(){
const [data, setData] = useState();
return(
<Child setData={setData}/>
);
}
function Child({setData}){
useEffect(()=>{
setData('new data'); //Calling setter here to update
}, [])
}
Yes, you can do that. In fact, it is good practice to do that. To avoid unnecessary re-renders and infinite loops, either include setData in the dependency array of the Child's useEffect or wrap the data in a useCallback in the Parent component. Also, it is recommended to initialise the data to some initial value when using useState. In your case, I would initialise it to null -> const [data, setData] = useState(null)
Example with the dependency array:
function Child({ setData }) {
useEffect(() => {
setData("new data"); //Calling setter here to update
}, [setData]);
return (...);
}
If you want to pass it to a child that is multiple levels deep, I would suggest using Context. With context, you can then use the useState in any child and you don't need to pass it as props throughout all of the children between the Parent and the Child that you want to use it in.
Example with the Context:
// Step 1. Create context
const MyContext = React.createContext();
// Step 2. Create a custom hook that will return Context
// This will allow you to use the context in any number of children more easily.
// And it will also make sure that it is only used within Parent component
const useData = () => {
const context = React.useContext(MyContext);
if (!context) {
throw new Error("useData must be used within a <Parent />");
}
return context;
};
function Parent() {
const [data, setData] = useState(null);
const value = [data, setData];
// Step 3. Use Context Provider and pass the value of the props that
// need to be used by any children down the tree
return (
<MyContext.Provider value={value}>
<Child />
</MyContext.Provider>
);
}
function Child() {
return <ChildTwo />;
}
function ChildTwo() {
// Step 4. Extract the prop from the Context that you need using custom hook
const [data, setData] = useData();
useEffect(() => {
setData("new data"); //Calling setter here to update
}, [setData]);
return (...);
}
Yes, that is perfectly ok. You can also utilize useContext() (global variables/functions) and even consider using an HOC in the future if you'll be working with many hooks. Then you can centralize all of the hooks to one component and re-use them throughout your application.
For an HOC some common use cases from my past projects are:
Errors
Validation
Authentication

I have read the React Docs on Hooks and I'm confused. When is the useEffect Hook clean up function called?

The react docs explanation on when the useEffect clean up function is called is confusing and generic to be honest.
They even confuse you more by comparing the class mental model with hooks. Class based components work differently from function based components with hooks.
React remembers the effect function you provided to useEffect, and runs it after flushing changes to the DOM which is understandable.
Now how and when is the function which is returned ("clean up function") called?
Code example below:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
The function returned from the function given as first argument to useEffect will be called when any element in the array given as second argument changes, or on every render if no second argument is given, or when the component is unmounted.
Example
const { useEffect, useState } = React;
function MyComponent({ prop }) {
useEffect(() => {
console.log('Effect!');
return () => console.log('Cleanup!')
}, [prop])
return (
<div>{prop}</div>
);
}
function App() {
const [value, setValue] = useState(0);
useEffect(() => {
setInterval(() => {
setValue(value => value + 1);
}, 1000)
}, [])
return value < 3 ? <MyComponent prop={value} /> : null;
}
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>
Without using the second parameter it will get called on every render.
This is often overkill, so it often a good idea to use the second parameter even if it's empty []..
eg.
useEffect(() => {....}, []);
Doing the above will only then call the cleanup when the component is physically detached from the DOM.
You can also pass props instead of [], this is handy if say a prop change, like what chat room you was in, it would then cleanup the current chat room, and initialize the new chat room etc.
So in your example passing [props.friend.id] would make sense, because if the id changed, it would make sense to call the cleanup, and then run the effect again for the new id.

Categories

Resources