2

I'm working on my first React project and I have the following problem. How I want my code to work:

  • I add Items into an array accessible by context (context.items)
  • I want to run a useEffect function in a component, where the context.items are displayed, whenever the value changes

What I tried:

  1. Listing the context (both context and context.items) as a dependency in the useEffect
  • this resulted in the component not updating when the values changed
  1. Listing the context.items.length
  • this resulted in the component updating when the length of the array changed however, not when the values of individual items changed.
  1. wraping the context in Object.values(context)
  • result was exactly what I wanted, except React is now Complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *

Do you know any way to fix this React warning or a different way of running useEffect on context value changing?

Well, didn't want to add code hoping it would be some simple error on my side, but even with some answers I still wasn't able to fix this, so here it is, reduced in hope of simplifying.

Context component:

const NewOrder = createContext({
  orderItems: [{
    itemId: "",
    name: "",
    amount: 0,
    more:[""]
  }],
  addOrderItem: (newOItem: OrderItem) => {},
  removeOrderItem: (oItemId: string) => {},
  removeAllOrderItems: () => {},
});

export const NewOrderProvider: React.FC = (props) => {
  // state
  const [orderList, setOrderList] = useState<OrderItem[]>([]);

  const context = {
    orderItems: orderList,
    addOrderItem: addOItemHandler,
    removeOrderItem: removeOItemHandler,
    removeAllOrderItems: removeAllOItemsHandler,
  };

  // handlers
  function addOItemHandler(newOItem: OrderItem) {
    setOrderList((prevOrderList: OrderItem[]) => {
      prevOrderList.unshift(newOItem);
      return prevOrderList;
    });
  }
  function removeOItemHandler(oItemId: string) {
    setOrderList((prevOrderList: OrderItem[]) => {
      const itemToDeleteIndex = prevOrderList.findIndex((item: OrderItem) => item.itemId === oItemId);
      console.log(itemToDeleteIndex);
      prevOrderList.splice(itemToDeleteIndex, 1);
      return prevOrderList;
    });
  }
  function removeAllOItemsHandler() {
    setOrderList([]);
  }

  return <NewOrder.Provider value={context}>{props.children}</NewOrder.Provider>;
};

export default NewOrder;

the component (a modal actually) displaying the data:

const OrderMenu: React.FC<{ isOpen: boolean; hideModal: Function }> = (
  props
) => {
const NewOrderContext = useContext(NewOrder);
useEffect(() => {
    if (NewOrderContext.orderItems.length > 0) {
      const oItems: JSX.Element[] = [];
      NewOrderContext.orderItems.forEach((item) => {
        const fullItem = {
          itemId:item.itemId,
          name: item.name,
          amount: item.amount,
          more: item.more,
        };
        oItems.push(
          <OItem item={fullItem} editItem={() => editItem(item.itemId)} key={item.itemId} />
        );
      });
      setContent(<div>{oItems}</div>);
    } else {
      exit();
    }
  }, [NewOrderContext.orderItems.length, props.isOpen]);

some comments to the code:

  • it's actually done in Type Script, that involves some extra syntax -content (and set Content)is a state which is then part of return value so some parts can be set dynamically -exit is a function closing the modal, also why props.is Open is included
  • with this .length extension the modal displays changes when i remove an item from the list, however, not when I modify it not changeing the length of the orderItems,but only values of one of the objects inside of it.
  • as i mentioned before, i found some answers where they say i should set the dependency like this: ...Object.values(<contextVariable>) which technically works, but results in react complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
  • the values displayed change to correct values when i close and reopen the modal, changing props.isOpen indicating that the problem lies in the context dependency
Matus86309
  • 41
  • 1
  • 8
  • By providing the piece of code, I'm sure you'll have more luck getting help. – jperl Sep 22 '21 at 19:10
  • If the reference to items have not changed, the useEffect won't be run again. It all depends on how you update the items (you shouldn't mutate state). – jperl Sep 22 '21 at 19:13
  • Could you please show us what the useEffect contains and how you update the state (context.items)? – jperl Sep 22 '21 at 19:22
  • @jperl here is what i consider the core part of code where my problem lies, hope I didn't miss anything important – Matus86309 Sep 23 '21 at 18:28
  • 1
    I don't know where you found these answers telling you to use `...Object.values()` but please, don't do that, it's horrible. @EmmaJoe actually provided you with a nice example. Do you notice the difference? As I told you, so long as the reference doesn't change, useEffect won't rerun. EmmaJoe destructured the previous value to build a new one, like so `let updatedCart = [...cart]`. This way we get a new reference. You didn't, you took the previous value and applied unshift on it. This is bad, not only you mutated the state but you also didn't get a new reference. – jperl Sep 23 '21 at 21:37
  • As you mutated the state directly and as React will consider that nothing changed (remember, React does a shallow comparison), the context won't rerender and so won't the children either. – jperl Sep 23 '21 at 21:41
  • @jperl, thank you for clearing it up, I didn't really understand what was the difference, why EmmaJoe's code worked but mine not and all you mentioned indeed were the "errors" in my code. It's kinda surprising that for such a basic thing I could google no answer, and had to ask here... – Matus86309 Sep 25 '21 at 06:16

2 Answers2

5

You can start by creating your app context as below, I will be using an example of a shopping cart

import * as React from "react"

const AppContext = React.createContext({
  cart:[]
});

const AppContextProvider = (props) => {
  const [cart,setCart] = React.useState([])

  const addCartItem = (newItem)=>{
    let updatedCart = [...cart];
    updatedCart.push(newItem)
    setCart(updatedCart)
    
  }
  
  return <AppContext.Provider value={{
    cart
  }}>{props.children}</AppContext.Provider>;
};

const useAppContext = () => React.useContext(AppContext);

export { AppContextProvider, useAppContext };

Then you consume the app context anywhere in the app as below, whenever the length of the cart changes you be notified in the shopping cart

import * as React from "react";
import { useAppContext } from "../../context/app,context";

const ShoppingCart: React.FC = () => {
  const appContext = useAppContext();

  React.useEffect(() => {
    console.log(appContext.cart.length);
  }, [appContext.cart]);
  return <div>{appContext.cart.length}</div>;
};

export default ShoppingCart;
EmmaJoe
  • 362
  • 4
  • 9
  • thank you for your answer, although I didn't understand why at first, it is the correct solution. – Matus86309 Sep 25 '21 at 06:17
  • For those like me who still don't get it after reviewing this code the core part is explained in the comments under the question. – Matus86309 Sep 25 '21 at 06:18
  • ...and how can I use _setCart_ from any other component of my app without passing it with the props? I mean... _useAppContext_ gives me the content of the cart, but it does not expose _setCart_ (In my scenario I want to be able to add notifications in any part of my application and show them in a notification bar) – Alex 75 Mar 27 '22 at 11:30
  • @Alex 75, this change should be done via "reducer" in the context / reducer way – Carmine Tambascia Sep 09 '22 at 08:01
0

You can try passing the context variable to useEffect dependency array and inside useEffect body perform a check to see if the value is not null for example.

Sorin
  • 111
  • 1
  • 6