Search code examples
javascriptreactjsuse-effectreact-contextuse-context

UseContext doesn't re-execute component


I am experienced javascript developer. However I have to move on react so right now I know few concepts. I've been debugging this code for 4 hours and couldn't find any mistake. I mean, it is probably up to because this hook is a bit confusing.

What app should do? Basically it should display food component based on dummy coded data. It works good. On that newly displayed component should be a form which has input field. When we submit form, that number from the form should be taken and written in a food object. Object should look like this: {Sushi: 0}. Whenever I submit, it should update. Besides that, there isn't just one property in object. There could be more, for example: {Sushi: 0, Pizza: 0, Cherry: 0}. That depends on dummy data. At the begging of the app it automatically creates this object and push into state. Then, I send state updating function to the child component and when we submit form it updated the state. That works fine. Now on the current component, I have useEffect. That use effect trigers whenever state changes. What it does, it updates context. Now when I update context, in "third" component all the values should be taken and sum of the values should bee displayed. The problem is that, whatever I tried, It doesn't re-executes that "third" component...

Code: App.js

function App() {
return (
<FoodStateContext.Provider
  value={{
    foodState: {},
    setContext: function (val) {
      this.foodState = val;
    },
  }}
>
  <Header />
  <BodyContainer>
    <Card className={styles["food-message"]}>
      <h2>Delicious Food, Delivered To You</h2>
      <p>
        Choose your favourite meal from our broad selection of avaible meets
        and enjoy a delicious lunch or dinner at home.
      </p>
      <p>
        All our meals are cooked with high-quality ingredients, just-in-time
        and of course by experienced chefs!
      </p>
    </Card>
    <FoodContainer foodList={food} />
  </BodyContainer>
</FoodStateContext.Provider>
);
}

export default App;


Food container

import react, { useState, useEffect, useReducer, useContext } from "react";

import styles from "./FoodContainer.module.css";
import Card from "../UI/Card/Card";
import FoodComponent from "./FoodComponent";
import FoodStateContext from "../store/food-state-context";

const FoodContainer = (props) => {
const foodList = props.foodList;

const [foodState, setFoodState] = useState({});
const ctx = useContext(FoodStateContext);

useEffect(() => {
let obj = {};
for (let i = 0; i < foodList.length; i++) {
  obj[foodList[i].name] = 0;
}
setFoodState(obj);
}, []);

console.log(foodState);

useEffect(() => {
console.log("muda");
ctx.foodState = foodState;
ctx.setContext(foodState);
console.log(ctx.foodState);
}, [foodState]);

return (
<Card className={styles["food-container"]}>
  {foodList.map((food) => {
    return (
      <FoodComponent
        key={food.name}
        food={food}
        updateFoodState={setFoodState}
      ></FoodComponent>
    );
  })}
</Card>

); };

export default FoodContainer;


Food component

import React, { useReducer, useEffect, useContext } from "react";
import styles from "./FoodComponent.module.css";
import Input from "../UI/Input/Input";
import Button from "../UI/Button/Button";

const FoodComponent = (props) => {
const food = props.food;

const formSubmitHandler = (e) => {
e.preventDefault();
props.updateFoodState((prevState) => {
  return {
    ...prevState,
    [food.name]: prevState[food.name] + +e.target[0].value,
  };
});
};

return (
<div className={styles["food-component"]}>
  <div className={styles["food-component__description"]}>
    <p>{food.name}</p>
    <p>{food.description}</p>
    <p>{food.price}</p>
  </div>

  <form onSubmit={formSubmitHandler}>
    <div>
      <div>
        <span>Amount:</span>
        <Input />
      </div>
      <Button type="submit">+Add</Button>
    </div>
  </form>
</div>
);
};

export default FoodComponent;

Food context

import react from "react";

const FoodStateContext = react.createContext({
foodState: "",
setContext: (val) => {
console.log(val);
this.foodState = val;
},
});

export default FoodStateContext;

That third component

import React, { useState, useContext, useEffect } from "react";
import styles from "./Cart.module.css";
import FoodStateContext from "../store/food-state-context";

const Cart = (props) => {
const [totalProductsNumber, setTotalProductsNumber] = useState(0);

const ctx = useContext(FoodStateContext);
console.log(ctx.foodState);
useEffect(() => console.log("govna"), [ctx]);

return (
<div className={styles.cart}>
  <div>Ic</div>
  <p>Your Cart</p>
  <div>
    <span>{totalProductsNumber}</span>
  </div>
</div>
);
};

export default Cart;

Solution

    1. You are writing the context in the wrong way. To set the new foodState value you can't use this.foodState but you need to use the useState hooks to save and update the value.
    2. React.useEffect doesn't use a deep comparison for objects, so the condition in the third component never triggers.

    This is my solution. I haven't tested that but I have done some changes that should fix your problems and optimize your code.

    // FoodStateProvider.js
    import React from "react";
    
    export const FoodStateContext = React.useContext({
      foodState: {},
      setFoodState: () => undefined,
    });
    
    // Use a custom provider to handle the context logic
    export const FoodStateProvider = ({ children }) => {
      const [foodState, setFoodState] = React.useState({});
    
      return (
        <FoodStateContext.Provider
          value={{
            foodState,
            setFoodState,
          }}
        >
          {children}
        </FoodStateContext.Provider>
      );
    };
    

    Then use the FoodStateProvider in your app

    // App.js
    const App = () => (
      <FoodStateProvider> ...your components </FoodStateProvider>
    )
    

    Then, I have remove the useState in the FoodContainer because now you handle it in the context:

    // FoodContainer.js
    import React, { useState, useEffect, useContext } from "react";
    
    import styles from "./FoodContainer.module.css";
    import Card from "../UI/Card/Card";
    import FoodComponent from "./FoodComponent";
    import FoodStateContext from "../store/food-state-context";
    
    const FoodContainer = ({ foodList }) => {
      const ctx = useContext(FoodStateContext);
    
      // The useEffect don't works well here, using a callback helps to remove 
      // additional data structures and conditions problems
      const setFoodState = useCallback(({name, valueToAdd}) => {
        // Logic from FoodComponent now is here
        const newValue = (ctx.foodState[name] ?? 0) + valueToAdd;
        ctx.setFoodState(currentFoodState => {...currentFoodState, [name]: newValue});
      }, [foodState]);
    
      return (
        <Card className={styles["food-container"]}>
          {foodList.map((food) => (
            <FoodComponent
              key={food.name}
              food={food}
              updateFoodState={setFoodState}
            />
          ))}
        </Card>
      );
    };
    
    // FoodComponent.js
    import React, { useReducer, useEffect, useContext } from "react";
    import styles from "./FoodComponent.module.css";
    import Input from "../UI/Input/Input";
    import Button from "../UI/Button/Button";
    
    const FoodComponent = ({ food, updateFoodState }) => {
      const formSubmitHandler = (e) => {
        e.preventDefault();
        updateFoodState({ name: food.name, valueToAdd: e.target[0].value });
      };
    
      return (
        <div>
          your components
          <form onSubmit={formSubmitHandler}> your form </form>
        </div>
      );
    };
    
    export default FoodComponent;