Component not re rendering when value from useContext is updated
Asked Answered
R

4

33

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.

Rerun answered 8/9, 2019 at 2:55 Comment(1)
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 equalityJehiah
J
32

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

Edit context-arrays

Jehiah answered 8/9, 2019 at 3:1 Comment(8)
I believe my function is already assigning a new array rather than mutating const addToOrder = (idToAdd, quantityToAdd = 1) => { let newProducts = products newProducts[newProducts.length] = { id: idToAdd, quantity: quantityToAdd, } setProducts(newProducts) }Rerun
You can see here that's not the case, it is being mutated: codesandbox.io/s/array-mutation-5oocrJehiah
I don't understand why it is being mutated, since I am creating a new array and mutating that and then setting the state to the new array. I used your solution and it worked, so thank you very much for your help.Rerun
The same array was being assigned to different variables/arguments. So it may have had a different variable name, but it was always the same array.Jehiah
let newProducts = products - this is an assignment by reference. Any modifications done to newProducts will affect products as well. To avoid mutating original array, always create a copy - let newProducts = [ ...products ]. I wouldn't recommend this approach in your situation though since it's an unnecessary step. Just follow @skovy's advice : )Electroplate
Thank you. You're right. I think react detects address changes for reference types rather than content changes.Daddylonglegs
Unlike @JordanPaz's example, I'm doing: const newArray = []; newArray.push("example"); setArray(newArray); and it's still not updating. This returns false: newArray === array. A useEffect on array in the context itself is triggered when the value changes. The array is a dependency of the hook in question.Bolshevist
Immediately after my previous comment, I noticed that I had nested two ArrayProviders. After removing the inner ArrayProvider, it works.Bolshevist
D
6

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]
})
Daddylonglegs answered 7/4, 2020 at 6:13 Comment(0)
S
5

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

Spasmodic answered 16/2, 2021 at 19:56 Comment(0)
C
0

enter image description here

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.

Catima answered 2/10, 2022 at 8:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.