denislexic
denislexic

Reputation: 11342

React hooks & Context: Error when using context inside a child component with useEffect

I've created a react function component for the context as follows:

const ItemContext = createContext()

const ItemProvider = (props) => {
    const [item, setItem] = useState(null)

    const findById = (args = {}) => {
        fetch('http://....', { method: 'POST' })
          .then((newItem) => {
            setItem(newItem)
          })
    }

    let value = {
        actions: {
            findById
        },
        state: {
            item
        }
    }

    return <ItemContext.Provider value={value}>
        {props.children}
    </ItemContext.Provider>
}

In this way, I have my context that handles all the API calls and stores the state for that item. (Similar to redux and others)

Then in my child component further down the line that uses the above context...

const smallComponent = () =>{
    const {id } = useParams()
    const itemContext = useContext(ItemContext)

    useEffect(()=>{
        itemContext.actions.findById(id)
    },[id])

    return <div>info here</div>
}

So the component should do an API call on change of id. But I'm getting this error in the console:

React Hook useEffect has a missing dependency: 'itemContext.actions'. Either include it or remove the dependency array react-hooks/exhaustive-deps

If I add it in the dependency array though, I get a never ending loop of API calls on my server. So I'm not sure what to do. Or if I'm going at this the wrong way. Thanks.

=== UPDATE ==== Here is a jsfiddle to try it out: https://jsfiddle.net/zx5t76w2/ (FYI I realized the warning is not in the console as it's not linting)

Upvotes: 0

Views: 8505

Answers (2)

Bennett Dams
Bennett Dams

Reputation: 7033

You could just utilize useCallback for your fetch method, which returns a memoized function:

const findById = useCallback((args = {}) => {
  fetch("http://....", { method: "POST" }).then(newItem => {
    setItem(newItem);
  });
}, []);

...and put it in the useEffect:

...
const { actions, state } = useContext(ItemContext)

useEffect(() => {
    actions.findById(id)
}, [id, actions.findById])
...

Working example: https://jsfiddle.net/6r5jx1h7/1/

Your problem is related to useEffect calling your custom hook again and again, because it's a normal function that React is not "saving" throughout the renders.


UPDATE

My initial answer fixed the infinite loop. Your problem was also related to the way you use the context, as it recreates the domain objects of your context (actions, state, ..) again and again (See caveats in the official documentation).

Here is your example in Kent C. Dodds' wonderful way of splitting up context into state and dispatch, which I can't recommend enough. This will fix your infinite loop and provides a cleaner structure of the context usage. Note that I'm still using useCallback for the fetch function based on my original answer:

Complete Codesandbox https://codesandbox.io/s/fancy-sea-bw70b

App.js

import React, { useEffect, useCallback } from "react";
import "./styles.css";
import { useItemState, ItemProvider, useItemDispatch } from "./item-context";

const SmallComponent = () => {
  const id = 5;
  const { username } = useItemState();
  const dispatch = useItemDispatch();

  const fetchUsername = useCallback(async () => {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/users/" + id
    );
    const user = await response.json();
    dispatch({ type: "setUsername", usernameUpdated: user.name });
  }, [dispatch]);

  useEffect(() => {
    fetchUsername();
  }, [fetchUsername]);

  return (
    <div>
      <h4>Username from fetch:</h4>
      <p>{username || "not set"}</p>
    </div>
  );
};

export default function App() {
  return (
    <div className="App">
      <ItemProvider>
        <SmallComponent />
      </ItemProvider>
    </div>
  );
}

item-context.js

import React from "react";

const ItemStateContext = React.createContext();
const ItemDispatchContext = React.createContext();

function itemReducer(state, action) {
  switch (action.type) {
    case "setUsername": {
      return { ...state, username: action.usernameUpdated };
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
}

function ItemProvider({ children }) {
  const [state, dispatch] = React.useReducer(itemReducer, {
    username: "initial username"
  });
  return (
    <ItemStateContext.Provider value={state}>
      <ItemDispatchContext.Provider value={dispatch}>
        {children}
      </ItemDispatchContext.Provider>
    </ItemStateContext.Provider>
  );
}

function useItemState() {
  const context = React.useContext(ItemStateContext);
  if (context === undefined) {
    throw new Error("useItemState must be used within a CountProvider");
  }
  return context;
}

function useItemDispatch() {
  const context = React.useContext(ItemDispatchContext);
  if (context === undefined) {
    throw new Error("useItemDispatch must be used within a CountProvider");
  }
  return context;
}

export { ItemProvider, useItemState, useItemDispatch };

Both of these blog posts helped me a lot when I started using context with hooks initially:

https://kentcdodds.com/blog/application-state-management-with-react https://kentcdodds.com/blog/how-to-use-react-context-effectively

Upvotes: 2

Arber Sylejmani
Arber Sylejmani

Reputation: 2108

OK, I didn't want to write an answer as Bennett basically gave you the fix, but I think it is missing the part in the component, so here you go:

const ItemProvider = ({ children }) => {
   const [item, setItem] = useState(null)

   const findById = useCallback((args = {}) => {
      fetch('http://....', { method: 'POST' }).then((newItem) => setItem(newItem))
   }, []);

   return (
      <ItemContext.Provider value={{ actions: { findById }, state: { item } }}>
         {children}
      </ItemContext.Provider>
   )
}

const smallComponent = () => {
   const { id } = useParams()
   const { actions } = useContext(ItemContext)

   useEffect(() => {
     itemContext.actions.findById(id)
   }, [actions.findById, id])

   return <div>info here</div>
}

Extended from the comments, here's the working JSFiddle

Upvotes: 0

Related Questions