useContext Hook - A Primer
Managing state in large React applications is difficult. Determining what state belongs locally to a component, what data a component receives from props, how much logic needs to be decoupled from a component, etc. directly affects an application's maintainability. One issue most developers face when developing a React application is prop drilling . Sending data down an entire component tree just to reach a single component is unnecessary, especially if none of the intermediate components need this data. With context , we can share global data (e.g., theme, time zone, i18n locale and user authentication status) with any component within the component tree without repeatedly passing data down as props. To understand why context is important, we must first understand the kinds of problems brought about by prop drilling. Consider the following barebones React application that comes with i18n support and allows users to select a preferred language. For instance, within the footer, switching the language from "English" to "Español" translates the application's text from English to Spanish. Note : For production-grade React applications, please use a dedicated i18n library like react-i18next for internationalization. Try it out in this CodeSandbox demo here . When you look at the <Layout /> component, you will notice that the props translate , changeLocale and locale are not used by this component whatsoever. Its sole purpose is to pass these props from the <App /> parent component to the <Navbar /> and <Footer /> child components. Two of the <Layout /> component's props, changeLocale and locale , are only needed for the <Footer /> component. If we decide to remove the <Footer /> component from the <Layout /> component, then we would have to make modifications to: And for larger applications, even more components would have to be refactored before you're able to successfully remove/add a single component. With the same problem also arising with the translate prop, which the <Layout /> component has no use for besides just passing it straight down the component hierarchy, taking the context approach directly exposes these methods to lower-level components like the <Footer /> component without introducing more bloat to intermediate components (in this case, the <Layout /> component). To demonstrate this, let's rewrite the application with context: Try it out in this CodeSandbox demo here . The first step to using context is to create a Context object with the createContext() method: This method accepts a single argument: the default data to share with consumer components if no matching provider component is found. Often, this data is replaced within the component responsible for rendering the provider component. Therefore, the main purpose of passing this data to the createContext() method is to: Every Context object provides the following: Within the render() method of the <App /> component, we have the I18nContext 's provider component, <I18nContext.Provider /> , wrap the entire application so that any child component with an <I18nContext.Consumer /> consumer component, such as <Footer /> , can access the currently selected language ( locale ) and methods for translating text ( translate ) and changing the language ( changeLocale ). Here, the context's default value gets replaced by the <App /> component's state object, which has the same properties. The only difference is that the translate and changeLocale methods interact with the <App /> component's state and actually do as they are named. With the <Layout /> , <Home /> and <Login /> components no longer receiving any props, the <App /> component's render() method becomes much cleaner and easier to reason about. Plus, we no longer have to track these props in the component hierarchy. Although the <Footer /> component is now tightly coupled to the I18nContext , if we decide to remove the <Footer /> component, then we could without making any additional modifications to other parts of the application. Taking a look at the <Footer /> component, changeLocale and locale come from the <I18nContext.Consumer /> consumer component's render props, not from the <Footer /> component's props. With changeLocale and locale only made available in the component's rendering logic, this render props pattern prevents us from defining functions (that rely on these values, such as event handlers) outside of the rendering logic. So for the (evt) => changeLocale(evt.target.value) callback passed to the <select /> element's onChange , when locale changes, a different callback gets created upon each re-render. This isn't problematic for the example above, but for callbacks passed as a prop to lower-level components, it causes extra re-renders. For function components, the useContext Hook simplifies how components consume and update data from a context provider. Let's see this in action by rewriting the application with function components calling the useContext Hook: Try it out in this CodeSandbox demo here . Creating the Context object and wrapping the entire application within the context's provider component remains the same as before. Instead of wrapping each component with a I18nContext.Consumer component, we can call a useContext Hook within the component's function body. This way, we eliminate the extra <I18nContext.Consumer /> wrapper components from the component hierarchy. Plus, we can access the context value anywhere within the component's function body, including functions defined within it and the useEffect callback. In the <Footer /> component, we can define the event handler, which calls the context's changeLocale method, outside of the rendering logic, like so: The useContext Hook accepts a single argument: a Context object (returned by the createContext() method). It returns the current context value for that specific Context. Anytime the provider's context value changes, any component subscribed to this context via useContext will re-render upon those changes. Note : If you have multiple components subscribed to the same provider, and one of those components updates the provider's value, then every one of these components will re-render upon that update. Therefore, you may want to use the useMemo Hook to avoid unnecessary, expensive re-renders. Since React's Context API serves as a state management solution for a React application's global state, most developers believe that they can substitute a library like Redux for the Context API. However, the Context API is not a one-to-one replacement for Redux. Using Context, we define the global state within a component and supply that state to the context's provider component's value prop. But with Redux, we define the global state outside of the component hierarchy within a centralized store, which holds this state. Compared to Redux, Context requires a lot less setup (no actions, reducers, etc.), but can be difficult to debug in applications with lots of deeply nested components. To learn more about the useContext Hook, check out the fifth YouTube tutorial in the six-part series on React Hooks by Paige Niedringhaus, a Blues Wireless Staff Software Engineer and author of The newline Guide to Modernizing an Enterprise React App .