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

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>;
};

Related

setting state with useState affects all useState hooks

I am trying to build an ecommerce website, and I hit a problem I cannot seem to resolve. I am very new to react and JS so have some patience please :)
I declared 4 useStates in my app.js:
const [elementeDinState, setElementeDinState] = useState([]);
const [currentCategorie, setCurrentCategorie] = useState("Acasa");
const [subCategorie, setSubcategorie] = useState([]);
const [cartContents, setCartContents] = useState([]);
const fetchData = useCallback(async () => {
const data = await getCategories();
setElementeDinState(data);
}, []);
useEffect(() => {
fetchData().catch(console.error);
}, [fetchData]);
const changeHeader = (dataFromMenuItem) => {
setCurrentCategorie(dataFromMenuItem);
};
const changeCopiiContent = (data1FromThere) => {
setSubcategorie(data1FromThere);
};
const changeCart = (dataFromCart) => {
setCartContents(dataFromCart);
};
I am passing the functions to change those states to different child components as props. my problem is, when I add items to cart it triggers a re render of my component (products listing component) that should not be affected by cartContents and that resets the state of said component to the initial value that changes the items being shown. does useState hook create a single global state comprised of all those states?
If these useState are defined in the app.js and then passed down, when a child will use them chasing the state will happen in the app.js so all the children of <App /> will be re-rendered.
I guess that your app.js looks similar:
function App() {
const [elementeDinState, setElementeDinState] = useState([]);
// ...and the other hooks and methods
return (
<cartContents setElementDinState={setElementeDinState} />
<ProductList />
)
}
In this case the state is in the component so when <CartContents /> changes it, it will trigger a re-render of the and all its children <ProductList /> included.
To avoid this problem think better when each piece of state needs to be and put the state as near as possibile to that component. For example, if the state of the cart does not influence the Product list. Move the useState in the <Cart /> component.
From what I understand, your problem is that you're simply resetting the cartContents state every time you call the changeCart function, correct?
What you probably want, is to add (or remove ?) the item to the cart, like this?
const changeCart = (dataFromCart) => {
setCartContents(oldContents => [...oldContents, dataFromCart]);
};
Here is a description of useState from the oficial site:
useState is a Hook (...). We call it inside a function component to add some local state to it
So it creates just a local state.
About your problem, We need more information, but I believe that some parent component of that widget is trying to render other component instead of your the component that you wanted (let's call it "ProblemComponent") and rendering you ProblemComponent from scratch again, before you can see it.
it's something like that:
function ParentComponent(props: any) {
const isLoading = useState(false);
// Some logic...
if(isLoading) {
return <LoadingComponent/>;
}
return <ProblemComponent/>;
}
If that doesn't work you can also try to use React.memo() to prevent the ProblemComponent to update when it props change.
well, seems like I wanted to change the way react works so I figured out a work around, based on what you guys told me. I declared the state of the productsComponent in the parent component and adding to cart now doesn't force a refresh of the items being shown. thank you!

Can you control the re-renders of a functional react component based on state change?

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...

Why React Component unmounts on each useEffect dependency change?

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 is my component that receives props not working when I destructure out the properties, but when I use props.key it's working?

The Problem
I have an application that uses this React Redux Boilerplate: https://github.com/flexdinesh/react-redux-boilerplate
I created a new page that is connected to the injected reducer + saga.
I receive following props: posts, loading, error, loadPosts and match
When I use these directly the app is working as expected. But as soon as I start to destructure the props, the app is behaving unexpectedly.
Especially with the match props.
When I do it like this:
const SubforumPage = (props) => {
useEffect(() => {
const { id: subId } = props.match.params;
console.log('props: ', subId);
}, []);
// .... other code
}
No problem everything works.
But when I do it like this:
const SubforumPage = ({match}) => {
useEffect(() => {
const { id: subId } = match.params;
console.log('props: ', subId);
}, []);
// .... other code
}
match suddenly gets undefined!
I have really no clue what so ever why this is happening. It's the first time that I see an error like this.
This specific page is set up like this in the routing file:
<Route path="/sub/:id" component={SubforumPage} />
And it's clearly working when using (props) in the function arguments but not with ({match})
Why is this? Can please someone help me out here.
What have I tried?
I continuesly started destructuring one prop after another. At first this approach works and it's still not undefined but when I get to some props, it's different which ones, it will stop working.
I think it has to do something with how I use my useEffect() hook?
I pass an empty array so it does just run when mounting. It seems like when I refresh the page, the posts are cleared out but the useEffect doesn't run anymore, so the new posts doesn't get fetched. Because hen also the console.log inside the useEffect hook is undefined doesn't even run. But for example the loading prop in console.log outside of useEffect is indeed not undefined
(But that still does not explain why it's working with (props) as argument).
Am I just using useEffect wrong?
Many thanks
Ok guys that was completely my fault. Guess I'm too tired :D. Here is what caused the problem:
I fetch my post in the useEffect hook. I also render a component where I pass in the posts. But the posts are not available because the component has to wait for the data to come in. So I completely forgot that I have to wait for the data.
Before:
return <PostsGroup posts={posts} />;
After: (correct)
return <PostsGroup posts={posts || []} />;
I had a check in place looking like this:
if (loading) return <CircularProgress />;
(before the other return). But it doesn't matter because loading is false when the component initially renders.
So I also set the initial value from loading to true (in my initialState of the reducer). So I have now two checks in place.
Sorry guys. So stupid.

React: loading and updating nested resources when URI changes without excess duplication?

Let's say I have a nested URI structure, something like the following:
http://example.com/collections/{id}
http://example.com/collections/{collectionId}/categories/{id}
http://example.com/collections/{collectionId}/categories/{categoryId}/book/{id}
I can use react-router to render the correct component on page load, and when the URI changes.
Let's take the first case:
http://example.com/collections/{id}
Let's assume we have a CollectionShow component.
When the component first loads, I can pull the collection ID out of the URI and load the correct collection:
componentDidMount () {
this.loadCollection(this.props.match.params.id);
}
(Assume that loadCollection loads a collection with an AJAX call and sets it into the component's state.)
However, when the URI changes (through, e.g., the user clicking on a <Link>, react-router doesn't entirely re-build the component, it simply updates its props, forcing it to rerender. So, in order to update the compomnent's state, we also need to update the state on update:
componentDidUpdate(prevProps) {
if (!this.state.collection || this.collectionDidChange(prevProps)) {
this.loadCollection(this.props.match.params.id);
}
}
collectionDidChange(prevProps) {
return String(prevProps.match.params.id) !== String(this.props.match.params.id)
}
So far so good. But what about the second URL?
http://example.com/collections/{collectionId}/categories/{id}
Let's assume we have a CategoryShow component.
Now we don't only have to consider the collectionId changing, but also the category ID. We have to reload the collection if that ID changes, and we also have to reload the category if that changes.
The problem compounds with a third-level nesting (a BookShow component). We end up with something like this:
componentDidUpdate(prevProps) {
if (!this.state.collection || this.collectionDidChange(prevProps)) {
this.loadCollection(this.props.match.params.collectionId);
}
if (!this.state.category || this.collectionDidChange(prevProps) || this.categoryDidChange(prevProps)) {
this.loadCollection(this.props.match.params.collectionId)
.then(() => this.loadCategory(this.props.match.params.categoryId);
}
if (!this.state.book || this.collectionDidChange(prevProps) || this.categoryDidChange(prevProps) || this.bookDidChange(prevProps)) {
this.loadCollection(this.props.match.params.collectionId)
.then(() => this.loadCategory(this.props.match.params.categoryId)
.then(() => this.loadBook(this.props.match.params.id);
}
}
Not only is this unwieldy, it also results in a fair amount of code duplication across the three components, CollectionShow, CategoryShow and BookShow.
Using redux won't help matters much, because we still have to update the global state when the URI changes.
Is there a clean, efficient, React-friendly way of handling updates of nested resources such as these?
You could create a CollectionPage component that handles all the AJAX calls and keeps data in state.
This could pass down the collection, category/categories and books to the components (CollectionShow, CategoryShow and BookShow).
In CollectionPage you could use componentDidUpdate and componentDidMount as you presented it.
Your <*>Show components will know nothing about props.match.params.* and will only get the data needed to render the wanted content.
CollectionPage can be use for all your routes or you could change the route to something like
/collections/:collectionId?/:categoryId?/:bookId?
making all params options. You can check for the available ids in CollectionPage.
Hope it helps!
If I understood your problem it is something architectural. The parent component is the one that should be doing this management and injecting the result through subcomponents. Split your component in small components and render each one accordingly.
The code you shared will be splint in 3 others
The mponentDidUpdate(prevProps) method will go to the parent component simply as a componentDidMount().
Then if the router changes the component will be recreated and the new values will be sent across the modules.
If you dont wanna split you code you should at least do the step 2.
//everytime you get to the router this will be triggered and depending of the parameters of your router, you get the values you need and set the state
componentDidMount() {
if (!this.state.collection) {
this.loadCollection(this.props.match.params.collectionId);
}
if (!this.state.category) {
this.loadCollection(this.props.match.params.collectionId)
.then(() => this.loadCategory(this.props.match.params.categoryId);
}
if (!this.state.book) {
this.loadCollection(this.props.match.params.collectionId)
.then(() => this.loadCategory(this.props.match.params.categoryId)
.then(() => this.loadBook(this.props.match.params.id);
}
}
render() {
return (
//you can add conditions to render as well
<CollectionComponent {...this.props} {...{
collection: this.collection
}} />
<CategoryComponent {...this.props} {...{
categ: this.categ
}} />
<BookComponent {...this.props} {...{
book: this.book
}} />
)
}

Categories