Working with React context providers in Typescript
Tags: react, typescript, javascript
Published: 2020-03-19 13:48:06

**Disclaimer 📣
This post was originally a part of my [other](https://dev.to/mitchkarajohn/tips-for-migrating-a-react-app-to-typescript-4jpk) article, but, since it became quite big, I decided to break it into its own mini post.**


Say we have a simple provider that takes an [`axios`](https://github.com/axios/axios) instance as a prop and provides it to the rest of the application via context

```tsx
import React from 'react';

const AxiosContext = React.createContext(undefined);

function AxiosProvider(props) {
  const { children, axiosInstance } = props;
 
  return (
    <AxiosContext.Provider value={axiosInstance}>
      {children}
    </AxiosContext.Provider>
  );
}

const useAxios = () => React.useContext(AxiosContext);

export { AxiosProvider, useAxios };
```

So, let's write this in TS:

```tsx
import { AxiosInstance } from 'axios';
import React, { ReactNode } from 'react';

const AxiosContext = React.createContext(undefined);

export type Props = {
  children: ReactNode;
  axiosInstance: AxiosInstance;
};

function AxiosProvider(props: Props) {
  const { children, axiosInstance } = props;
 
  return (
    <AxiosContext.Provider value={axiosInstance}>
      {children}
    </AxiosContext.Provider>
  );
}

const useAxios = () => React.useContext(AxiosContext);

export { AxiosProvider, useAxios };
```

All is well now, right? We defined the `Props` type, so we are good to go. Well, not exactly. This will not work right away, because when we did 

```tsx
const AxiosContext = React.createContext(undefined);
```

we implicitly set the type of the provider value to `undefined` and thus doing

```tsx
return (
    <AxiosContext.Provider value={axiosInstance}>
```

will throw a TS error, since the value we are passing is of `AxiosInstance` type, according to our `Props` type declaration, but is also `undefined` according to the context initialisation.

To fix this we declare a new type like this

```tsx
export type ContextValue = undefined | AxiosInstance;
```

which can be further broken into

```tsx
export type ProviderValue = AxiosInstance; // since you know this is what the provider will be passing

export type DefaultValue = undefined;

export type ContextValue = DefaultValue | ProviderValue;
```

and then declare the type during the context initialisation like this:

```tsx
const AxiosContext = React.createContext<ContextValue>(undefined);
```

Now we let TS know that the context value can either be `undefined` (the default value) or an `AxiosInstance` (which is what will actually be returned by your provider). Now everything is ok then? Not yet, hang in there. 

Because, now if we use the `useAxios` hook inside another component and try to use the value it returns, we will get a TS error telling us that the return value of `useAxios` can be `undefined` since this is how we defined it when we initialised the `AxiosContext`. How do we tackle this problem? We'll take a two-pronged approach.

### A development time solution
As the programmer, we know that when we use the `useAxios` hook, the value it will return will never be `undefined`. It will always be of type `ProviderValue` since we know that we are using the hook inside a component that is a child of the `AxiosProvider` (because this is how we must use context hooks in order for them to work). 

So, the fix here is simple, and it's a type assertion. When we use the `useAxios` hook, we should always assert that its type is of `ProviderValue` like so

```tsx
import { useAxios, ProviderValue } from '<Path_to_AxiosProvider>'

function SomeComponent() {
  const axiosInstance = useAxios() as ProviderValue;
  // Do something with the axiosInstance object
}
```

and TS now knows that this is in fact an `axios` instance object.

### A runtime approach

The above solution just solves the issue during development. But what happens if a new developer comes along, who they don't know that in order to use a React context value the component using it must be a child of the  Provider component? This is a case where the assertion we made above stops being true during runtime and the whole app crashes because we try to access stuff on an `axiosInstance` that is `undefined`.

We could add a 

```tsx
if(axiosInstance === undefined) {
   throw new Error('The component using the the context must be a descendant of the context provider')
}
```

right after we do `const axiosInstance = useAxios()` but in that case the type assertion we did earlier is useless and we also need to be writing this runtime check every time we make use of `useAxios`.

The solution I've come up with for this is the following:

**Use a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) as the default context value**

Proxies are very useful in that they allow you to completely define the behaviour of a proxied object.

To elaborate, remember how we initialise our context, currently:

```tsx
const AxiosContext = React.createContext<ContextValue>(undefined);
```

So, what if instead of `undefined` we initialised the context with a `Proxy` of a random `axios` instance object? like so

```tsx
const AxiosContext = React.createContext<ContextValue>(
  new Proxy(axios.create())
);
```

Our types definition can now also change to this:

```tsx
type ProviderValue = AxiosInstance; 

type DefaultValue = AxiosInstance;

type ContextValue = DefaultValue | ProviderValue;
```

But this is still not enough. We want the app to throw in case the default context is used, with an appropriate error message (and we do not want to do this check every time we use the `useAxios` hook, because we are lazy)

So, we simply define what we want to happen if the application code tries to access any members of this proxied `axios` instance that we return as a default context value:

```tsx
const AxiosContext = React.createContext<ContextValue>(
  new Proxy(axios.create(), {
    apply: () => {
      throw new Error('You must wrap your component in an AxiosProvider');
    },
    get: () => {
      throw new Error('You must wrap your component in an AxiosProvider');
    },
  })
);
```

`apply` handles the behaviour when we try to call any methods from the proxied `axios` instance object and `get` handles the behaviour when we try to access any of its properties.

## In conclusion

With the above approach we both keep Typescript satisfied and we also need to write the least code possible: Just a type assertion when we use the context hook and define the default context value as a proxy which throws if any code tries to access it.

Thanks for reading! 🎉