I try to understand how useEffect() work in React, and why is executed in the last not in the order of my code. This issue causes a problem in my bigger program. My question : There is a possibility to choice when this one is executed ?
log of the console, like you see, is not of the order of print :(
0 children null App.js:11:10
2 children null App.js:16:10
1 children
Array [ "truc" ]
https://codesandbox.io/s/busy-cookies-yvq16?file=/src/App.js
I make this snippet code to explain my problem
import "./styles.css";
import { useEffect } from "react";
let list = [];
function Buffer() {
let init = null;
list.push(init);
return <Comp>{list[0]}</Comp>;
}
function Comp({ children }) {
console.log("0 children", children);
useEffect(() => {
children = ["truc"];
console.log("1 children", children);
}, []);
console.log("2 children", children);
return <div>Super Buffer</div>;
}
export default function App() {
return (
<div className="App">
<Buffer />
</div>
);
}
why is executed in the last not in the order of my code.
Because that's its purpose, that's what it's for. From the documentation:
useEffect
useEffect(didUpdate);
Accepts a function that contains imperative, possibly effectful code.
Mutations, subscriptions, timers, logging, and other side effects are not allowed inside the main body of a function component (referred to as React’s render phase). Doing so will lead to confusing bugs and inconsistencies in the UI.
Instead, use useEffect. The function passed to useEffect will run after the render is committed to the screen. Think of effects as an escape hatch from React’s purely functional world into the imperative world.
This article by Dan Abramov may also be useful: A Complete Guide to useEffect. Despite the title, it doesn't just cover useEffect, but the whole way hooks and function components work.
There is a possibility to choice when this one is executed ?
It's not really clear from your question what you want to do instead, but if you want to do something right away, just do it right away without any wrapper function. But remember that your component function runs every time the component needs to re-render, so useEffect is what you use to run things only A) When the component first mounts (and optionally when it unmounts) or B) When certain things change.
children = ["truc"];
That's very suspect code, since children is a prop. You shouldn't be assigning to props (not even destructured copies of them). Props are state controlled by the parent component. The component should only use them, not try to directly modify them.
Just in case it helps, render takes place from children to parent components. In case you need to handle some execution in the order wanted, you can use the parent's useEffect like this:
import "./styles.css";
import { useEffect, useState } from "react";
let list = [];
function Buffer() {
useEffect(() => {
console.log("2 children", list[0]); //log moved here
}, []);
let init = null;
let children = list.push(init);
return <Comp>{list[0]}</Comp>;
}
function Comp({ children }) {
console.log("0 children", children);
useEffect(() => {
children = ["truc"];
console.log("1 children", children);
}, []);
return <div>Super Buffer</div>;
}
export default function App() {
return (
<div className="App">
<Buffer />
</div>
);
}
OutPut:
0 children null
1 children (1) ["truc"]
2 children null
To log the children in the parent you would need to define the children variable in the parent and wont need to pass that down the prop, for this sample case at least
your useEffect() has a dependency array which specifies when to rerender
it is the empty array in your code. When it is empty, it only renders once your compnent renders. If you want to make it render when something changes, you need this dependency inside of that array
useEffect(() => {
children = ["truc"];
console.log("1 children", children);
}, [children]);
https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect
Related
I have a parent Component with a state variable that gets changed by one of its child components upon interaction. The parent then also contains some more components based on the data in the state variable.
The problem is that the child component rerenders when the state of its parent changes because the reference to the setState function changes. But when I use useCallback (as suggested here), the state of my parent just does not update at all.
This is my current setup:
function ArtistGraphContainer() {
const [artistPopUps, setArtistPopUps] = useState([])
const addArtistPopUp = useCallback(
(artistGeniusId, xPos, yPos) => {
setArtistPopUps([{artistGeniusId, xPos, yPos}].concat(artistPopUps))
},
[],
)
return (
<div className='artist-graph-container'>
<ArtistGraph addArtistPopUp={addArtistPopUp} key={1}></ArtistGraph>
{artistPopUps.map((popUp) => {
<ArtistPopUp
artistGeniusId={popUp.artistGeniusId}
xPos={popUp.xPos}
yPos={popUp.yPos}
></ArtistPopUp>
})}
</div>
)
}
And the Child Component:
function ArtistGraph({addArtistPopUp}) {
// querying data
if(records) {
// wrangling data
const events = {
doubleClick: function(event) {
handleNodeClick(event)
}
}
return (
<div className='artist-graph'>
<Graph
graph={graph}
options={options}
events={events}
key={uniqueId()}
>
</Graph>
</div>
)
}
else{
return(<CircularProgress></CircularProgress>)
}
}
function areEqual(prevProps, nextProps) {
return true
}
export default React.memo(ArtistGraph, areEqual)
In any other case the rerendering of the Child component wouldn't be such a problem but sadly it causes the Graph to redraw.
So how do I manage to update the state of my parent Component without the Graph being redrawn?
Thanks in advance!
A few things, the child may be rerendering, but it's not for your stated reason. setState functions are guaranteed in their identity, they don't change just because of a rerender. That's why it's safe to exclude them from dependency arrays in useEffect, useMemo, and useCallback. If you want further evidence of this, you can check out this sandbox I set up: https://codesandbox.io/s/funny-carson-sip5x
In my example, you'll see that the parent components state is changed when you click the child's button, but that the console log that would fire if the child was rerendering is not logging.
Given the above, I'd back away from the usCallback approach you are using now. I'd say it's anti-pattern. As a word of warning though, your useCallback was missing a required dependency, artistPopUp.
From there it is hard to say what is causing your component to rerender because your examples are missing key information like where the graphs, options, or records values are coming from. One thing that could lead to unexpected rerenders is if you are causing full mounts and dismounts of the parent or child component at some point.
A last note, you definitely do not need to pass that second argument to React.memo.
I have a basic e-commerce app for practice. There's a functional component called "Shop" which has two states:
[products, setProducts] = useState([10ProductObjects])
and [cart, setCart] = useState([])
On the first render cycle, the 10 products are loaded and each Product component has an "add to cart" button that adds a product to the cart array.
When I click the button, the cart array gets populated and its length is displayed. However, doing so re-renders the 10 products again even though nothing has been changed on them.
Now as I see it since one of the states changes i.e. the cart array, the whole Shop component is rendered again. Which in turn renders its child components, including those which were not changed.
I've tried to use React.memo but it doesn't help since no props are being changed on "Shop", rather the state is changing. I've also used the useMemo hook and it had some interesting results.
Using the products array as a dependency solves the extra re-rendering problem, but the new products are not added to the cart anymore. Using both [products, cart] as the dependencies works but brings back the original problem.
I know it could be done using shouldComponentUpdate but I need that kind of flexibility in functional components.
N.B: This is my first ever question and I'm all ears for any kind of feedback.
import React, { useState, useMemo } from 'react';
import fakeData from '../../fakeData';
import Product from '../Product/Product';
const Shop = () => {
console.log('[Shop.js] rendered')
const first10 = fakeData.slice(0, 10);
const [products, setProducts] = useState(first10);
const [cart, setCart] = useState([]);
const addProductHandler = (product) => {
console.log(cart, product);
const newCart = [...cart, product];
console.log(newCart);
setCart(newCart);
}
let productsOnScreen = useMemo(() => {
return products.map( prod => {
return <Product product={prod} addProductHandler={addProductHandler} />
});
}, [products])
return (
<div className="shop-container">
<div className="product-container">
{productsOnScreen}
</div>
<div className="cart-container">
<h3>this is cart</h3>
<h5>Order Summary: {cart.length}</h5>
</div>
</div>
);
};
export default Shop;
Memoize addProductHandler with React.useCallback so that the reference to it does not change between renders:
const addProductHandler = React.useCallback((product) => {
setCart(oldCart => {
return [...oldCart, product];
});
}, [setCart]);
Then, memoize <Product> with React.memo. You didn't post your code for that component but it would look something like this:
export const Product = React.memo((props) => {
// normal functional component stuff
return <>product component or whatever</>;
});
These are both necessary for a component to avoid unnecessary rerenders. Why?
React.useCallback allows for comparison-by-reference to work for a callback function between renders. This does not work if you declare the callback function every render, as you have currently.
React.memo wraps a component to enable it to render or not render depending on a shallow comparison of its props. React components do NOT do this by default, you have to explicitly enable it with React.memo.
As the docs mention:
This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs.
In other words, you should only use this as a performance optimization if you are experiencing slowdowns, NOT to prevent renders for any other reason, and ONLY if you are actually experiencing performance issues.
If this doesn't work, it's likely that one of your <Product> props is still changing (in the context of shallow comparison-by-reference) between renders. Keep playing with and memoizing props until you figure out which ones are changing between renders. One way to test this is a simple React.useEffect (for debug purposes only) inside of <Product> which will alert you when a prop has changed:
React.useEffect(() => {
console.log('product prop changed');
}, [product]);
React.useEffect(() => {
console.log('addProductHandler prop changed');
}, [addProductHandler]);
// etc...
Suppose we have an input for buyer id and we want to fetch the buyer details each time the buyerId is changed.
The following code looks like this for class components
componentDidUpdate(prevProps,prevState) {
if (this.state.buyerId !== prevState.buyerId) {
this.props.fetchBuyerData(this.state.buyerId); // some random API to search for details of buyer
}
}
But if we want to use useEffect hook inside a functional component how would we control it. How can we compare the previous props with the new props.
If I write it as per my understanding it will be somewhat like this.
useEffect(() => {
props.fetchBuyerData(state.buyerId);
}, [state.buyerId]);
But then react's hooks linter suggests that I need to include props into the dependency array as well and if I include props in the dependency array, useEffect will be called as soon as props changes which is incorrect as per the requirement.
Can someone help me understand why props is required in dependency array if its only purpose is to make an API call.
Also is there any way by which I can control the previous state or props to do a deep comparison or maybe just control the function execution inside useEffect.
Deconstruct props either in the function declaration or inside the component. When fetchBuyerData is used inside the useEffect hook, then only it needs to be listed as a dependency instead of all of props:
// deconstruct in declaration
function MyComponent({ fetchBuyerData }) {
useEffect(() => {
// caveat: something is wrong with the use of `this.state` here
fetchBuyerData(this.state.buyerId);
}, [fetchBuyerData, state.buyerId]);
}
// deconstruct inside component
function MyComponent(props) {
const { fetchBuyerData } = props;
useEffect(() => {
// caveat: something is wrong with the use of `this.state` here
fetchBuyerData(this.state.buyerId);
}, [fetchBuyerData, state.buyerId]);
}
I'd assume you're rewriting your class component info functional one. Then you'd be better off including your fetch request right where you set new state.bayerId (I assume it's not an external prop). Something like:
const [buyerId, setBuyerId] = React.useState('')
const handleOnChange = (ev) => {
const { value } = ev.target
if (value !== buyerId) {
props.fetchBuyerData(value)
setBuyerId(value)
}
...
return (
<input onChange={handleOnChange} value={buyerId} />
...
The code snippet is somewhat suboptimal. For production I'd assume wrap change handler into useCallback for it to not be recreated on each render.
I am trying to learn React by building a web application. Since I want to learn it step by step, for now I don't use Redux, I use only the React state and I have an issue.
This is my components architecture:
App.js
|
_________|_________
| |
Main.js Side.js
| |
Game.js Moves.js
As you can see, I have the main file called App.js, in the left side we have the Main.js which is the central part of the application which contains Game.js where actually my game is happening. On the right side we have Side.js which is the sidebar where I want to display the moves each player does in the game. They will be displayed in Moves.js.
To be more clear think at the chess game. In the left part you actually play the game and in the right part your moves will be listed.
Now I will show you my code and explain what the problem is.
// App.js
const App = React.memo(props => {
let [moveList, setMovesList] = useState([]);
return (
<React.Fragment>
<div className="col-8">
<Main setMovesList={setMovesList} />
</div>
<div className="col-4">
<Side moveList={moveList} />
</div>
</React.Fragment>
);
});
// Main.js
const Main = React.memo(props => {
return (
<React.Fragment>
<Game setMovesList={props.setMovesList} />
</React.Fragment>
);
});
// Game.js
const Game= React.memo(props => {
useEffect(() => {
function executeMove(e) {
props.setMovesList(e.target);
}
document.getElementById('board').addEventListener('click', executeMove, false);
return () => {
document.getElementById('board').removeEventListener('click', executeMove, false);
};
})
return (
// render the game board
);
});
// Side.js
const Side= React.memo(props => {
return (
<React.Fragment>
<Moves moveList={props.moveList} />
</React.Fragment>
);
});
// Moves.js
const Moves= React.memo(props => {
let [listItems, setListItems] = useState([]);
useEffect(() => {
let items = [];
for (let i = 0; i < props.moveList.length; i++) {
items.push(<div key={i+1}><div>{i+1}</div><div>{props.moveList[i]}</div></div>)
}
setListItems(items);
return () => {
console.log('why this is being triggered on each move?')
};
}, [props.moveList]);
return (
<React.Fragment>
{listItems}
</React.Fragment>
);
});
As you can see on my code, I have defined the state in App.js. On the left side I pass the function which updates the state based on the moves the player does. On the right side I pass the state in order to update the view.
My problem is that on each click event inside Game.js the component Moves.js unmounts and that console.log is being triggered and I wasn't expected it to behave like that. I was expecting that it will unmount only when I change a view to another.
Any idea why this is happening ? Feel free to ask me anything if what I wrote does not make sense.
Thanks for explaining your question so well - it was really easy to understand.
Now, the thing is, your component isn't actually unmounting. You've passed props.movesList as a dependency for the usEffect. Now the first time your useEffect is triggered, it will set up the return statement. The next time the useEffect gets triggered due to a change in props.movesList, the return statement will get executed.
If you intend to execute something on unmount of a component - shift it to another useEffect with an empty dependency array.
answering your question
The answer to your question
"why this is being triggered on each move"
would be:
"because useEffect wants to update the component with the changed state"
But I would be inclined to say:
"you should not ask this question, you should not care"
understanding useEffect
You should understand useEffect as something that makes sure the state is up to date, not as a kind of lifecycle hook.
Imagine for a moment that useEffect gets called all the time, over and over again, just to make sure everything is up to date. This is not true, but this mental model might help to understand.
You don't care if and when useEffect gets called, you only care about if the state is correct.
The function returned from useEffect should clean up its own stuff (e.g. the eventlisteners), again, making sure everything is clean and up to date, but it is not a onUnmount handler.
understanding React hooks
You should get used to the idea that every functional component and every hook is called over and over again. React decides if it might not be necessary.
If you really have performance problems, you might use e.g. React.memo and useCallback, but even then, do not rely on that anything is not called anymore.
React might call your function anyway, if it thinks it is necessary. Use React.memo only as kind of a hint to react to do some optimization here.
more React tips
work on state
display the state
E.g. do not create a list of <div>, as you did, instead, create a list of e.g. objects, and render that list inside the view. You might even create an own MovesView component, only displaying the list. That might be a bit too much separation in your example, but you should get used to the idea, also I assume your real component will be much bigger at the end.
Don’t be afraid to split components into smaller components.
It seems the problem is occurred by Game element.
It triggers addEventListener on every render.
Why not use onClick event handler
/* remove this part
useEffect(() => {
function executeMove(e) {
props.setMovesList(e.target);
}
document.getElementById('board').addEventListener('click', executeMove, false);
})
*/
const executeMove = (e) => {
props.setMovesList(e.target);
}
return (
<div id="board" onClick={executeMove}>
...
</div>
)
If you want to use addEventListener, it should be added when the component mounted. Pass empty array([]) to useEffect as second parameter.
useEffect(() => {
function executeMove(e) {
props.setMovesList(e.target);
}
document.getElementById('board').addEventListener('click', executeMove, false);
}, [])
Why does my HomeTypes component unmount and remount every time I use setDrawerOpen?
Does a state change inside a component always cause that component to unmount and then re-mount?
import React, { useEffect, useState } from 'react';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import Drawer from '../../components/DrawerComponent/Drawer';
function HomeTypes() {
const [drawerOpen, setDrawerOpen] = useState(null);
useEffect(() => {
console.log('Home Types Mounted');
return () => {
console.log('Home Types Unmounted');
};
});
return (
<div className={`dashboard-page`}>
<h1 className="dashboard-page-title">
Home Types
<button
className={`btn bright primary`}
onClick={() => {
setDrawerOpen(true);
}}>
<FontAwesomeIcon icon={['fas', 'plus']} />
<span>add home type</span>
</button>
</h1>
<Drawer
drawerOpen={drawerOpen}
closeDrawer={() => {
setDrawerOpen(false);
}}
title="Add Home Type"
drawerContent="Hello World"
/>
</div>
);
}
export default HomeTypes;
You're missing a dependencies array in your useEffect, that might be causing the behavior
useEffect(() => {
console.log('Home Types Mounted');
return () => {
console.log('Home Types Unmounted');
};
}. []); // this array right here
documentation see the Note in orange below: Using the Effect Hook
The hook useEffect will run every time the component renders.
to run the effect once on mount (end cleanup on dismount) run it with a second parameter [] in this form:
useEffect(() => {
// Side Effect
return () => {
// Cleanup
}
}, [])
Edit: Better to just quote the docs
Note
If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous renders. Learn more about how to deal with functions and what to do when the array changes too often.
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.
If you pass an empty array ([]), the props and state inside the effect will always have their initial values. While passing [] as the second argument is closer to the familiar componentDidMount and componentWillUnmount mental model, there are usually better solutions to avoid re-running effects too often. Also, don’t forget that React defers running useEffect until after the browser has painted, so doing extra work is less of a problem.
We recommend using the exhaustive-deps rule as part of our eslint-plugin-react-hooks package. It warns when dependencies are specified incorrectly and suggests a fix.