Skip to content

Commit 1a052de

Browse files
Add custom hook for context to avoid assertions (typescript-cheatsheets#604)
1 parent 61f5609 commit 1a052de

File tree

2 files changed

+195
-485
lines changed

2 files changed

+195
-485
lines changed

README.md

+97-242
Original file line numberDiff line numberDiff line change
@@ -1504,296 +1504,151 @@ Sources:
15041504

15051505
#### Context
15061506

1507-
#### Basic Example
1507+
#### Basic example
1508+
1509+
Here's a basic example of creating a context containing the active theme.
15081510

15091511
```tsx
15101512
import { createContext } from "react";
15111513

1512-
interface AppContextInterface {
1513-
name: string;
1514-
author: string;
1515-
url: string;
1516-
}
1514+
type ThemeContextType = "light" | "dark";
15171515

1518-
const AppCtx = createContext<AppContextInterface | null>(null);
1519-
1520-
// Provider in your app
1516+
const ThemeContext = createContext<ThemeContextType>("light");
1517+
```
15211518

1522-
const sampleAppContext: AppContextInterface = {
1523-
name: "Using React Context in a Typescript App",
1524-
author: "thehappybug",
1525-
url: "http://www.example.com",
1526-
};
1519+
Wrap the components that need the context with a context provider:
15271520

1528-
export const App = () => (
1529-
<AppCtx.Provider value={sampleAppContext}>...</AppCtx.Provider>
1530-
);
1521+
```tsx
1522+
import { useState } from "react";
15311523

1532-
// Consume in your app
1533-
import { useContext } from "react";
1524+
const App = () => {
1525+
const [theme, setTheme] = useState<ThemeContextType>("light");
15341526

1535-
export const PostInfo = () => {
1536-
const appContext = useContext(AppCtx);
15371527
return (
1538-
<div>
1539-
Name: {appContext.name}, Author: {appContext.author}, Url:{" "}
1540-
{appContext.url}
1541-
</div>
1528+
<ThemeContext.Provider value={theme}>
1529+
<MyComponent />
1530+
</ThemeContext.Provider>
15421531
);
15431532
};
15441533
```
15451534

1546-
You can also use the [Class.contextType](https://reactjs.org/docs/context.html#classcontexttype) or [Context.Consumer](https://reactjs.org/docs/context.html#contextconsumer) API, let us know if you have trouble with that.
1547-
1548-
_[Thanks to @AlvSovereign](https://github.com/typescript-cheatsheets/react/issues/97)_
1535+
Call `useContext` to read and subscribe to the context.
15491536

1550-
#### Extended Example
1537+
```tsx
1538+
import { useContext } from "react";
15511539

1552-
Using `createContext` with an empty object as default value.
1540+
const MyComponent = () => {
1541+
const theme = useContext(ThemeContext);
15531542

1554-
```tsx
1555-
interface ContextState {
1556-
// set the type of state you want to handle with context e.g.
1557-
name: string | null;
1558-
}
1559-
// set an empty object as default state
1560-
const Context = createContext({} as ContextState);
1561-
// set up context provider as you normally would in JavaScript [React Context API](https://reactjs.org/docs/context.html#api)
1543+
return <p>The current theme is {theme}.</p>;
1544+
};
15621545
```
15631546

1564-
Using `createContext` and [context getters](https://kentcdodds.com/blog/application-state-management-with-react/) to make a `createCtx` with **no `defaultValue`, yet no need to check for `undefined`**:
1547+
#### Without default context value
15651548

1566-
```ts
1567-
import { createContext, useContext } from "react";
1549+
If you don't have any meaningful default value, specify `null`:
15681550

1569-
const currentUserContext = createContext<string | undefined>(undefined);
1551+
```tsx
1552+
import { createContext } from "react";
15701553

1571-
function EnthusiasticGreeting() {
1572-
const currentUser = useContext(currentUserContext);
1573-
return <div>HELLO {currentUser!.toUpperCase()}!</div>;
1554+
interface CurrentUserContextType {
1555+
username: string;
15741556
}
15751557

1576-
function App() {
1558+
const CurrentUserContext = createContext<CurrentUserContextType | null>(null);
1559+
```
1560+
1561+
```tsx
1562+
const App = () => {
1563+
const [currentUser, setCurrentUser] = useState<CurrentUserContextType>({
1564+
username: "filiptammergard",
1565+
});
1566+
15771567
return (
1578-
<currentUserContext.Provider value="Anders">
1579-
<EnthusiasticGreeting />
1580-
</currentUserContext.Provider>
1568+
<CurrentUserContext.Provider value={currentUser}>
1569+
<MyComponent />
1570+
</CurrentUserContext.Provider>
15811571
);
1582-
}
1572+
};
15831573
```
15841574

1585-
Notice the explicit type arguments which we need because we don't have a default `string` value:
1575+
Now that the type of the context can be `null`, you'll notice that you'll get a `'currentUser' is possibly 'null'` TypeScript error if you try to access the `username` property. You can use optional chaining to access `username`:
15861576

1587-
```ts
1588-
const currentUserContext = createContext<string | undefined>(undefined);
1589-
// ^^^^^^^^^^^^^^^^^^^^^^^
1577+
```tsx
1578+
import { useContext } from "react";
1579+
1580+
const MyComponent = () => {
1581+
const currentUser = useContext(CurrentUserContext);
1582+
1583+
return <p>Name: {currentUser?.username}.</p>;
1584+
};
15901585
```
15911586

1592-
along with the non-null assertion to tell TypeScript that `currentUser` is definitely going to be there:
1587+
However, it would be preferrable to not have to check for `null`, since we know that the context won't be `null`. One way to do that is to provide a custom hook to use the context, where an error is thrown if the context is not provided:
15931588

1594-
```ts
1595-
return <div>HELLO {currentUser!.toUpperCase()}!</div>;
1596-
// ^
1597-
```
1598-
1599-
This is unfortunate because _we know_ that later in our app, a `Provider` is going to fill in the context.
1600-
1601-
There are a few solutions for this:
1602-
1603-
1. You can get around this by asserting non null:
1604-
1605-
```ts
1606-
const currentUserContext = createContext<string>(undefined!);
1607-
```
1608-
1609-
([Playground here](https://www.typescriptlang.org/play?jsx=1#code/JYWwDg9gTgLgBAKjgQwM5wEoFNkGN4BmUEIcARFDvmQNwBQduEAdqvLgK5SXMwCqqLFADCLGFgAe8ALyYqMAHS5KycaN6SYAHjZRgzAOYA+ABQdmAEywF9WCwEIAlPQLn8wFnACivABYdUYDQYYFwAcUosEMMTRzgAbzo4OCZWdi4efkEoOFlsPEUArHVxKRNObixeASESzWckuEoYLmY4LQtgADcjAAkvABkBgHkEisyaqAUYCD4wMFq0LFiAX3stAHpOnvoVuldmd08AQXnYhMbm1vbxqqzasU0FAAViLuArHK7kABsOLGkZAAyr5kAB3ZhkIyNZJaHwwfyBYKhCJYKL6AxwDbQ2EbW7VbJ1KQvN4fIRGXZAA)) This is a quick and easy fix, but this loses type-safety, and if you forget to supply a value to the Provider, you will get an error.
1610-
1611-
2. We can write a helper function called `createCtx` that guards against accessing a `Context` whose value wasn't provided. By doing this, API instead, **we never have to provide a default and never have to check for `undefined`**:
1612-
1613-
```tsx
1614-
import { createContext, useContext } from "react";
1615-
1616-
/**
1617-
* A helper to create a Context and Provider with no upfront default value, and
1618-
* without having to check for undefined all the time.
1619-
*/
1620-
function createCtx<A extends {} | null>() {
1621-
const ctx = createContext<A | undefined>(undefined);
1622-
function useCtx() {
1623-
const c = useContext(ctx);
1624-
if (c === undefined)
1625-
throw new Error("useCtx must be inside a Provider with a value");
1626-
return c;
1627-
}
1628-
return [useCtx, ctx.Provider] as const; // 'as const' makes TypeScript infer a tuple
1629-
}
1630-
1631-
// Usage:
1632-
1633-
// We still have to specify a type, but no default!
1634-
export const [useCurrentUserName, CurrentUserProvider] = createCtx<string>();
1635-
1636-
function EnthusiasticGreeting() {
1637-
const currentUser = useCurrentUserName();
1638-
return <div>HELLO {currentUser.toUpperCase()}!</div>;
1639-
}
1640-
1641-
function App() {
1642-
return (
1643-
<CurrentUserProvider value="Anders">
1644-
<EnthusiasticGreeting />
1645-
</CurrentUserProvider>
1646-
);
1647-
}
1648-
```
1649-
1650-
[View in the TypeScript Playground](https://www.typescriptlang.org/play?jsx=1#code/JYWwDg9gTgLgBAKjgQwM5wEoFNkGN4BmUEIcARFDvmQNwBQdA9AgnYnAIJwAWWANmCxQ4MCHFyVkMLCjgBhCADtpAD3jJFAEzgAFYgDdgmoXADuwGNziKxAVzBEl8YwWS2+8fcj62sAGhQtNiRzSwhbeG5kQ0UAcxExXF5cAGs4Amg4Wy0sAmBFLG1vPhFeEVAsADpgxjoCbPxgJXFJaTkYFQAeLiw1LC10AG8AXzgAH2t3PgA+AAoASjhBtnElVHh8FTgAXkwqGEqJHDanXphu8aycvILNOeyXfML5+jh0hpgmxSzULHaVBZLFZvXBrDY7PZ4A62X4KZRnWabF7AuDAAhwRE7ba7B65J6aRaWYimaxYEkAUSgxCgszIML+HTgIBh8AARjJ8qgjDJkLoDNzhKErLyvD4sGRkW83pQYLYoN9cK84MMVjK5d8ANr0-4BTaVPQQQzGKAAXRQ6FBinWNDgjEYcAA5GhVlaYA6mcgUlh0AAVACeggAyhJgGB4PkCCZebKwHwsHQVUx7QBVVDIWJYABcDDtcAA6jJ1sA+CUovoZKI4KhBLg0X7ZDAA-44KyItYxC43B4AIR0XqQWAu9ZwLWwuWUZSpoQAOWQIGbcnH-RgU6gBqNQjNuyOUgZXXWUHysTmyLqHy+cHJym4MOAaE+uAA4pQsJ84oDliCweIl5PfsIcTHKll1XWd5wWJU1XlOBOk0YB9GmAAJckABkUIAeSWXBfxXf9KlEZMwEEKA5DQLAFmGbtOkYOCEPoRN6kURpmg4IiP1VV91RgxdgL-IR1wFOBRV8bYyEDKJTEUMhphRTor0sW972AJ8XzfeJGBkt5qJ4idcP4-ljWmeigA)
1651-
1652-
3. You can go even further and combine this idea using `createContext` and [context getters](https://kentcdodds.com/blog/application-state-management-with-react/).
1653-
1654-
```tsx
1655-
import { createContext, useContext } from "react";
1656-
1657-
/**
1658-
* A helper to create a Context and Provider with no upfront default value, and
1659-
* without having to check for undefined all the time.
1660-
*/
1661-
function createCtx<A extends {} | null>() {
1662-
const ctx = createContext<A | undefined>(undefined);
1663-
function useCtx() {
1664-
const c = useContext(ctx);
1665-
if (c === undefined)
1666-
throw new Error("useCtx must be inside a Provider with a value");
1667-
return c;
1668-
}
1669-
return [useCtx, ctx.Provider] as const; // 'as const' makes TypeScript infer a tuple
1670-
}
1671-
1672-
// usage
1673-
1674-
export const [useCtx, SettingProvider] = createCtx<string>(); // specify type, but no need to specify value upfront!
1675-
export function App() {
1676-
const key = useCustomHook("key"); // get a value from a hook, must be in a component
1677-
return (
1678-
<SettingProvider value={key}>
1679-
<Component />
1680-
</SettingProvider>
1681-
);
1682-
}
1683-
export function Component() {
1684-
const key = useCtx(); // can still use without null check!
1685-
return <div>{key}</div>;
1686-
}
1687-
```
1688-
1689-
[View in the TypeScript Playground](https://www.typescriptlang.org/play/?jsx=2#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wFgAoCtCAOwGd4BXOpAYWZlwAkIIBrOAF44ACj5IAngC44DKMBoBzAJRCAfHADeFOHGr14AbQYoYSADSykMAMoxTSALpDExGADpmSOw5GaAvso6cEQwjFA0svZmhuISjhT+FAD0yXpEDnq0ZgAe8ADuwDAAFnA0EHCMYNjZcAAmSJgojAA2MABqKC2MSClphSUQjPDFKABuCopwnPUVjDQNmApIdXrFSGgCXS3T69OgveSY8xjAtOmoZqwwOQA8AIJqIqra5Lr6DHo3LsjoHmgZK7ZJB5B5wAA+lQWjWWdSe80WsOUAG5gscaKdzl5rjlnlpgu9aJ80D83J4WKxgXkRBgciiCXBgJhRABCNCqEo4fJlJDcgCiUBwUBEACJsd8QBw4AAjJCM+jABpwFBwAAKOAmDSgcAGpRVYy6PRF9LeuhC1nCkTQqNNSVNoUtcEM4pyllp7nVEE1SCgzhQdCyBmRcFScBAKHEcAAKhIwN4AcAwPAFJgfcrplUWhYyhB4ChIihBSgJHAIMz5mdIjBY0g6IkKH1KnQUIpDhQQZBYIHPs6KTdLDZrDBJp7vb6XADLmwbrc5JMniiQ2k6HG0EyS9W45ZpcMczyVtMKiuNuu4AbunKqjUaDAWe2cp2sCdh+d7mAwHjXoSDHA4i5sRw3C8HwopxMawahq2eZnoaco1HgKrFMBliSp8sryum1DgLQSA3sEDoRKIDK3IOMDDkoo6Kmm549IImhxP4agMrotyUthNC4fAyRMaaLHJKR5GKJRWo8boJp2h20BPhiL6RGxkAcTen7BB88B-sILrPBBaRoPmUTAC0OxeDqRRIbuNCtDsaDrJsd72hahG3HUwBjGo9GSP4tzJM5rk2v4QA)
1690-
1691-
4. Using `createContext` and `useContext` to make a `createCtx` with [`unstated`](https://github.com/jamiebuilds/unstated)-like context setters:
1692-
1693-
```tsx
1694-
import {
1695-
createContext,
1696-
Dispatch,
1697-
PropsWithChildren,
1698-
SetStateAction,
1699-
useState,
1700-
} from "react";
1701-
1702-
export function createCtx<A>(defaultValue: A) {
1703-
type UpdateType = Dispatch<SetStateAction<typeof defaultValue>>;
1704-
const defaultUpdate: UpdateType = () => defaultValue;
1705-
const ctx = createContext({
1706-
state: defaultValue,
1707-
update: defaultUpdate,
1708-
});
1709-
1710-
function Provider(props: PropsWithChildren<{}>) {
1711-
const [state, update] = useState(defaultValue);
1712-
return <ctx.Provider value={{ state, update }} {...props} />;
1713-
}
1714-
return [ctx, Provider] as const; // alternatively, [typeof ctx, typeof Provider]
1715-
}
1716-
1717-
// usage
1718-
import { useContext } from "react";
1719-
1720-
const [ctx, TextProvider] = createCtx("someText");
1721-
export const TextContext = ctx;
1722-
export function App() {
1723-
return (
1724-
<TextProvider>
1725-
<Component />
1726-
</TextProvider>
1727-
);
1728-
}
1729-
export function Component() {
1730-
const { state, update } = useContext(TextContext);
1731-
return (
1732-
<label>
1733-
{state}
1734-
<input type="text" onChange={(e) => update(e.target.value)} />
1735-
</label>
1736-
);
1737-
}
1738-
```
1739-
1740-
[View in the TypeScript Playground](https://www.typescriptlang.org/play/?jsx=2#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wFgAoCpAD0ljkwFcA7DYCZuNIlGJAYRjUAPAEEAfAAoAJkkwpGAGxgA1FIsZIAXHFEBKOAG8KcODACeYJHACqYabyQAVS9YC8iYjAB0AEWAAzmC8aAAWwsjoPgDKSDDRMI6ibBzCFlYQmHCy8kqq6pri4gDcJlwcAfA5Csp2Dnw6dY4uVnAekgZu4tlyNfkaSKXkpmgV8BjUbZ5R3tyofPwcfNQwksbDpnCVjjrVeWoDADRlpoz2Oz25ted8ZQC+ekOmTKww7JwACjgAbsCyUJIwDgwAEdJEMN4vhAQQB1YAwUL8ULARTSIjMYSGO7iAzrTblZiVOAAbW2fEOcDO9SQAF0puCfIwAkgEo4ZL19gUkI8TnAiDBGFBOMIJpCfn8kFA4N8uW5DIYtolyZSbtY7ncjN4tUDoQENQB6Er3Mr8wWcYkTClQ37-OkoAIEyrFOD6-VwdR8IW8YDfJCKcwU4npJCZLhCCnB0PWiVQGkUO4UCiuykBFAAcyQifIo0J8At4bgThoMGjtqmc0cgmokgARAFcM5izWeeQaHRxmNC8XFsxlvAPBMhm3oFgWClOKIwGAOkYTXEzXBJLzhEWVqXJeJeaZhItwBwkL2XZuNtv9auS+L-sfTC2E63aCOGGO3hw4LvIMwD6tcWUc0SFWSSAUlSjhwBqHgMt4TICEsxaSOePZ9i2pimkKi7LooKAAEZ+te+JGIBd74XAwjAMwYCMPAwZuDWfY1nAHBIigzAZnK7jdCBfCSEg3iJFAGY+DKAx6AaeGnphOGKHht5AA)
1741-
1742-
5. A [useReducer-based version](https://gist.github.com/sw-yx/f18fe6dd4c43fddb3a4971e80114a052) may also be helpful.
1589+
```tsx
1590+
import { createContext } from "react";
17431591

1744-
<details>
1592+
interface CurrentUserContextType {
1593+
username: string;
1594+
}
1595+
1596+
const CurrentUserContext = createContext<CurrentUserContextType | null>(null);
1597+
1598+
const useCurrentUser = () => {
1599+
const currentUserContext = useContext(CurrentUserContext);
17451600

1746-
<summary><b>Mutable Context Using a Class component wrapper</b></summary>
1601+
if (!currentUserContext) {
1602+
throw new Error(
1603+
"useCurrentUser has to be used within <CurrentUserContext.Provider>"
1604+
);
1605+
}
1606+
1607+
return currentUserContext;
1608+
};
1609+
```
17471610

1748-
_Contributed by: [@jpavon](https://github.com/typescript-cheatsheets/react/pull/13)_
1611+
Using a runtime type check in this will has the benefit of printing a clear error message in the console when a provider is not wrapping the components properly. Now it's possible to access `currentUser.username` without checking for `null`:
17491612

17501613
```tsx
1751-
interface ProviderState {
1752-
themeColor: string;
1753-
}
1614+
import { useContext } from "react";
17541615

1755-
interface UpdateStateArg {
1756-
key: keyof ProviderState;
1757-
value: string;
1758-
}
1616+
const MyComponent = () => {
1617+
const currentUser = useCurrentUser();
17591618

1760-
interface ProviderStore {
1761-
state: ProviderState;
1762-
update: (arg: UpdateStateArg) => void;
1763-
}
1619+
return <p>Username: {currentUser.username}.</p>;
1620+
};
1621+
```
17641622

1765-
const Context = createContext({} as ProviderStore); // type assertion on empty object
1623+
##### Type assertion as an alternative
17661624

1767-
class Provider extends React.Component<
1768-
{ children?: ReactNode },
1769-
ProviderState
1770-
> {
1771-
public readonly state = {
1772-
themeColor: "red",
1773-
};
1625+
Another way to avoid having to check for `null` is to use type assertion to tell TypeScript you know the context is not `null`:
17741626

1775-
private update = ({ key, value }: UpdateStateArg) => {
1776-
this.setState({ [key]: value });
1777-
};
1627+
```tsx
1628+
import { useContext } from "react";
17781629

1779-
public render() {
1780-
const store: ProviderStore = {
1781-
state: this.state,
1782-
update: this.update,
1783-
};
1630+
const MyComponent = () => {
1631+
const currentUser = useContext(CurrentUserContext);
17841632

1785-
return (
1786-
<Context.Provider value={store}>{this.props.children}</Context.Provider>
1787-
);
1788-
}
1789-
}
1633+
return <p>Name: {currentUser!.username}.</p>;
1634+
};
1635+
```
1636+
1637+
Another option is to use an empty object as default value and cast it to the expected context type:
17901638

1791-
const Consumer = Context.Consumer;
1639+
```tsx
1640+
const CurrentUserContext = createContext<CurrentUserContextType>(
1641+
{} as CurrentUserContextType
1642+
);
17921643
```
17931644

1794-
</details>
1645+
You can also use non-null assertion to get the same result:
17951646

1796-
[Something to add? File an issue](https://github.com/typescript-cheatsheets/react/issues/new).
1647+
```tsx
1648+
const CurrentUserContext = createContext<CurrentUserContextType>(null!);
1649+
```
1650+
1651+
When you don't know what to choose, prefer runtime checking and throwing over type asserting.
17971652

17981653
<!--END-SECTION:context-->
17991654

0 commit comments

Comments
 (0)