Search code examples
reactjsreact-hooksuse-reduceruse-context

How to stop re-rendering when using useContext (Reactjs)?


I have a simple react app in which there is a FruitsList component for showing the fruits in the list, a FruitForm component to add a fruit, and both are contained inside a Fruits component. I am using useContext and useReducer to manage the state of the fruits. I have created a FruitContext for same. I want to stop the re-rendering of FruitForm since it is only using dispatch function and re-rendering it is useless every time new fruit is added. Plz suggest any solution for same.

Form Component

const Form = () => {

  const { dispatch } = useContext(FruitsContext);
  const { setLoading } = useContext(LoaderContext);

  let formRef = null;
  const fruit = {};

  const formSubmitHandler = async (event) => {
      event.preventDefault();
      setLoading(true);
      await fetch('https://fruit-basket-74269.firebaseio.com/fruits.json', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(fruit)
        });
        dispatch({type: 'ADD', fruit: fruit});
        // formRef.reset();
        setLoading(false);
      }

      return (
        <Card>
          {console.log('[Form]')}
          <form ref={ref => formRef = ref} onSubmit={formSubmitHandler} className={style.form} autoComplete="off">
            <div className={style.formGroup}>
              <input onChange={event => fruit.item = event.target.value} className={style.input} type="text" id="name" placeholder="Enter fruit name" />
              <label className={style.label} htmlFor="name">Name</label>
            </div>
            <div className={style.formGroup}>
              <input onChange={event => fruit.qty = event.target.value} className={style.input} type="number" min="0" id="qty" placeholder="Enter quantity" />
              <label className={style.label} htmlFor="qty">Quantity</label>
            </div>
            <Button>Add Fruit</Button>
          </form>
        </Card>
      )
}

export default React.memo(Form);

FruitList

const FruitList = () => {

  const { fruits } = useContext(FruitsContext);
  console.log('[FruitList]:', fruits);

  return useMemo(() => {

    return (
      <div className={style.fruitList}>
        <h2 className={style.heading}>Fruits</h2>
        <hr />
        <div className={style.list}>
          <FruitCard name={'Apple'} qty={15} />
          <FruitCard name={'Orange'} qty={10} />
          <FruitCard name={'Grapes'} qty={20} />
        </div>
      </div>
    );
  }, []);
}

export default FruitList;

Fruits

const Fruits = () => {

  console.log('[Fruits Parent]');

  // const { loading } = useContext(LoaderContext);

  return (
    <div className={style.fruits}>
      {/* {loading && <Loader />} */}
      <Form />
      <br />
      <Filter />
      <br />
      <FruitList />
    </div>
  )
}

export default Fruits

FruitContext

export const FruitsContext = createContext();

const FruitsProvider = ({children}) => {
  const [fruits, dispatch] = useReducer(reducer, []);

  const value = ({
    fruits, dispatch
  });

  return (
    <FruitsContext.Provider value={value}>
      { children }
    </FruitsContext.Provider>
  );
}

export default FruitsProvider;

FruitReducer

export default (state, action) => {
  switch(action.type) {
    case 'LOAD':
      return action.fruits
    case 'ADD':
      console.log('[Pre-Action]', state);
      const newList = [...state];
      newList.push(action.fruit);
      console.log('[Post-Action]', newList);
      return newList;
    case 'DELETE':
      return state.filter(fruit => fruit.id !== action.id);
    default: return state;
  }
}

Solution

  • Components that consume a context will always rerender if anything in the providers value changed. Regardless of whether you actually use that value or not (In this case for example even if you only pull the dispatch function).

    Usually you don't need to optimize something like that for most react applications, react is already quite fast and a few additional rerenders don't hurt. Any performance issues can be solved when and where they happen. If you want to optimize from the start you can split your reducers state and dispatch into two different contexts. They both can be put into the same ProviderComponent but there have to be two different Context.Provider components. One will use the state as value and the other will use the dispatch function as value. If you then consume the dispatch context, it won't cause the component to rerender if the dispatch action changes the state.

    // Update

    As an example:

    const FruitsProvider = ({children}) => {
      const [fruits, dispatch] = useReducer(reducer, []);
    
      return (
        <FruitsStateContext.Provider value={fruits}>
          <FruitsDispatchContext.Provider value={dispatch}>
            { children }
          </FruitsDispatchContext.Provider>
        </FruitsStateContext.Provider>
      );
    }
    

    I would also recommend to not export the context directly but instead export hooks that expose the state or the dispatch.

    e.g.

    export const useFruits = () => {
      const fruitsState = React.useContext(FruitsStateContext);
      if (!fruitsState) {
        throw new Error('you cant use the useFruits hook outside the FruitsStateContext');
      }
      return fruitsState;
    }