{"id":7526,"date":"2024-04-12T15:43:24","date_gmt":"2024-04-12T19:43:24","guid":{"rendered":"https:\/\/cindypotvin.com\/?p=7526"},"modified":"2024-04-12T15:43:27","modified_gmt":"2024-04-12T19:43:27","slug":"managing-state-with-react-context","status":"publish","type":"post","link":"https:\/\/cindypotvin.com\/managing-state-with-react-context\/","title":{"rendered":"Managing state with React Context"},"content":{"rendered":"

Managing state with React doesn’t need to be complex, and the built-in Context in React is a great way to do that. The documentation is a bit lacking around some frequent use cases like fetching data from a backend API service efficiently, but it’s possible to go pretty far with just the context. <\/p>\n\n\n\n

I’ll take you through the step from an API call in a component to wrapping the state management in a context.<\/p>\n\n\n\n

The first concern when managing state here is to avoid calling the API every time the component re-render. The barebone solution is to call the API and save it to a state inside a useEffect<\/code> with an empty dependency array so it runs only once.<\/p>\n\n\n

\nimport React, { useState } from "react";\n\nconst DataDisplay = () => {\n  const [data, setData] = useState("");\n\n  useEffect(() => {\n    getDataFromAPI().then((result) => setData(result));\n  }, []);\n\n  return <div>{data}<\/div>;\n}\n\nexport default DataDisplay;\n<\/pre>\n\n\n\n

That works when you have only one call, but once you start to fetch that data in multiple places it’s not enough: you will either need to copy-paste the whole thing, or pass the result down as props to child components. Also, you’ll likely want more code to handle error cases, logging and the like, and you won’t want that cluttering all your components. <\/p>\n\n\n\n

Using hooks<\/h2>\n\n\n\n

The next logical step is then be to extract all that logic into a hook. This can be a perfectly good long-term solution if you only call it only once in each page and just want to avoid the code duplication, but don’t forget that the full code of the hook runs each time you use it in a component.<\/p>\n\n\n\n

Here is the previous example turned into a hook. I’m only returning the resulting data, but you could also return extra info like a loading state.<\/p>\n\n\n

\nimport React, { useState } from "react";\n\nconst useData = () => {\n  const [data, setData] = useState("");\n\n  useEffect(() => {\n    getDataFromAPI().then((result) => setData(result));\n  }, []);\n\n  return data;\n}\n\nexport default useData;\n<\/pre>\n\n\n\n

If you’re willing to take a dependency, you could also add a caching library such as TanStack Query<\/strong> on top of the API call so it won’t be called more than once, even if the hook runs multiple time. <\/p>\n\n\n\n

Now that we have a hook, the component that displays the data need to use that new hook to access the data, and the useEffect<\/code> will run in the same way it previously did :<\/p>\n\n\n

\nimport React from "react";\nimport useData from ".\/useData";\n\nconst DataDisplay = () => {\n  const data = useData();\n\n  return <div>{data}<\/div>;\n}\n\nexport default DataDisplay;\n<\/pre>\n\n\n\n

But what if you have multiple components in the page that needs that same data, or you have some dependency between your hooks? For a complex page, it may not be as clean to pull in a bunch of hooks.<\/p>\n\n\n\n

Using a context<\/h2>\n\n\n\n

That’s where we’re leveraging the React Context: we’ll wrap all the components that needs the data in a context provider<\/em>, which handles all the fetching and makes sure it runs only once. <\/p>\n\n\n\n

Since a context provider is also a component, you can pass any prop you wish to it. You can also access any useful hooks such as the ones we defined previously. It’s not required, but I find it useful to keep each API call in a separate hook so they can be reused to build another provider, or used straight into another page that don’t need that much complexity.<\/p>\n\n\n\n

There is a bit more boilerplate for a context than a hook since you’ll need to define some default values for the context, initialize it and export everything. <\/p>\n\n\n\n

Here is the data fetching in a provider, with an additional API being fetched so you can see what querying multiple APIs looks like:<\/p>\n\n\n

\nimport React, { createContext, useContext } from "react";\nimport useData from ".\/useData";\nimport useOtherData from ".\/useOtherData";\n\nexport const DataProvider = ({children}) => {\n  const { data } = useData();\n  const { otherData } = useOtherData();\n\n  return (\n    <DataContext.Provider value={{ data, otherData }}>\n      {children}\n    <\/DataContext.Provider>\n  );\n};\n\nconst dataDefault = {\n  data: "",\n  otherData: "",\n};\n\nconst DataContext = createContext(dataDefault);\n\nexport const useDataContext = () => useContext(DataContext);\n<\/pre>\n\n\n\n

Once your provider is ready, you’ll then wrap your top component in the provider: the context will be accessible in each child component under the provider. I’m using one object as the return value, so I can destructure only the parts I need for that component. <\/p>\n\n\n\n

Here are the final parent and child component now using the context we just defined:<\/p>\n\n\n

\nimport React from "react";\nimport DataProvider from ".\/DataProvider";\nimport DataChild from ".\/DataChild";\n\nconst DataDisplay = () => {\n  return <DataProvider><DataChild\/><\/DataProvider>;\n}\n\nexport default DataDisplay;\n<\/pre>\n\n\n
\nimport { useDataContext } from ".\/DataProvider";\n\nconst DataChild = () => {\n  const { otherData } = useDataContext();\n\n  return <div>{otherData}<\/div>;\n}\n\nexport default DataChild;\n<\/pre>\n\n\n
\n <\/div>\n