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.
When the nearest <MyContext.Provider>above the component updates, this Hook will trigger a rerender with the latest context value passed to that MyContext provider. Even if an ancestor uses React.memoor shouldComponentUpdate, a rerender will still happen starting at the component itself using useContext. ...
This does not appear to work with the useContext hooks, as components with useContext won’t automatically re-render when the value is changed from the method. I force a re-render I then see the correct values.
This is by design. If useContext were to conditionally trigger rerenders, the hook would become non-composable. There has been several discussions, especially in this issue . Currently, there’s no direct solution from React core. Three options are described in this issue.
The consumer then can update the context value by invoking the update function setUserName (newContextValue). <UserInfo /> is another consumer of the context. When <UserNameInput /> updates the context, this component is updated too. Note that <Application /> memoizes the context value.
@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.
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]
})
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With