Search code examples
reactjsreact-contextreact-custom-hooksglobal-state

Is there a way to make a custom hook modify a global state on any call?


This is my custom hook:

function useFetch({url = '', method = 'get', body}) {
  const [data, setData] = useState(null);
  useEffect(() => {
    try {
      (async () => {
        const data = await fetch[method](url, body);
        setData(data);
      })();
    } catch (err) {
      console.log("An error ocurred")
    }
  }, [url, method, body]);
  return [(!data && <LoadingIcon />, data, setData];
}

I want to execute setUserFeedbackMsg("An error ocurred"), which is part of a context component, every time an error ocurrs on any instantiation of the hook. Of course I could do it manually on every component that uses the hook, but I'm wondering if there's a way to condense it all in one place. Thank you!


Solution

  • Compose your context (global state) into a hook that components and other hooks can access.

    // obviously, this has been simplified to help with the concept
    
    // fake fetch
    function fakeFetchThatErrs() {
      return new Promise((resolve, reject) => {
        setTimeout(() => reject("error msg from our api!"), 1500);
      });
    }
    
    
    // a custom hook to wrap our state 
    function useUserFeedback() {
      const [message, setMessage] = React.useState('');
      
    
      return {
        message,
        setMessage
      };
    }
    
    // our context
    const UserFeedbackContext = React.createContext();
    
    // a custom hook to access our context
    function useUserFeedbackContext() {
      return React.useContext(UserFeedbackContext);
    }
    
    // the context provider component
    function UserFeedbackProvider({ children }) {
      const feedback = useUserFeedback();
      return (<UserFeedbackContext.Provider value={feedback}>{children}
      </UserFeedbackContext.Provider>);
    }
    
    // your useFetch hook (without the loading component)
    function useFetch() {
      const [data, setData] = React.useState(null);
      // here we use our custom hook to "hook" into the context so we can use the setter!
      const { setMessage } = useUserFeedbackContext();
      React.useEffect(() => {
        // changing this because (a) StackOverflow snippets don't support JSX along with async/await and (b) no need to really fetch here, we'll fake it
         fakeFetchThatErrs().then((data) => {
           // write to our data
           setData(data);
         }).catch((err) => {
           console.log("Error:", err)
           // uh oh, error! write to our context 
           setMessage(err);
         });
        
      }, [setMessage]);
      return [data, setData];
    }
    
    // our demo app component
    function App() {
      const [data] = useFetch();
      // consume our context!
      const { message } = useUserFeedbackContext();
      
      return data ? <div>Success: {data}</div> : message ? <div>Something went wrong: {message}</div> : <div>Fetching...</div>;
       
    }
    
    // don't forget to use our provider!
    ReactDOM.render(<UserFeedbackProvider><App /></UserFeedbackProvider>, document.getElementById('root'));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
    <div id="root"></div>

    Here's what's happening here in a nutshell:

    1. We have a state object that we can access via a hook, useUserFeedback.
    2. We create a Context around this state object and wrap that in its own hook, useUserFeedbackContext. This way, we can easily access the state getter and setter in components and, importantly, other hooks!
    3. Thus, we can use the useUserFeedbackContext hook in our useFetch hook to set the message state to an error assuming there is one.
    4. A common question around this (apologies if you already know this) - why can't we just use the useUserFeedback hook in useFetch? The answer is an important concept in React - hooks share behavior, while Contexts share state. If we simply tried to use the useUserFeedback hook, each time we called useFetch would result in a new '' message state object. Using Context, we share one useUserFeedback instantiation across the board.