6

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.

Jordan Paz
  • 101
  • 1
  • 4
  • 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 – skovy Sep 08 '19 at 02:58

3 Answers3

14

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

skovy
  • 4,535
  • 2
  • 14
  • 31
  • 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) }` – Jordan Paz Sep 08 '19 at 03:05
  • 1
    You can see here that's not the case, it is being mutated: https://codesandbox.io/s/array-mutation-5oocr – skovy Sep 08 '19 at 03:17
  • 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. – Jordan Paz Sep 08 '19 at 03:25
  • 1
    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. – skovy Sep 08 '19 at 04:25
  • 1
    `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 : ) – Pavel Ye Sep 10 '19 at 17:39
  • Thank you. You're right. I think react detects address changes for reference types rather than content changes. – mutoe Apr 07 '20 at 06:09
3

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]
})
mutoe
  • 159
  • 1
  • 10
1

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

Cory Lewis
  • 51
  • 3