How to use useContext in functional components?

December 04, 2020

A post by Will Mayger
Twitter . Instagram

In this article we will cover the basics of using context, how to use useContext in functional components, when to use it, when not to use it and things you should be aware of.

The plan here is to explain this topic in a way that allows everyone, no matter what your skill level, to be able to follow along, learn and understand useContext.

The react hook useContext is called inside of functional components and is used to gain access to a context that has been set somewhere upstream of the react component.

You can’t use useContext in a class component because useContext is a react hook, using context in class components is too big of a topic to cover in this post, so for the time being here is the official guide for using context with class components.

With that said, we do have to have a basic understanding of how contexts work in React before we can carry on.

You can think of a context as a kind of parent state that will contain values that the all child components can access, and make use of without having to pass this down as props through all of the components.

Before we get started with using context with hooks there are a few basic concepts that we need to know about.

First we have a context provider that is going to be where we store our data and provide it’s child components the context.

The context provider will look something like this:

import React from "react"

const userContextTemplate = {
  userName: "JohnSnow",
}

export const UserContext = React.createContext(userContextTemplate)

export default function ProviderComponent({ children }) {
  return (
    <UserContext.Provider value={userContextTemplate}>
      {children}
    </UserContext.Provider>
  )
}

Then we will need to have a consumer, which is something that accesses and uses the context given by the provider.

With react hooks that will look something like the following:

import React, { useContext } from "react"
import { UserContext } from "./provider"

export default function ConsumerComponent() {
  const context = useContext(UserContext)
  return <p>Hello {context?.userName}</p>
}
Infographic showing basic concepts of how to use useContext in functional components

The provider will create a context and pass it down to all of its child components. Any child component that chooses to use the context can do so by using the consumer component or using the react hook, these child components are called consumers.

First steps of how to use useContext in functional components

Okay, let’s get started on looking at how you can begin using the react hook useContext in function components.

Before we do, just very quickly we need to take a look at how a functional component and a class component both look.

Here is a class component:

export class MyClassComponent extends React.Component {
  render() {
    return <p>This is a Class Component</p>
  }
}

Here is a functional component:

export default function MyFunctionalComponent() {
  return <p>This is a Functional Component</p>
}

We aren’t going to cover class components at all here because that would be a much larger topic and deserves its own article, regardless, the principles of using the context will be similar (provider => consumer).

Also, when I say using useContext in functional components that just means the react hook useContext is called in functional components rather than using the consumer element.

Okay, now we have got that out of the way let’s take a look at how the useContext hook works and how we can use it in our functional component.

Firstly we need to think about what we want from our context and what data it is going to need, for the purposes of this article we are going to use a user object that many components need to be able to access.

In our user object, we are going to have a name, an email, and a status.

const userObjectContext = {
  name: "John Snow",
  email: "john.snow@thewall.north",
  status: "Winter is coming",
}

Now we know how our user object is going to look, we need to create a context and use our object as a template.

import React from "react"

const userObjectContext = {
  name: "John Snow",
  email: "john.snow@thewall.north",
  status: "Winter is coming",
}

export const UserContext = React.createContext(userObjectContext)

As you can see, that wasn’t too bad! We are using the React.createContext function along with our userObjectContext object to create a basic context that is ready to be put into our functional components.

It is important to note that we are adding the context as an export so we can later use this in our hook.

The next step is to create our provider.

import React from "react"

const userObjectContext = {
  name: "John Snow",
  email: "john.snow@thewall.north",
  status: "Winter is coming",
}

export const UserContext = React.createContext(userObjectContext)

export default function ProviderComponent({ children }) {
  const [context] = useState(userObjectContext)

  return <UserContext.Provider value={context}>{children}</UserContext.Provider>
}

As you can see we have created a state object that passes the actual context values into the provider. We will cover why we are doing this a little later on, for right now, you only need to know that we are passing the user object into the provider’s value prop.

This is our context now created and ready to be consumed!

Let’s create a basic consumer component that uses some of the user details.

import React from "react"

export default function UserDetails({ name, email, status }) {
  return (
    <div>
      <p>Name: {name}</p>
      <p>Email: {email}</p>
      <p>Status: {status}</p>
    </div>
  )
}

As you can see this is pretty basic and we are currently expecting props just like normal when using React.

But we want to get this information from the context, we don’t want to pass it down as props.

The next step then is to use the useContext react hook to access and consume the context we created so that we can get that information.

import React, { useContext } from "react"
import { UserContext } from "./provider"

export default function UserDetails() {
  const context = useContext(UserContext)
  return (
    <div>
      <p>Name: {context?.name}</p>
      <p>Email: {context?.email}</p>
      <p>Status: {context?.status}</p>
    </div>
  )
}

Okay great, we are now consuming the context!

The only changes we had to make to our component were to remove the props, import the context we created and then add it into our useContext hook to get the data.

And there we have our basic provider and consumer using useContext with functional components.

Next, we need to look at how we can pass a function into our React context.

How to pass a function in react context then read and use it with useContext

Learning how to pass a function through react context and then use it with useContext is a prerequisite to many things that you may end up needing to do.

For example you need this to be able to update, and make asynchronous calls with your context you will need to know how to pass in a function.

Thankfully though, it is pretty much as straightforward as you might expect.

Let’s assume that we want to add a function that will display the time of the user.

First, let’s look at how we can add a function to our user object template. We will assume that it is just a template and that we will want to add an actual function afterward.

const functionTemplate = () => {}

const userObjectContext = {
  name: "John Snow",
  email: "john.snow@thewall.north",
  status: "Winter is coming",
  logTime: functionTemplate,
}

As you can see we are creating the template with a basic empty function that returns undefined (nothing / void) by using arrow notation.

We have created this arrow function as a variable, and the reason for this is due to something called referential equality which we will get into more detail on later along with why we need this to be stored in a variable.

The next thing we need to do is to add a function into the provider.

import React from "react"

const functionTemplate = () => {}

const userObjectContext = {
  name: "John Snow",
  email: "john.snow@thewall.north",
  status: "Winter is coming",
  logTime: functionTemplate,
}

export const UserContext = React.createContext(userObjectContext)

export default function ProviderComponent({ children }) {
  const [context] = useState({
    ...userObjectContext,
    logTime: () => {
      console.log("02:15am on the 11th of November in the year 900")
    },
  })

  return <UserContext.Provider value={context}>{children}</UserContext.Provider>
}

This function is now accessible to the context and can be used by any consumer.

Here is how we might go about using this function in our functional component that consumes the context via useContext.

import React, { useContext } from "react"
import { UserContext } from "./provider"

export default function UserDetails() {
  const context = useContext(UserContext)
  return (
    <div>
      <p>Name: {context?.name}</p>
      <p>Email: {context?.email}</p>
      <p>Status: {context?.status}</p>
      <button onClick={context?.logTime}>Log time</button>
    </div>
  )
}

In this step we have simplified a lot of things and even done a few things that aren’t truly necessary, but it is solely for the purpose of explaining how you can pass a function into your context and it gets across some important ideas.

  1. How to create your context template with a dummy function
  2. How you can provide the real function to your context
  3. How to use and consume the function.

How to update contexts with useContext in functional components

Now we know how to add a basic function into our context, we need to know how to use this to be able to update our context.

As you might have known, there is no updateContext react hook. You can only ever consume the context.

What this means is that we have to handle updating it ourselves.

We can do this by leveraging functions in our context along with useState and useContext.

Let’s take a look at our past example again where we add a function into the context.

import React from "react"

const functionTemplate = () => {}

const userObjectContext = {
  name: "John Snow",
  email: "john.snow@thewall.north",
  status: "Winter is coming",
  logTime: functionTemplate,
}

export const UserContext = React.createContext(userObjectContext)

export default function ProviderComponent({ children }) {
  const [context] = useState({
    ...userObjectContext,
    logTime: () => {
      console.log("02:15am on the 11th of November in the year 900")
    },
  })

  return <UserContext.Provider value={context}>{children}</UserContext.Provider>
}

This function is currently just console logging the time, but let’s change it up a little and get it to update the users status.

To do this we are going to need to be able to change the state of the context itself. Here we are going to tie together a lot of the pieces of the puzzle from what we did earlier regarding the function.

Let’s look at the code that enables us to be able to update the context in our provider.

import React from "react"

const functionTemplate = () => {}

const userObjectContext = {
  name: "John Snow",
  email: "john.snow@thewall.north",
  status: "Winter is coming",
  updateStatus: functionTemplate,
}

export const UserContext = React.createContext(userObjectContext)

export default function ProviderComponent({ children }) {
  const [context, setContext] = useState(userObjectContext)

  const updateContext = (contextUpdates = {}) =>
    setContext(currentContext => ({ ...currentContext, ...contextUpdates }))

  useEffect(() => {
    if (context?.updateStatus === functionTemplate) {
      updateContext({
        updateStatus: value => updateContext({ status: value }),
      })
    }
  }, [context?.updateStatus])

  return <UserContext.Provider value={context}>{children}</UserContext.Provider>
}

Now it is going to become a little clearer as to why we needed to store our functionTemplate in a variable.

As you can see we are using that variable to check to see if our context update function has already been updated or not.

This would not be possible without referencing the function as a variable because of referential equality.

What this means is that in JavaScript you can’t compare certain things as you might expect because they do not actually match each other.

This means you can compare two equal numbers but you can’t compare two equal functions.

When it comes to functions, being identical is not enough, it needs to be the exactly the same, litterally, it needs to be the same actual function in order to compare it.

To be able to do that you need to reference the function declaration rather than providing the same function.

Here is an example of this:

// Does not have referential equality
() => {} === () => {} // false
// Does have referential equality
const a = () => {}
a === a // true

Now, back to our context example, what is happening here is we are giving our context the ability to update itself by adding in an update function that can be used with any property in the context object.

To break this down a little more, we have our two values that come back from useState, the value and the setter function.

At this point, there is no way to update the context because we have only just been provided with the function to be able to update it, so we now need to add it in.

To be able to add it in and make it re-usable we create a function called updateContext which is a fairly general use function that accepts any object and then merges the object with the existing context. We can make this specific for each property in the context.

The updateContext function will prioritize new object properties over the old object (new > old) and add any new properties and replace any existing properties with the new versions.

We then run a useEffect hook to allow us to inject our update functions into the context itself.

The reason why we can’t just add it into the context value prop is because it could cause the entire context to be re-rendered whenever this component gets rendered because it would always be creating a new object.

Similar to a function, an identical object would return false when compared to itself, it has to actually be the same object for it to return true.

To avoid this we let the object be defined within the state and let that state be passed directly into the context.

Okay so we now have a context, with an injected function that allows us to update the context.

The only thing remaining is to create the ability in the consumer to use this function.

import React, { useContext, useState } from "react"
import { UserContext } from "./provider"

export default function UserDetails() {
  const [nextStatus, setNextStatus] = useState("")
  const context = useContext(UserContext)

  const handleStatusChange = e => {
    e.preventDefault()
    setNextStatus(e.target.value)
  }

  const handleSubmit = e => {
    e.preventDefault()
    context?.updateStatus(nextStatus)
    setNextStatus("")
  }

  return (
    <div>
      <p>Name: {context?.name}</p>
      <p>Email: {context?.email}</p>
      <p>Status: {context?.status}</p>
      <form onSubmit={handleSubmit}>
        <input
          name="nextStatus"
          value={nextStatus}
          onChange={handleStatusChange}
        />
        <button type="submit">Update status</button>
      </form>
    </div>
  )
}

And there we have a working update function that will update the users status!

Gif showing updating context in react with useContext and functional components

Next, we need to look at how we can use this with an asynchronous call, because for the most part, we aren’t going to have the user details hardcoded like we have up to this point.

How to handle asynchronous calls with useContext

This is the last part of actually using the useContext hook in functional components and it is a pretty important step.

If you are sharing data across many components there is a big chance that it has come from an external source so it is important to know how to make an asynchronous call to get data and then update the context with it.

Let’s start by looking at the function that will make our asynchronous call for us to get the user data.

export const fetchUser = async () => {
  try {
    const res = await fetch("https://got-example.com/api/v1/user/102")
    return res.json()
  } catch (e) {
    console.error(e)
    return {
      name: "Not found",
      email: "Not found",
      status: "Not found",
    }
  }
}

As you can see this is a pretty standard async function that is going to return the users data.

Now all we really need to do is to combine this with our context. We can do this by making use of the function we created earlier called updateContext.

Because of how the function will merge the new object data into the old context data (new > old), the only thing we need to do is pass the new data into that function.

We want to make sure we only do this once, when the component is first rendered. To do this we have a variety of options available to us, you can read about all of the options here.

We are going to need to use another useEffect hook and then update the context with the async result inside of it.

For this instance we achieve the componentDidMount effect by using an empty dependency array because we won’t need or use any dependencies.

Now for those who have worked and have experience with the dependency array of useEffect, you might be thinking, wait what about the updateContext function, isn’t that a dependency?

And you would be right, it would be a dependency if we used it, however for simplicity we are not going to use it, instead we are going to create another version of it inside the useEffect so we can avoid having any dependencies within the array.

So here is how this will look:

import React, { useState, useEffect } from "react"

const functionTemplate = () => {}

const userObjectContext = {
  name: "John Snow",
  email: "john.snow@thewall.north",
  status: "Winter is coming",
  updateStatus: functionTemplate,
}

export const UserContext = React.createContext(userObjectContext)

export const fetchUser = async () => {
  try {
    const res = await fetch("https://got-example.com/api/v1/user/102")
    return res.json()
  } catch (e) {
    console.error(e)
    return {
      name: "Not found",
      email: "Not found",
      status: "Not found",
    }
  }
}

export default function ProviderComponent({ children }) {
  const [context, setContext] = useState(userObjectContext)

  const updateContext = (contextUpdates = {}) =>
    setContext(currentContext => ({ ...currentContext, ...contextUpdates }))

  useEffect(() => {
    const populateContext = (contextUpdates = {}) =>
      setContext(currentContext => ({ ...currentContext, ...contextUpdates }))

    async function fetchData() {
      const user = await fetchUser()
      populateContext(user)
    }

    fetchData()
  }, [])

  useEffect(() => {
    if (context?.updateStatus === functionTemplate) {
      updateContext({
        updateStatus: value => updateContext({ status: value }),
      })
    }
  }, [context?.updateStatus])

  return <UserContext.Provider value={context}>{children}</UserContext.Provider>
}

Now whenever this code is run, it will only run once, and when our async call resolves it will then update the context which will propagate down and update all consumers of the context (any component using the useContext hook).

And there we have a working asynchronous call with useContext!

That is about everything we will cover in terms of a step by step guide in this article but, the next few things are very important so make sure you don’t skip over them!

When should you use useContext in functional components?

Sparingly, generally speaking it is better to avoid using context unless absolutely it is absolutely essential.

1. It makes your code more complex and confusing.

Context is a fairly complex thing and it is for the most part hidden behind the scenes which makes it difficult to track down and debug when compared to drilling props.

This means that people who end up looking at your code, when using context, may have a hard time trying to work out what is going on and it can take longer to get used to how it works.

2. It can cause more rerenders

Using context along with the useContext hook will cause those components to rerender whenever the context gets updated.

That is fine if all of the renders are intentional, but if you have many child components using the context and all of them get re-rendered every time the context changes that can start to cause issues.

You may think that you can solve this by adding in more contexts but then you are adding more confusion into your codebase which is also not ideal.

To top things off, context will ignore things like React.memo and will still cause renders.

3. It will make reusing components more difficult

If you consume a context in a component, that context becomes a requirement of that component. Whilst there is nothing inherently wrong with that, it does mean you won’t be able to re-use that component as much as you may like.

This can be solved by careful consideration of what components you want to re-use and which ones you don’t want to re-use.

4. It is not ideal for state management There are many libraries out there to help you with state management that can be implemented in a similar way, that have been tested and have solutions to problems that you might not even be aware of.

For example, using redux with react hooks is a very easy and clean way to manage state that allows you to access and update global values with ease.

On top of this it also provides you with the ability to add middlewares such as redux sagas to handle async calls that populate your state.

Summarising

All of those points sound fairly negative and against using useContext directly, but it is important to understand that you should always be careful before implementing a new tool in your code.

With all of that said, context and useContext is a massively useful thing and when used correctly can be really powerful and useful.

Summary

Okay that about sums up how to use useContext in function components, for the next steps I suggest you take the examples in this article and practice using them.

I would suggest implementing a new function that lets you update the user’s name!

I hope you enjoyed this article and it has given you clarity on how to use useContext in functional components!

Will

Image icon made by Freepik from www.flaticon.com