DEV Community

Cover image for Understanding the difference between useContext and custom hooks
kebin20
kebin20

Posted on • Updated on

Understanding the difference between useContext and custom hooks

Continuing from my previous blog post, I wanted to share my understanding of the difference between useContext and custom hooks.

After completing all the main functions of my app, I noticed that there was some repeated code across three of my components. I thought, "Why not refactor it and create a common component to handle all the business logic that could be used across all other components and future components too?"

My first idea was to use useContext to hold all the common state variables and functions, which could then be shared across all of the other components.

For context (heh, get it), useContext is a hook that allows access to data and functions from a parent component within a child component without having to pass props down through layers of children components (aka prop drilling). It's a way to share state between components without the use of props, cutting the middleman out.

Although my project wasn't deeply nested, I wanted to be more familiar with useContext, so I refactored my code and transferred all of the common business logic into a context file.

Essentially, my components would receive an array of objects passed via props, and various functions would modify the passed-down data and send the modified data back upstream, changing the state within the parent component. Here's what it looks like when using useContext:

export const ExampleContext = React.createContext<ExampleContextObj>({
    data: [],
    doSomething1: () => {},
    doSomething2: () => {},
  });

function ExampleContextProvider({passedInObjectArray, children}){
  const [modifedData, setModifiedData] = useState(passedInObjectArray);
  const [something, setSomething] = useState("");

  function doSomething1() {
      //...rest of code,
      setModifedData(changedData)
    }

    function doSomething2() {
      //...rest of code,
      setSomething("thing")
    }

  const contextValue: ExampleContextObj = {
    data: modifiedData,
    doSomething1: doSomething1,
    doSomething2: doSomething2,
  };

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

export default ExampleContextProvider;
Enter fullscreen mode Exit fullscreen mode

Then, within my App component it will look like:

function App() {
  const [object1, setObject1] = useState(originalObjectArray1)
  const [object2, setObject2] = useState(originalObjectArray2)
  const [object3, setObject3] = useState(originalObjectArray3)

  function modifyObject1(newData){
    setObject1(newData)
    //... rest of code
   }

  //... rest of code

  return (
  <>
    <ExampleContextProvider passedInObjectArray={object1}>
          <ComponentOne />
    </ExampleContextProvider>

    <ExampleContextProvider passedInObjectArray={object2}>
          <ComponentTwo />
    </ExampleContextProvider>

    <ExampleContextProvider passedInObjectArray={object3}>
          <ComponentThree />
    </ExampleContextProvider>
  </>
   )
}

Enter fullscreen mode Exit fullscreen mode

Then, within say, Component One it will look like this:

import React, { useContext } from "react";

function ComponentOne() {
  const {
    data,
    doSomething1,
    doSomething2,
  } = useContext(FlashcardContext);

 return (
   <>
     <h1>{data[0].title}</h1>
     <button onClick={doSomething1} />
     <button onClick={doSomething2} />   
  </>
 )
Enter fullscreen mode Exit fullscreen mode

And imagine it will be similar with Component Two and Three.

When I first started using separate context providers to wrap around each component, I assumed that each component would only receive data that was relevant to it. However, I quickly discovered that this was not the case, as Component 2 and 3 were showing the same data as Component 1.

I was surprised to find that the data was being shared across all components, despite the fact that I had separated the context providers. I tried several hacks, such as using ternaries to check the type of object being passed down within each component, but none of them worked.

It was then that I realized I had overlooked a core concept of useContext: context holds a universal state, which means that components have easy access to the same data and functions without needing to drill down through props. Rather than using context to pass data down to individual components, it's better suited for sharing data across multiple components that are part of the same section of the application.

Although I was disappointed that I couldn't use useContext for my business logic, I wanted to refactor my code since it was sharing the same logic. I considered making an intermediary component to hold the business logic, but I thought that might make things unnecessarily complicated.

Then I thought of Higher Order Components, which seemed to fit the bill. However, I realized that this method was mostly used in class-based React applications, while my project was using functional components. After some research and prompting from ChatGPT, I remembered something about custom hooks, which I had only used a bit in my course projects. I was excited to finally be able to use them in my project!

I set out to rework the whole code using a custom hook, and everything worked just like before. The great thing about changing it to a custom hook was that I didn't have to change too much code from the context file. I deleted some unnecessary code and renamed a few things, and that was it!

function useExampleCustomHook({passedInObjectArray}){
  const [modifedData, setModifiedData] = useState(passedInObjectArray);
  const [something, setSomething] = useState("");

  function doSomething1() {
      //...rest of code,
      setModifedData(changedData)
    }

    function doSomething2() {
      //...rest of code,
      setSomething("thing")
    }

  return {
    data: modifiedData,
    doSomething1: doSomething1,
    doSomething2: doSomething2,
  };
}

export default useExampleCustomHook;
Enter fullscreen mode Exit fullscreen mode

Which then made my App component leaner:


  function App() {
  const [object1, setObject1] = useState(originalObjectArray1)
  const [object2, setObject2] = useState(originalObjectArray2)
  const [object3, setObject3] = useState(originalObjectArray3)

  function modifyObject1(newData){
    setObject1(newData)
    //... rest of code
   }

  //... rest of code

  return (
     <>
          <ComponentOne passedInObjectArray={object1} />
          <ComponentTwo passedInObjectArray={object2} />
          <ComponentThree passedInObjectArray={object3} />
     </>
   )
}
Enter fullscreen mode Exit fullscreen mode

And finally used it here:

import React from "react";
import useExampleCustomHook  from "./hooks/useExampleCustomHook"

function ComponentOne({passedInObjectArray}) {
  const {
    data,
    doSomething1,
    doSomething2,
  } = useExampleCustomHook(passedInObjectArray);

 return (
   <>
     <h1>{data[0].title}</h1>
     <button onClick={doSomething1} />
     <button onClick={doSomething2} />   
  </>
 )
Enter fullscreen mode Exit fullscreen mode

You can just imagine the sigh of relief I got when this was working for me!

In summary, my exploration of useContext and custom hooks in React highlighted a crucial distinction. Initially drawn to useContext for its promise of simplified state management, I discovered its inherent nature of holding universal state, making it more suitable for sharing data across components within the same section.

Realizing this wasn't the optimal fit for my specific use case, I seamlessly transitioned to custom hooks. This switch not only resolved shared data issues but also emphasized a more modular code structure. The key takeaway lies in understanding the nuanced differences between these tools.

While useContext is designed for universal state-sharing within a specific context, custom hooks provide a more flexible and targeted approach, allowing for efficient encapsulation of logic tailored to individual components.

This was a great learning journey and knowing when to use custom hooks and useContext.

I hope this was a good read and maybe it will help you too in your learning journey!

For anyone wanting to see which project I'm referencing to, it's in my Github repo.

Top comments (0)