Component not re rendering when value from useContext is updated - reactjs

I'm using React's context api to store an array of items. There is a component that has access to this array via useContext() and displays the length of the array. There is another component with access to the function to update this array via useContext as well. When an item is added to the array, the component does not re-render to reflect the new length of the array. When I navigate to another page in the app, the component re-renders and reflects the current length of the array. I need the component to re-render whenever the array in context changes.
I have tried using Context.Consumer instead of useContext but it still wouldn't re-render when the array was changed.
//orderContext.js//
import React, { createContext, useState } from "react"
const OrderContext = createContext({
addToOrder: () => {},
products: [],
})
const OrderProvider = ({ children }) => {
const [products, setProducts] = useState([])
const addToOrder = (idToAdd, quantityToAdd = 1) => {
let newProducts = products
newProducts[newProducts.length] = {
id: idToAdd,
quantity: quantityToAdd,
}
setProducts(newProducts)
}
return (
<OrderContext.Provider
value={{
addToOrder,
products,
}}
>
{children}
</OrderContext.Provider>
)
}
export default OrderContext
export { OrderProvider }
//addToCartButton.js//
import React, { useContext } from "react"
import OrderContext from "../../../context/orderContext"
export default ({ price, productId }) => {
const { addToOrder } = useContext(OrderContext)
return (
<button onClick={() => addToOrder(productId, 1)}>
<span>${price}</span>
</button>
)
}
//cart.js//
import React, { useContext, useState, useEffect } from "react"
import OrderContext from "../../context/orderContext"
export default () => {
const { products } = useContext(OrderContext)
return <span>{products.length}</span>
}
//gatsby-browser.js//
import React from "react"
import { OrderProvider } from "./src/context/orderContext"
export const wrapRootElement = ({ element }) => (
<OrderProvider>{element}</OrderProvider>
)
I would expect that the cart component would display the new length of the array when the array is updated, but instead it remains the same until the component is re-rendered when I navigate to another page. I need it to re-render every time the array in context is updated.

The issue is likely that you're mutating the array (rather than setting a new array) so React sees the array as the same using shallow equality.
Changing your addOrder method to assign a new array should fix this issue:
const addToOrder = (idToAdd, quantityToAdd = 1) =>
setProducts([
...products,
{
id: idToAdd,
quantity: quantityToAdd
}
]);

As #skovy said, there are more elegant solutions based on his answer if you want to change the original array.
setProducts(prevState => {
prevState[0].id = newId
return [...prevState]
})

#skovy's description helped me understand why my component was not re-rendering.
In my case I had a provider that held a large dictionary and everytime I updated that dictionary no re-renders would happen.
ex:
const [var, setVar] = useState({some large dictionary});
...mutate same dictionary
setVar(var) //this would not cause re-render
setVar({...var}) // this caused a re-render because it is a new object
I would be weary about doing this on large applications because the re-renders will cause major performance issues. in my case it is a small two page applet so some wasteful re-renders are ok for me.

Make sure to do the object or array to change every property.
In my examples, I updated the context from another file, and I got that context from another file also.
See my 'saveUserInfo' method, that is heart of the whole logic.

Related

Adding recommended dependency by linter results in an infinite re-rendering loop

Here's my code:
SectionState.js:
import { React, useState, useEffect } from "react";
import QuestionContext from "./QuestionContext";
import questions from "../data/questions.json";
import { useNavigate } from "react-router-dom";
const SectionState = (props) => {
// set questions from json to an array of 4 elements
const newQuestions = Object.keys(questions.content).map(
(key) => questions.content[key].question
);
//useState for Question state
const [currentQuestion, setCurrentQuestion] = useState(0);
const newQuestionsArr = {
qID: 0,
questionTxt: newQuestions[currentQuestion],
}
const [questionCtx, setQuestionCtx] = useState(newQuestionsArr);
const navigate = useNavigate()
useEffect(() => {
setQuestionCtx(prevState => ({
...prevState,
qID: currentQuestion,
questionTxt: newQuestions[currentQuestion],
}));
}, [currentQuestion]);
const updateNextQuestion = () => {
if (!(currentQuestion >= newQuestions.length)) {
setCurrentQuestion((nextCurrentQuestion) => nextCurrentQuestion + 1);
}
else{
navigate('/result')
}
};
const updatePrevQuestion = () => {
if (currentQuestion <= 0) {
console.log(`No more questions`);
} else {
setCurrentQuestion((prevCurrentQuestion) => prevCurrentQuestion - 1);
}
};
return (
<QuestionContext.Provider
value={{ questionCtx, updateNextQuestion, updatePrevQuestion }}>
{props.children}
</QuestionContext.Provider>
);
};
export default SectionState;
Linter throws the following warning
React Hook useEffect has a missing dependency: 'newQuestions'. Either include it or remove the dependency array
If I add newQuestions in the dependency array, it results in re-rendering loop. I can't declare either newQuestions or questionCtx state inside useEffect as it is used elsewhere in the code.
I can see that I have to update the questionTxt. What should I do here to update the said value and remove the warning?
A new newQuestions object is created at every render. usEffect is triggered when one of the dependencies changes. Hence the infinite render loop.
If the newQuestions is a constant that depends on a json you import from a file, you could move it outside of the component as mentioned in #CertainPerformance answer. codesandbox
If for some reasons you want to declare the newQuestions variable inside of your component, you could use useMemo hook. codesandbox
You could disable the lint rule which is probably not a good idea.
I'm not really sure what you trying to achieve, but it seems like you probably don't need the useEffect and might have some redundant states.
Maybe you could use only one state, and get rid of the useEffect. You only need one state to keep track of the current question, and calculate other variables in each render.
const [currentQuestion, setCurrentQuestion] = React.useState(0);
const questionCtx = React.useMemo(
() => ({
qId: currentQuestion,
questionTxt: newQuestions[currentQuestion]
}),
[currentQuestion]
);
codesandbox
You could read more about managing state in the react beta documentation.
The problem is that the newQuestions array is computed anew each time the function runs, and so won't be === to the old array (and so will run every render). If newQuestions depended on other React values, the usual fix would be to memoize it with useMemo, but because it looks to depend only on a static imported value, you may as well just declare it outside the component (which means it doesn't need to be a dependency anymore either).
It also looks like you don't care about the keys, only the values - so, easier to use Object.values than Object.keys.
import { React, useState, useEffect } from "react";
import QuestionContext from "./QuestionContext";
import questions from "../data/questions.json";
import { useNavigate } from "react-router-dom";
const newQuestions = Object.values(questions.content).map(
val => val.question
);

Pull data from firestore using useEffect works on re-render only

Here is my code:
import React, { useEffect, useState } from 'react';
import { getDocs, collection } from 'firebase/firestore';
import { db } from '../firebase-config';
const Home = () => {
const [postList, setPostList] = useState([]);
const postsCollectionRef = collection(db, "data");
useEffect(() => {
const getPosts = async () => {
const data = await getDocs(postsCollectionRef);
let postListArray = []
data.forEach((doc) => {
const post = { ...doc.data() }
postListArray.push(post)
});
setPostList(postListArray)
};
getPosts();
console.log(postList);
}, []);
return (
<div>test</div>
);
};
export default Home;
On loading, the console.log returned an empty array. The spooky thing is when i changed anything , for example
return (
<div>test_epic</div>
);
The console.log shows that it is an array. Anyone has any idea as to why? Please refer to the screepcap as attached.
the first render on loading
I changed anything and components rerendered
Setting state in react is asynchronous, so the data is loaded and the state is set but the console.log statement is executed before the setting state async operation is complete
To make it a bit more clear this is how it works step by step
Component is rendered and postList is initialized with a value of []
useEffect is triggered
Data is fetched
A call to set a new value of postList is placed using setPostList (key here is a call is placed not that the data is actually updated)
You print console.log with a value from Step 1
The call from Step 4 is complete and now the data is actually updated
Here is an article that explains it with examples
And here is another answer that explains this deeply

UseEffect doesn't behave as expected -

When I refresh the page useEffect only render one function out of the two inside, and when I change routes it works normally.
I am using redux to set global state with the vehicles and dispatching the API in useEffect
so I have vehicles available all the time. however, the second function vchStatusNumbers that it should return the length of the array filtered as per its status , it only runs once, and when i add its state as dependency i get an infinite loop!
I need to understand how i should approach it?
Below is the component
import React,{useEffect,useState} from "react";
import StatisticBanner from "./StatisticBanner";
import {getAllVehicles,fetchVehiclesReport } from "./vehiclesReducer";
import { useSelector, useDispatch } from "react-redux";
const Home= ()=> {
const {vehicles} = useSelector(getAllVehicles); // get the state
const [statusTotal, setStatusTotal] = useState({})
const dispatch = useDispatch(); // dispatch fn to reducers
useEffect(() => {
dispatch(fetchVehiclesReport());
vchStatusNumbers();
}, [dispatch]);
const vchStatusNumbers = () =>{
const status = {}
let availableLength = 0
let parkedLength = 0
let serviceLength = 0
vehicles.map(vch=>{
if(vch.status === 'available'){
++availableLength
status.available = availableLength
}
if(vch.status === 'parked'){
++parkedLength
status.parked = parkedLength
}
if(vch.status === 'service'){
++serviceLength
status.service = serviceLength
}
})
setStatusTotal (status)
}
return (
<>
<div style={{ margin: 20 }}>
<StatisticBanner key={"statics"} statusTotal={statusTotal} />
</div>
</>
);
}
export default Home
Yes, if you add a dependency to an useEffect hook that ultimately updates that dependency value then this will cause render looping.
Seems vchStatusNumbers should be in a separate useEffect hook with a dependency on the vehicles redux state value.
useEffect(() => {
vchStatusNumbers();
}, [vehicles]);
This is because it is derived state from the vehicles data and won't be updated yet in the first effect that dispatches the action to update it.

Force component to wait for redux state update

I know that question was asked a number of times, but none of answers work for me. I got the component, which job is to display user data from react-redux BUT userData can be stored in localStorage and on first load of website this localStorage is fetched to redux state, so I get the error where I want to display initials of currently logged in user. After adding async/await to mapStateToProps function localStorage gets fetched, but I still got error saying 'Cannot get 0 of undefined', using useEffect with pre set initials also gives an error. Here is my current code:
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { ReactComponent as Person } from 'assets/person.svg'
import { ReactComponent as Add } from 'assets/add.svg'
import { ReactComponent as Settings } from 'assets/settings.svg'
import { Styled, Container, Button } from './styles'
const UserInfo = ({ initial }) => {
let initials = 'UN'
useEffect(() => {
initials = initial
}, [initial])
return (
<Styled>
{initials}
<Container>
<Button>
<Add />
</Button>
<Button>
<Person />
</Button>
<Button>
<Settings />
</Button>
</Container>
</Styled>
)
}
const mapStateToProps = async ({ user }) => {
const initial = await `${user.name[0]}${user.surname[0]}`
return initial
}
export default connect(mapStateToProps)(UserInfo)
UserInfo.propTypes = {
initial: PropTypes.string.isRequired,
}
I also tried returning initial: initial || 'UNDEF' but I still receive an error. Additionally, when I don't refresh the browser on VSC save, I don't get an error. How can I force my component to wait for props from react-redux
I did it a little around, but now it works, i used useState and useEffect hooks and changed a bit mapStateToProps, that's my solution:
const [initials, setInitials] = useState('')
useEffect(() => {
if(name && surname) {
setInitials(`${name[0]}${surname[0]}`)
}
}, [name, surname])
and simple mapStateToProps
const mapStateToProps = ({ user }) => ({
name: user.name,
surname: user.surname,
})
This is not something that can be awaited: await '${user.name[0]}${user.surname[0]}' which is why you're still getting an error. You shouldn't need (or want, see https://react-redux.js.org/using-react-redux/connect-mapstate#mapstatetoprops-functions-should-be-pure-and-synchronous) to make mapStateToProps async. You're likely already doing the async call with an async redux action. You should either check for the presence of user name/surname and return a default state if undefined
const initial = user.name && user.surname ? '${user.name[0]}${user.surname[0]}' : 'UNDEF';
or add an isUserLoaded flag for the user state and return the default state, loading indicator, or not render that portion of UI until that flag is true.

React get state from Redux store within useEffect

What is the correct way to get state from the Redux store within the useEffect hook?
useEffect(() => {
const user = useSelector(state => state.user);
});
I am attempting to get the current state within useEffect but I cannot use the useSelector call because this results in an error stating:
Invariant Violation: Hooks can only be called inside the body of a function component.
I think I understand why as it breaks one of the primary rules of hooks.
From reviewing the example on the Redux docs they seem to use a selectors.js file to gather the current state but this reference the mapStateToProps which I understood was no longer necessary.
Do I need to create some kind of "getter" function which should be called within the useEffect hook?
Don't forget to add user as a dependency to useEffect otherwise your effect won't get updated value.
const user = useSelector(state => state.user);
useEffect(() => {
// do stuff
}, [user]);
You can place useSelector at the top of your component along with the other hooks:
const MyComponent = () => {
...
const user = useSelector(state => state.user);
...
}
Then you can access user inside your useEffects.
I found using two useEffects to works for me, and have useState to update the user (or in this case, currUser).
const user = useSelector(state=>state.user);
const [currUser, setCurrUser] = useState(user);
useEffect(()=>{
dispatch(loadUser());
}, [dispatch]);
useEffect(()=>{
setCurrUser(user);
}, [user]);
You have to use currUser to display and manipulate that object.
You have two choices.
1 - If you only need the value from store once or 'n' time your useEffect is called and don't want to listen for any changes that may occur to user state from redux then use this approach
//import the main store file from where you've used createStore()
import {store} from './store' // this will give you access to redux store
export default function MyComponent(){
useEffect(() =>{
const user = store.getState().user;
//...
},[])
}
2 - If you want to listen to the changes that may occur to user state then the recommended answer is the way to go about
const MyComponent = () => {
//...
const user = useSelector(state => state.user);
useEffect(() => {
//...
},[])
//...
}
const tournamentinfofromstore=useSelector(state=>state.tournamentinfo)
useEffect(() => {
console.log(tournamentinfofromstore)
}, [tournamentinfofromstore])
So the problem is that if you change the state inside the useEffect that causes a rerender and then again the useEffect gets called "&&" if that component is passing data to another component will result in infinite loops.and because you are also storing that data in the child component's state will result in rerendering and the result will be infinite loop.!!
Although it is not recommended, you can use store directly in your component, even in the useEffect.
First, you have to export store from where it is created.
import invoiceReducer from './slices/invoiceSlice';
import authReducer from './slices/authSlice';
export const store = configureStore({
reducer: {
invoices: invoicesReducer,
auth: authReducer,
},
});
Then you can import it to a React Component, or even to a function, and use it.
import React, { useEffect } from 'react';
import { store } from './store';
const MyComponent = () => {
useEffect(()=> {
const invoiceList = store.getState().invoices
console.log(invoiceList)
}, [])
return (
<div>
<h1>Hello World!</h1>
</div>
)
}
export default MyComponent
You can study the API for Store in here.
You can also see why this approach is not recommended in
here.
Or, if you are interested in using redux store outside a react component, take a look at this blog post.
To add on top of #Motomoto's reply. Sometimes you depend on store to be loaded before useEffect. In this case you can simply return in if the state is undefined. useEffect will rerender once the store is loaded
const user = useSelector(state => state.user);
useEffect(() => {
if(user === undefined){
return}else{
// do stuff
}}, [user]);
I'm having the same issue, The problem to the useSelector is that we cant call it into the hook, so I can't be able to update with the action properly. so I used the useSelector variable as a dependency to the useEffect and it solved my problem.
const finalImgData_to_be_assigned = useSelector((state) => state.userSelectedImg);
useEffect(()=>{
console.log('final data to be ready to assign tags : ', finalImgData_to_be_assigned.data);
}, [finalImgData_to_be_assigned ])

Resources