Why React Component unmounts on each useEffect dependency change? - javascript

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);
}, [])

Related

How do I stop the re-rendering of my react component? It rerenders for every object in a drop down

After building the homepage of my website I finally figured out how to dynamically navigate to other pages. I wanted the browser to render the State homepage when a user clicked on a dropdown and selected a state. The navigation works, but it re-renders the component 50 times which I do not understand. I suspect it is due to the map function that is creating the menuitems. I could build out 50 individual menuitems but that is really ugly.
I am just starting out learning React. I have 7 YRS experience in backend development, but I am still trying to get a handle on React development. I have created a wepage with Material UI that has a dropdown that looks like this
<FormControl>
<InputLabel>Select a State</InputLabel>
<Select value={location} onChange={selectionChangeHandler}>
{locations.map((value) => (
<MenuItem value={value.toLowerCase()} key={value.toLowerCase()} component={Link} to={`${value.toLowerCase()}/home`} >
{value}
</MenuItem>
))}
</Select>
</FormControl>
This returns a dropdown with the 50 states in it. When the user clicks on a state I want the program to route to that page on click. This dynamic routing works BUT. It re-renders my component 50 times. I think this is happening because the dropdown is being built inside of a .map functions and there are 50 entries in that list.
I can remove the map function and hardcode in 50 menuitems but that is ugly.
Here is my onChange Function
const selectionChangeHandler = (event) => {
console.log(event.target.value)
}
I have also tried removing the component={Link} and using the useNavigate hook in the selectionChangeHandler like so
const selectionChangeHandler = (event) => {
console.log(event.target.value)
setlocation(event.target.value)
link = `${event.target.value}/home`
navigate(link)
}
This works but also renders 50 times. I am at a loss.
I cross posted the above to reddit and then I researched a little more. It turns out in React. When a parent component's state is updated it re-renders all child components. This may be what is going on here, but I do not know how to fix it. Even if I pass the state as a prop to the child component I still have to link to the correct page.
I am kind of nervous about posting I really tried to put work into solving my own problem before reaching out for help, and I might be reaching out for help a lot as I learn. I am committed to learning, but some problems I just cannot figure out on my own.
Link to Code Link to Code
The problem here is inside StateHome.js. You use a naked axios.get call directly in your component so it's going to re-render anytime the component changes from state change.
When you get the results you set state inside the same component which re-renders the component, which then re-runs the axios.get call, causing the loop.
I understand you want to call that endpoint when someone lands on the /alabama/home page. To tell React "Do this one time when the component loads" you must use a useEffect with an empty dependency array. So instead of this:
const StateHome = () => {
const { location } = useParams();
const [PageData, SetPageData] = useState();
axios.get(`http://localhost:4000/${location}/home`).then((response) => {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
SetPageData(response.data);
});
return <h1>This is my State Home Page Component for State {location}</h1>;
};
You need to use this:
const StateHome = () => {
console.log("StateHome");
const { location } = useParams();
const [PageData, SetPageData] = useState();
useEffect(() => {
axios.get(`http://localhost:4000/${location}/home`).then((response) => {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
SetPageData(response.data);
});
}, []); // <--- indicates "on mount", loads once regadless of other side-effects (setState)
return <h1>This is my State Home Page Component for State {location}</h1>;
};

How do I re-render a list of components in React JS when the props change?

I hope you all are doing well. I have this problem where components inside a list are not re-rendering when their props change.
setTestFs([...testFs, <TestFunction t={test}/>])
This is the line used to assign a component to the list. Each component uses test as its 't' property.
function RenderTestFunctions({ts}) {
return (
ts.map((e, index) => {
return (
<div key={index}>
{e}
</div>
)
})
)
}
function TestFunction({t}) {
console.log('just rendered + ' + t)
return (
<h1>{t}</h1>
)
}
The RenderTestFunctions renders all of the TestFunction components.
For some reason whenever I update the test value, none of the components re-render, even though the RenderTestFunctions does render. Does anyone know how I can re-render all components in a list when a prop changes?
please ask if you need any clarification. thank you for taking the time to look at this question!
you can use useEffect hook whose main purpose is to solve and perform an action when he notices any change in that. You can useEffect like-
useEffect(()=>{
if(t!== null || undefined||""){
TestFunction({t})
}
},[testFs,test])
In use Effect, an array helps to trigger that particular function. while inside the arrow function you can create your own function. If you still facing just lemme know, and I will help you more.
Thanks

How to specify the rendering order in React

I have a .tsx file that renders two component:
export default observer(function MyModule(props: MyModuleProps) {
....
return (
<div>
<TopPart></TopPart>
<LowerPart></LowerPart>
</div>
);
});
The problem I have is that the TopPart contains lots of sub components, so it takes much longer time to render, and the LowerPart is more important and I want to render it first, but in this code, the LowerPart won't be available until the TopPart has been rendered.
What I want to do is to first render the LowerPart, then the TopPart, without changing the layout. I am wondering how I can achieve this goal properly.
Disclaimer: this is a hack.
If the problem is server side, this is easy for react. Just throw up a placeholder while data is loading, then save it in state when loading finishes and render.
The following answer assumes this is a rendering performance problem. If it is, then you look at that rendering performance. Paginate your lists, simplify your CSS rules, profile react and see what is taking the time.
What follows may be interesting, but is probably a bad idea. React is declarative, meaning you tell the result you want and then let it crunch things to deliver that. As soon as you start telling it what order to do things in, you break that paradigm and things may get painful for you.
If you want to break up rendering you could use state to prevent the expensive component from rendering, and then use an effect to update that state after the first render, which then renders both components.
You could make a custom hook like this:
function useDeferredRender(): boolean {
const [doRender, setDoRender] = useState(false);
useEffect(() => {
if (!doRender) {
setTimeout(() => setDoRender(true), 100);
}
}, [doRender]);
return doRender;
}
This hook create the doRender state, initialized to false. Then it has an effect which sets the state to true after a brief timeout. This means that doRender will be false on the first render, and then the hook will immediately set doRender to true, which triggers a new render.
The timeout period is tricky. Too small and React may decide to batch the render, too much and you waste time. (Did I mention this was a hack?)
You would this like so:
function App() {
const renderTop = useDeferredRender();
return (
<div className="App">
{renderTop ? <TopPart /> : "..."}
<LowerPart />
</div>
);
}
Working example
One last time: this is probably a bad idea.

How to access current state of component from useEffect without making it a dependency?

I've got the following component (simplified) which, given a note ID, would load and display it. It would load the note in useEffect and, when a different note is loaded or when the component gets unmounted, it saves the note.
const NoteViewer = (props) => {
const [note, setNote] = useState({ title: '', hasChanged: false });
useEffect(() => {
const note = loadNote(props.noteId);
setNote(note);
return () => {
if (note.hasChanged) saveNote(note); // bug!!
}
}, [props.noteId]);
const onNoteChange = (event) => {
setNote({ ...note, title: event.target.value, hasChanged: true });
}
return (
<input value={note.title} onChange={onNoteChange}/>
);
}
The issue is that within the useEffect I use note, which is not part of the dependencies so it means I always get stale data.
However, if I put the note in the dependencies then the loading and saving code will be executed whenever the note is modified, which is not what I need.
So I'm wondering how can I access the current note, without making it a dependency? I've tried to replace the note with a ref, but it means the component no longer updates when the note is changed, and I'd rather not use references.
Any idea what would be the best way to achieve this? Maybe some special React Hooks pattern?
You can't get the current state because this component does not render on the app render that removes it. Which means your effect never runs that last time.
Using an effect cleanup function is not a good place for this sort of thing. That should really be reserved for cleaning up that effect and nothing else.
Instead, whatever logic you have in the app that changes the state to close the NoteViewer should also save the note. So in some parent component (perhaps a NoteList or something) you'd save and close like:
function NoteList() {
const [viewingNoteId, setViewingNoteId] = useState(null)
// other stuff...
function closeNote() {
if (note.hasChanged) saveNote(note)
setViewingNoteId(null)
}
return <>{/* ... */}</>
}

Where is it better to update the values of the hooks if the value received from the server?

I have useEffect, which gets two values from server 1, price of item 2, name of item. when I use the setPriceSquanchy functions (obj.price) setNameSquanchy (obj.name). my code is updated twice there is no way to make it so that it is updated only once.
import React, { useState,useEffect} from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [priceSquanchy, setPriceSquanchy] = useState("");
const [nameSquanchy, setNameSquanchy] = useState("");
useEffect(() => {
(async () => {
const res = await fetch(
`https://foo0022.firebaseio.com/vz.json`
);
const obj = await res.json();
setPriceSquanchy(obj.price)
setNameSquanchy(obj.name)
})();
}, []);
console.log("Hello")
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
TL;DR Your sample. Here is the fixed version (another fix with ReactDOM.unstable_batchUpdates here)
So your component is rendered twice after the promise is resolved right? Like this logs "App invoked" twice after logging "setting states". This is because first setA is called, then setB is called both causing a render.
In my opinion this is fine because anyway React will only apply the necessary patches to the DOM. It won't be a huge performance difference even if you fix it.
But if you want to fix it you can have a state containing both price and name something like { price: "", name: "" } in that way you'll only call setPriceName({ price: newPrice, name: newName }). Demo. As you see in this demo "App invoked" is only logged once after "setting states" is logged.
If you don't want to do that you can also use ReactDOM.unstable_batchedUpdates like this. As you can see this also works but it's "unstable". More on the API by Dan here and in this thread too
Also, at times React could also batch updates from setA and setB thus causing only one render. Like here. Here it got batched together because it didn't had a timeout and it was immediately after first render.
Focusing more on the question "Where is it better to update the values of the hooks if the value received from the server?"...
What you are doing is pretty correct. Another way would be making a container component for fetching the data then having a presentational component to actually render it. I don't really like this approach (nor does Dan suggests it now xD) it's really an overkill. You can read more on that here.
PS: Also in case you are wondering why there are still two renders in your sample's fixed version, well the first one is the initial render. So there's basically only one update after the promise is resolved.
useEffect is used for side effects.
From React Docs
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.
Now, what you're doing is in fact a side effect ( AJAX call) and here's a working demo to achieve the same via useEffect.
Notice the use of second argument to useEffect. This is dependency array and useEffect will only fire accordingly. By supplying an empty array, I make sure it fires runs only once. Failing to do this will continuously call this function in our component. You can also control it's firing based on some other state variables. More on reading here.
useEffects is a good place to do this. useEffects hook is intended for side effects, e.g. fetch request.
If a request is supposed to be done once on component mount and not on every render, useEffects should be used with empty inputs.
The function mixes async..await and promises in unnecessarily complex way.
It should be:
useEffect(() => {
(async () => {
const res = await fetch(
`......`
);
const obj = await res.json();
setPriceSquanchy(obj.price)
setNameSquanchy(obj.name)
})();
// return fetch cancellation function
}, []);

Categories