Post-mortem analysis: How I deployed a breaking change due to React Context
Published on Thursday, Mar 14, 2024
The Context API in React is a really useful tool for frontend developers who want to handle state within components and easily share data across their projects. But if you’re not careful with how you use it, you might run into some tricky bugs that could mess up your site. Let’s take a closer look at how that could happen.
What happened?
One day, I opted to use useContext()
to gain access to a field for decide what content to
display within a component. Initially, everything seemed smooth sailing—no errors emerged, and
the changes were approved. Consequently, I pushed these alterations into production. However, an
hour later, we received complaints from multiple users encountering error pages. Swiftly, my team
and I started an investigation to uncover the root cause. As it turned out, my changes were
responsible for the issue. The culprit? The utilization of useContext()
within my changes.
How did it happen?
As many React developers may know, you will see some errors coming if you attempt to use
useContext()
outside of the specified Context. I was really confused when that
happened, as I was certain the Provider was a parent of the component that had the changes.
That is, until I figured out that this component was used in more than one location.
Let’s take a look at this example:
import { useTodos } from '../../TodosProvider'
...
export const TodosHeader = ({ title, description }: Props) => {
const { isTodoCustom } = useTodos()
...
return (
<div>
{!isTodoCustom && <div>...</div>}
<div>...</div>
</div>
)
}
The useTodos()
hook is a wrapper around useContext()
, like so:
export const useTodos = () => {
const ctx = useContext(TodosContext)
return ctx
}
At first you’re probably thinking that there’s nothing wrong with this code. That is correct!
But what if <TodosHeader />
being used in another location and that the other location’s
filename was called Reminders.tsx
? If you were thinking that there’s still isn’t
enough info then you are also correct. At this point, I thought the same.
The missing piece of the puzzle is that Reminders.tsx
did not have the <TodosProvider />
wrapped around the location that used <TodosHeader />
in it’s render function. It didn’t
cross my mind when I made those changes, despite my best efforts to smoke test it. You could
argue that it’s very easy problem to avoid, but even small mistakes can get the best of us.
How can we prevent this?
There’s a couple of things we could do to prevent this but first let’s remind ourselves of the basics of React Context, particularly the most important detail: only children components of the Context can use it.
This means we have to write our codebase in such a way that we can understand whether it’s safe to use a context in component. We can do the following:
- Ensure all usages of
useContext()
are not abstracted to the point where you can’t differentiate it between regular hooks
Whenever we useContext()
there might be cases where we want to wrap useContext()
. It is best
to either avoid this or to make sure you append “Context” at the end of the hook name, e.g.
useUserContext()
instead of useUser()
.
- Define created Context with an
undefined
/null
value when usingcreateContext
This is also related to the previous point. By combining this and the previous advice, we can add more safety to prevent context hooks being used outside of their respective provider. Take a look at this example:
interface TodosContextType {
tasks: Tasks[]
...
}
const TodosContext = createContext<TodosContextType | undefined>(undefined)
export const useTodosContext = () => {
const ctx = useContext(TodosContext)
if (!ctx) {
throw new Error('useTodosContext must be used within TodosProvider.')
}
return ctx
}
In this example, we’re initialising TodosContext
to be undefined
first so that we can tell
whether a component consuming this context hook will be wrapped by a TodosContext
. If it wasn’t,
no error will show up until you use a value that doesn’t exist on the Provider, or even worse, a
defined value. This would definitely give me headaches trying to debug. Keep it mind it
wouldn’t completely solve the issues I mentioned earlier, as it was an edge case I forgot to check.
This stil minimises the potential of other errors showing means it’s easier to locate the errors,
so it’s still very helpful!
- Change components that are used in more than one location into a display component
A display component is a component that are stateless and are only concerned about how it’s viewed. In most cases, if a component is used in multiple spots it would likely have different behavior depending on where it’s used, despite having the same look. This also makes it very predictable if it’s stateless, which also means you won’t need to test this component as often as others.
Final Words
It may be a small mistake and easily avoidable but sometimes it’s small things that cause your web app to break and cost your company. A key takeaway from this is that you should find ways to prevent these kinds of mistakes, be it making your code reviews stricter, enforcing a style guide or maybe even just reading and being aware of these kinds of mistakes. I hope this helps you understand how dangerous it can be to misuse React Context and that my tips would be useful. If you got this far, thanks for taking the time to read my post!