From 73b3d9596e02f5c1fc98a26df1729b3fbad18fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tammerg=C3=A5rd?= Date: Mon, 9 Jan 2023 16:46:47 +0100 Subject: [PATCH 1/6] Add custom hook for context to avoid assertions --- README.md | 10 +++++++++- docs/basic/getting-started/context.md | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3fd787d6..6f025553 100644 --- a/README.md +++ b/README.md @@ -1529,11 +1529,19 @@ export const App = () => ( ... ); +const useAppContext = () => { + const appContext = useContext(AppCtx); + if (!appContext) { + throw new Error("useApp must be used within ."); + } + return appContext; +}; + // Consume in your app import { useContext } from "react"; export const PostInfo = () => { - const appContext = useContext(AppCtx); + const appContext = useAppContext(); return (
Name: {appContext.name}, Author: {appContext.author}, Url:{" "} diff --git a/docs/basic/getting-started/context.md b/docs/basic/getting-started/context.md index 748f1cb4..ac3ff749 100644 --- a/docs/basic/getting-started/context.md +++ b/docs/basic/getting-started/context.md @@ -28,11 +28,19 @@ export const App = () => ( ... ); +const useAppContext = () => { + const appContext = useContext(AppCtx); + if (!appContext) { + throw new Error("useApp must be used within ."); + } + return appContext; +}; + // Consume in your app import { useContext } from "react"; export const PostInfo = () => { - const appContext = useContext(AppCtx); + const appContext = useAppContext(); return (
Name: {appContext.name}, Author: {appContext.author}, Url:{" "} From 8b1af4e4965d6c60b780afd105cac94f6c1cba02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tammerg=C3=A5rd?= Date: Thu, 12 Jan 2023 15:07:06 +0100 Subject: [PATCH 2/6] Rework context page --- README.md | 347 +++++++------------------ docs/basic/getting-started/context.md | 349 ++++++++------------------ 2 files changed, 195 insertions(+), 501 deletions(-) diff --git a/README.md b/README.md index 6f025553..1634c816 100644 --- a/README.md +++ b/README.md @@ -1504,304 +1504,151 @@ Sources: #### Context -#### Basic Example +#### Basic example + +Here's a basic example of creating a context containing the active theme. ```tsx import { createContext } from "react"; -interface AppContextInterface { - name: string; - author: string; - url: string; -} - -const AppCtx = createContext(null); - -// Provider in your app +type ThemeContextType = "light" | "dark"; -const sampleAppContext: AppContextInterface = { - name: "Using React Context in a Typescript App", - author: "thehappybug", - url: "http://www.example.com", -}; +const ThemeContext = createContext("light"); +``` -export const App = () => ( - ... -); +Wrap the components that need the context with a context provider: -const useAppContext = () => { - const appContext = useContext(AppCtx); - if (!appContext) { - throw new Error("useApp must be used within ."); - } - return appContext; -}; +```tsx +import { useState } from "react"; -// Consume in your app -import { useContext } from "react"; +const App = () => { + const [theme, setTheme] = useState("light"); -export const PostInfo = () => { - const appContext = useAppContext(); return ( -
- Name: {appContext.name}, Author: {appContext.author}, Url:{" "} - {appContext.url} -
+ + + ); }; ``` -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. +Call `useContext` to read and subscribe to the context. -_[Thanks to @AlvSovereign](https://github.com/typescript-cheatsheets/react/issues/97)_ - -#### Extended Example +```tsx +import { useContext } from "react"; -Using `createContext` with an empty object as default value. +const MyComponent = () => { + const theme = useContext(ThemeContext); -```tsx -interface ContextState { - // set the type of state you want to handle with context e.g. - name: string | null; -} -// set an empty object as default state -const Context = createContext({} as ContextState); -// set up context provider as you normally would in JavaScript [React Context API](https://reactjs.org/docs/context.html#api) + return

The current theme is {theme}.

; +}; ``` -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`**: +#### Without default context value -```ts -import { createContext, useContext } from "react"; +If you don't have any meaningful default value, specify `null`: -const currentUserContext = createContext(undefined); +```tsx +import { createContext } from "react"; -function EnthusiasticGreeting() { - const currentUser = useContext(currentUserContext); - return
HELLO {currentUser!.toUpperCase()}!
; +interface CurrentUserContextType { + username: string; } -function App() { +const CurrentUserContext = createContext(null); +``` + +```tsx +const App = () => { + const [currentUser, setCurrentUser] = useState({ + username: "filiptammergard", + }); + return ( - - - + + + ); -} +}; ``` -Notice the explicit type arguments which we need because we don't have a default `string` value: +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`: -```ts -const currentUserContext = createContext(undefined); -// ^^^^^^^^^^^^^^^^^^^^^^^ +```tsx +import { useContext } from "react"; + +const MyComponent = () => { + const currentUser = useContext(CurrentUserContext); + + return

Name: {currentUser?.username}.

; +}; ``` -along with the non-null assertion to tell TypeScript that `currentUser` is definitely going to be there: +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: -```ts -return
HELLO {currentUser!.toUpperCase()}!
; -// ^ -``` - -This is unfortunate because _we know_ that later in our app, a `Provider` is going to fill in the context. - -There are a few solutions for this: - -1. You can get around this by asserting non null: - - ```ts - const currentUserContext = createContext(undefined!); - ``` - - ([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. - -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`**: - - ```tsx - import { createContext, useContext } from "react"; - - /** - * A helper to create a Context and Provider with no upfront default value, and - * without having to check for undefined all the time. - */ - function createCtx() { - const ctx = createContext(undefined); - function useCtx() { - const c = useContext(ctx); - if (c === undefined) - throw new Error("useCtx must be inside a Provider with a value"); - return c; - } - return [useCtx, ctx.Provider] as const; // 'as const' makes TypeScript infer a tuple - } - - // Usage: - - // We still have to specify a type, but no default! - export const [useCurrentUserName, CurrentUserProvider] = createCtx(); - - function EnthusiasticGreeting() { - const currentUser = useCurrentUserName(); - return
HELLO {currentUser.toUpperCase()}!
; - } - - function App() { - return ( - - - - ); - } - ``` - - [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) - -3. You can go even further and combine this idea using `createContext` and [context getters](https://kentcdodds.com/blog/application-state-management-with-react/). - - ```tsx - import { createContext, useContext } from "react"; - - /** - * A helper to create a Context and Provider with no upfront default value, and - * without having to check for undefined all the time. - */ - function createCtx
() { - const ctx = createContext(undefined); - function useCtx() { - const c = useContext(ctx); - if (c === undefined) - throw new Error("useCtx must be inside a Provider with a value"); - return c; - } - return [useCtx, ctx.Provider] as const; // 'as const' makes TypeScript infer a tuple - } - - // usage - - export const [useCtx, SettingProvider] = createCtx(); // specify type, but no need to specify value upfront! - export function App() { - const key = useCustomHook("key"); // get a value from a hook, must be in a component - return ( - - - - ); - } - export function Component() { - const key = useCtx(); // can still use without null check! - return
{key}
; - } - ``` - - [View in the TypeScript Playground](https://www.typescriptlang.org/play/?jsx=2#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wFgAoCtCAOwGd4BXOpAYWZlwAkIIBrOAF44ACj5IAngC44DKMBoBzAJRCAfHADeFOHGr14AbQYoYSADSykMAMoxTSALpDExGADpmSOw5GaAvso6cEQwjFA0svZmhuISjhT+FAD0yXpEDnq0ZgAe8ADuwDAAFnA0EHCMYNjZcAAmSJgojAA2MABqKC2MSClphSUQjPDFKABuCopwnPUVjDQNmApIdXrFSGgCXS3T69OgveSY8xjAtOmoZqwwOQA8AIJqIqra5Lr6DHo3LsjoHmgZK7ZJB5B5wAA+lQWjWWdSe80WsOUAG5gscaKdzl5rjlnlpgu9aJ80D83J4WKxgXkRBgciiCXBgJhRABCNCqEo4fJlJDcgCiUBwUBEACJsd8QBw4AAjJCM+jABpwFBwAAKOAmDSgcAGpRVYy6PRF9LeuhC1nCkTQqNNSVNoUtcEM4pyllp7nVEE1SCgzhQdCyBmRcFScBAKHEcAAKhIwN4AcAwPAFJgfcrplUWhYyhB4ChIihBSgJHAIMz5mdIjBY0g6IkKH1KnQUIpDhQQZBYIHPs6KTdLDZrDBJp7vb6XADLmwbrc5JMniiQ2k6HG0EyS9W45ZpcMczyVtMKiuNuu4AbunKqjUaDAWe2cp2sCdh+d7mAwHjXoSDHA4i5sRw3C8HwopxMawahq2eZnoaco1HgKrFMBliSp8sryum1DgLQSA3sEDoRKIDK3IOMDDkoo6Kmm549IImhxP4agMrotyUthNC4fAyRMaaLHJKR5GKJRWo8boJp2h20BPhiL6RGxkAcTen7BB88B-sILrPBBaRoPmUTAC0OxeDqRRIbuNCtDsaDrJsd72hahG3HUwBjGo9GSP4tzJM5rk2v4QA) - -4. Using `createContext` and `useContext` to make a `createCtx` with [`unstated`](https://github.com/jamiebuilds/unstated)-like context setters: - - ```tsx - import { - createContext, - Dispatch, - PropsWithChildren, - SetStateAction, - useState, - } from "react"; - - export function createCtx
(defaultValue: A) { - type UpdateType = Dispatch>; - const defaultUpdate: UpdateType = () => defaultValue; - const ctx = createContext({ - state: defaultValue, - update: defaultUpdate, - }); - - function Provider(props: PropsWithChildren<{}>) { - const [state, update] = useState(defaultValue); - return ; - } - return [ctx, Provider] as const; // alternatively, [typeof ctx, typeof Provider] - } - - // usage - import { useContext } from "react"; - - const [ctx, TextProvider] = createCtx("someText"); - export const TextContext = ctx; - export function App() { - return ( - - - - ); - } - export function Component() { - const { state, update } = useContext(TextContext); - return ( - - ); - } - ``` - - [View in the TypeScript Playground](https://www.typescriptlang.org/play/?jsx=2#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wFgAoCpAD0ljkwFcA7DYCZuNIlGJAYRjUAPAEEAfAAoAJkkwpGAGxgA1FIsZIAXHFEBKOAG8KcODACeYJHACqYabyQAVS9YC8iYjAB0AEWAAzmC8aAAWwsjoPgDKSDDRMI6ibBzCFlYQmHCy8kqq6pri4gDcJlwcAfA5Csp2Dnw6dY4uVnAekgZu4tlyNfkaSKXkpmgV8BjUbZ5R3tyofPwcfNQwksbDpnCVjjrVeWoDADRlpoz2Oz25ted8ZQC+ekOmTKww7JwACjgAbsCyUJIwDgwAEdJEMN4vhAQQB1YAwUL8ULARTSIjMYSGO7iAzrTblZiVOAAbW2fEOcDO9SQAF0puCfIwAkgEo4ZL19gUkI8TnAiDBGFBOMIJpCfn8kFA4N8uW5DIYtolyZSbtY7ncjN4tUDoQENQB6Er3Mr8wWcYkTClQ37-OkoAIEyrFOD6-VwdR8IW8YDfJCKcwU4npJCZLhCCnB0PWiVQGkUO4UCiuykBFAAcyQifIo0J8At4bgThoMGjtqmc0cgmokgARAFcM5izWeeQaHRxmNC8XFsxlvAPBMhm3oFgWClOKIwGAOkYTXEzXBJLzhEWVqXJeJeaZhItwBwkL2XZuNtv9auS+L-sfTC2E63aCOGGO3hw4LvIMwD6tcWUc0SFWSSAUlSjhwBqHgMt4TICEsxaSOePZ9i2pimkKi7LooKAAEZ+te+JGIBd74XAwjAMwYCMPAwZuDWfY1nAHBIigzAZnK7jdCBfCSEg3iJFAGY+DKAx6AaeGnphOGKHht5AA) - -5. A [useReducer-based version](https://gist.github.com/sw-yx/f18fe6dd4c43fddb3a4971e80114a052) may also be helpful. +```tsx +import { createContext } from "react"; -
+interface CurrentUserContextType { + username: string; +} + +const CurrentUserContext = createContext(null); -Mutable Context Using a Class component wrapper +const useCurrentUser = () => { + const currentUserContext = useContext(CurrentUserContext); -_Contributed by: [@jpavon](https://github.com/typescript-cheatsheets/react/pull/13)_ + if (!currentUserContext) { + throw new Error( + "useCurrentUser has to be used within " + ); + } + + return currentUserContext; +}; +``` + +Now it's possible to access `currentUser.username` without checking for `null`: ```tsx -interface ProviderState { - themeColor: string; -} +import { useContext } from "react"; -interface UpdateStateArg { - key: keyof ProviderState; - value: string; -} +const MyComponent = () => { + const currentUser = useCurrentUser(); -interface ProviderStore { - state: ProviderState; - update: (arg: UpdateStateArg) => void; -} + return

Username: {currentUser.username}.

; +}; +``` -const Context = createContext({} as ProviderStore); // type assertion on empty object +##### Not recommended alternatives -class Provider extends React.Component< - { children?: ReactNode }, - ProviderState -> { - public readonly state = { - themeColor: "red", - }; +Another way to avoid having to check for `null` is to use type assertion to tell TypeScript you know the context is not `null`: - private update = ({ key, value }: UpdateStateArg) => { - this.setState({ [key]: value }); - }; +```tsx +import { useContext } from "react"; - public render() { - const store: ProviderStore = { - state: this.state, - update: this.update, - }; +const MyComponent = () => { + const currentUser = useContext(CurrentUserContext); - return ( - {this.props.children} - ); - } -} + return

Name: {currentUser!.username}.

; +}; +``` + +Another option is to use an empty object as default value and cast it to the expected context type: -const Consumer = Context.Consumer; +```tsx +const CurrentUserContext = createContext( + {} as CurrentUserContextType +); ``` -
+You can also use non-null assertion to get the same result: -[Something to add? File an issue](https://github.com/typescript-cheatsheets/react/issues/new). +```tsx +const CurrentUserContext = createContext(null!); +``` + +However, it's recommended to avoid these kinds of assertions whenever possible. diff --git a/docs/basic/getting-started/context.md b/docs/basic/getting-started/context.md index ac3ff749..6f81e6a5 100644 --- a/docs/basic/getting-started/context.md +++ b/docs/basic/getting-started/context.md @@ -3,301 +3,148 @@ id: context title: Context --- -## Basic Example +## Basic example + +Here's a basic example of creating a context containing the active theme. ```tsx import { createContext } from "react"; -interface AppContextInterface { - name: string; - author: string; - url: string; -} - -const AppCtx = createContext(null); - -// Provider in your app +type ThemeContextType = "light" | "dark"; -const sampleAppContext: AppContextInterface = { - name: "Using React Context in a Typescript App", - author: "thehappybug", - url: "http://www.example.com", -}; +const ThemeContext = createContext("light"); +``` -export const App = () => ( - ... -); +Wrap the components that need the context with a context provider: -const useAppContext = () => { - const appContext = useContext(AppCtx); - if (!appContext) { - throw new Error("useApp must be used within ."); - } - return appContext; -}; +```tsx +import { useState } from "react"; -// Consume in your app -import { useContext } from "react"; +const App = () => { + const [theme, setTheme] = useState("light"); -export const PostInfo = () => { - const appContext = useAppContext(); return ( -
- Name: {appContext.name}, Author: {appContext.author}, Url:{" "} - {appContext.url} -
+ + + ); }; ``` -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. - -_[Thanks to @AlvSovereign](https://github.com/typescript-cheatsheets/react/issues/97)_ +Call `useContext` to read and subscribe to the context. -## Extended Example +```tsx +import { useContext } from "react"; -Using `createContext` with an empty object as default value. +const MyComponent = () => { + const theme = useContext(ThemeContext); -```tsx -interface ContextState { - // set the type of state you want to handle with context e.g. - name: string | null; -} -// set an empty object as default state -const Context = createContext({} as ContextState); -// set up context provider as you normally would in JavaScript [React Context API](https://reactjs.org/docs/context.html#api) + return

The current theme is {theme}.

; +}; ``` -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`**: +## Without default context value -```ts -import { createContext, useContext } from "react"; +If you don't have any meaningful default value, specify `null`: -const currentUserContext = createContext(undefined); +```tsx +import { createContext } from "react"; -function EnthusiasticGreeting() { - const currentUser = useContext(currentUserContext); - return
HELLO {currentUser!.toUpperCase()}!
; +interface CurrentUserContextType { + username: string; } -function App() { +const CurrentUserContext = createContext(null); +``` + +```tsx +const App = () => { + const [currentUser, setCurrentUser] = useState({ + username: "filiptammergard", + }); + return ( - - - + + + ); -} +}; ``` -Notice the explicit type arguments which we need because we don't have a default `string` value: +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`: -```ts -const currentUserContext = createContext(undefined); -// ^^^^^^^^^^^^^^^^^^^^^^^ -``` +```tsx +import { useContext } from "react"; -along with the non-null assertion to tell TypeScript that `currentUser` is definitely going to be there: +const MyComponent = () => { + const currentUser = useContext(CurrentUserContext); -```ts -return
HELLO {currentUser!.toUpperCase()}!
; -// ^ + return

Name: {currentUser?.username}.

; +}; ``` -This is unfortunate because _we know_ that later in our app, a `Provider` is going to fill in the context. - -There are a few solutions for this: - -1. You can get around this by asserting non null: - - ```ts - const currentUserContext = createContext(undefined!); - ``` - - ([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. - -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`**: - - ```tsx - import { createContext, useContext } from "react"; - - /** - * A helper to create a Context and Provider with no upfront default value, and - * without having to check for undefined all the time. - */ - function createCtx
() { - const ctx = createContext(undefined); - function useCtx() { - const c = useContext(ctx); - if (c === undefined) - throw new Error("useCtx must be inside a Provider with a value"); - return c; - } - return [useCtx, ctx.Provider] as const; // 'as const' makes TypeScript infer a tuple - } - - // Usage: - - // We still have to specify a type, but no default! - export const [useCurrentUserName, CurrentUserProvider] = createCtx(); - - function EnthusiasticGreeting() { - const currentUser = useCurrentUserName(); - return
HELLO {currentUser.toUpperCase()}!
; - } - - function App() { - return ( - - - - ); - } - ``` - - [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) - -3. You can go even further and combine this idea using `createContext` and [context getters](https://kentcdodds.com/blog/application-state-management-with-react/). - - ```tsx - import { createContext, useContext } from "react"; - - /** - * A helper to create a Context and Provider with no upfront default value, and - * without having to check for undefined all the time. - */ - function createCtx
() { - const ctx = createContext(undefined); - function useCtx() { - const c = useContext(ctx); - if (c === undefined) - throw new Error("useCtx must be inside a Provider with a value"); - return c; - } - return [useCtx, ctx.Provider] as const; // 'as const' makes TypeScript infer a tuple - } - - // usage - - export const [useCtx, SettingProvider] = createCtx(); // specify type, but no need to specify value upfront! - export function App() { - const key = useCustomHook("key"); // get a value from a hook, must be in a component - return ( - - - - ); - } - export function Component() { - const key = useCtx(); // can still use without null check! - return
{key}
; - } - ``` - - [View in the TypeScript Playground](https://www.typescriptlang.org/play/?jsx=2#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wFgAoCtCAOwGd4BXOpAYWZlwAkIIBrOAF44ACj5IAngC44DKMBoBzAJRCAfHADeFOHGr14AbQYoYSADSykMAMoxTSALpDExGADpmSOw5GaAvso6cEQwjFA0svZmhuISjhT+FAD0yXpEDnq0ZgAe8ADuwDAAFnA0EHCMYNjZcAAmSJgojAA2MABqKC2MSClphSUQjPDFKABuCopwnPUVjDQNmApIdXrFSGgCXS3T69OgveSY8xjAtOmoZqwwOQA8AIJqIqra5Lr6DHo3LsjoHmgZK7ZJB5B5wAA+lQWjWWdSe80WsOUAG5gscaKdzl5rjlnlpgu9aJ80D83J4WKxgXkRBgciiCXBgJhRABCNCqEo4fJlJDcgCiUBwUBEACJsd8QBw4AAjJCM+jABpwFBwAAKOAmDSgcAGpRVYy6PRF9LeuhC1nCkTQqNNSVNoUtcEM4pyllp7nVEE1SCgzhQdCyBmRcFScBAKHEcAAKhIwN4AcAwPAFJgfcrplUWhYyhB4ChIihBSgJHAIMz5mdIjBY0g6IkKH1KnQUIpDhQQZBYIHPs6KTdLDZrDBJp7vb6XADLmwbrc5JMniiQ2k6HG0EyS9W45ZpcMczyVtMKiuNuu4AbunKqjUaDAWe2cp2sCdh+d7mAwHjXoSDHA4i5sRw3C8HwopxMawahq2eZnoaco1HgKrFMBliSp8sryum1DgLQSA3sEDoRKIDK3IOMDDkoo6Kmm549IImhxP4agMrotyUthNC4fAyRMaaLHJKR5GKJRWo8boJp2h20BPhiL6RGxkAcTen7BB88B-sILrPBBaRoPmUTAC0OxeDqRRIbuNCtDsaDrJsd72hahG3HUwBjGo9GSP4tzJM5rk2v4QA) - -4. Using `createContext` and `useContext` to make a `createCtx` with [`unstated`](https://github.com/jamiebuilds/unstated)-like context setters: - - ```tsx - import { - createContext, - Dispatch, - PropsWithChildren, - SetStateAction, - useState, - } from "react"; - - export function createCtx
(defaultValue: A) { - type UpdateType = Dispatch>; - const defaultUpdate: UpdateType = () => defaultValue; - const ctx = createContext({ - state: defaultValue, - update: defaultUpdate, - }); - - function Provider(props: PropsWithChildren<{}>) { - const [state, update] = useState(defaultValue); - return ; - } - return [ctx, Provider] as const; // alternatively, [typeof ctx, typeof Provider] - } - - // usage - import { useContext } from "react"; - - const [ctx, TextProvider] = createCtx("someText"); - export const TextContext = ctx; - export function App() { - return ( - - - - ); - } - export function Component() { - const { state, update } = useContext(TextContext); - return ( - - ); - } - ``` - - [View in the TypeScript Playground](https://www.typescriptlang.org/play/?jsx=2#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wFgAoCpAD0ljkwFcA7DYCZuNIlGJAYRjUAPAEEAfAAoAJkkwpGAGxgA1FIsZIAXHFEBKOAG8KcODACeYJHACqYabyQAVS9YC8iYjAB0AEWAAzmC8aAAWwsjoPgDKSDDRMI6ibBzCFlYQmHCy8kqq6pri4gDcJlwcAfA5Csp2Dnw6dY4uVnAekgZu4tlyNfkaSKXkpmgV8BjUbZ5R3tyofPwcfNQwksbDpnCVjjrVeWoDADRlpoz2Oz25ted8ZQC+ekOmTKww7JwACjgAbsCyUJIwDgwAEdJEMN4vhAQQB1YAwUL8ULARTSIjMYSGO7iAzrTblZiVOAAbW2fEOcDO9SQAF0puCfIwAkgEo4ZL19gUkI8TnAiDBGFBOMIJpCfn8kFA4N8uW5DIYtolyZSbtY7ncjN4tUDoQENQB6Er3Mr8wWcYkTClQ37-OkoAIEyrFOD6-VwdR8IW8YDfJCKcwU4npJCZLhCCnB0PWiVQGkUO4UCiuykBFAAcyQifIo0J8At4bgThoMGjtqmc0cgmokgARAFcM5izWeeQaHRxmNC8XFsxlvAPBMhm3oFgWClOKIwGAOkYTXEzXBJLzhEWVqXJeJeaZhItwBwkL2XZuNtv9auS+L-sfTC2E63aCOGGO3hw4LvIMwD6tcWUc0SFWSSAUlSjhwBqHgMt4TICEsxaSOePZ9i2pimkKi7LooKAAEZ+te+JGIBd74XAwjAMwYCMPAwZuDWfY1nAHBIigzAZnK7jdCBfCSEg3iJFAGY+DKAx6AaeGnphOGKHht5AA) - -5. A [useReducer-based version](https://gist.github.com/sw-yx/f18fe6dd4c43fddb3a4971e80114a052) may also be helpful. - -
- -Mutable Context Using a Class component wrapper - -_Contributed by: [@jpavon](https://github.com/typescript-cheatsheets/react/pull/13)_ +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: ```tsx -interface ProviderState { - themeColor: string; -} +import { createContext } from "react"; -interface UpdateStateArg { - key: keyof ProviderState; - value: string; +interface CurrentUserContextType { + username: string; } -interface ProviderStore { - state: ProviderState; - update: (arg: UpdateStateArg) => void; -} +const CurrentUserContext = createContext(null); -const Context = createContext({} as ProviderStore); // type assertion on empty object +const useCurrentUser = () => { + const currentUserContext = useContext(CurrentUserContext); -class Provider extends React.Component< - { children?: ReactNode }, - ProviderState -> { - public readonly state = { - themeColor: "red", - }; + if (!currentUserContext) { + throw new Error( + "useCurrentUser has to be used within " + ); + } - private update = ({ key, value }: UpdateStateArg) => { - this.setState({ [key]: value }); - }; + return currentUserContext; +}; +``` - public render() { - const store: ProviderStore = { - state: this.state, - update: this.update, - }; +Now it's possible to access `currentUser.username` without checking for `null`: - return ( - {this.props.children} - ); - } -} +```tsx +import { useContext } from "react"; + +const MyComponent = () => { + const currentUser = useCurrentUser(); + + return

Username: {currentUser.username}.

; +}; +``` -const Consumer = Context.Consumer; +### Not recommended alternatives + +Another way to avoid having to check for `null` is to use type assertion to tell TypeScript you know the context is not `null`: + +```tsx +import { useContext } from "react"; + +const MyComponent = () => { + const currentUser = useContext(CurrentUserContext); + + return

Name: {currentUser!.username}.

; +}; ``` -
+Another option is to use an empty object as default value and cast it to the expected context type: + +```tsx +const CurrentUserContext = createContext( + {} as CurrentUserContextType +); +``` + +You can also use non-null assertion to get the same result: + +```tsx +const CurrentUserContext = createContext(null!); +``` -[Something to add? File an issue](https://github.com/typescript-cheatsheets/react/issues/new). +However, it's recommended to avoid these kinds of assertions whenever possible. From 3f55f57c20b132b96b2ca04248eacd54a1e1a988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tammerg=C3=A5rd?= Date: Thu, 12 Jan 2023 15:16:15 +0100 Subject: [PATCH 3/6] Clarify the runtime type check alternative --- README.md | 4 ++-- docs/basic/getting-started/context.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1634c816..ecf79d12 100644 --- a/README.md +++ b/README.md @@ -1608,7 +1608,7 @@ const useCurrentUser = () => { }; ``` -Now it's possible to access `currentUser.username` without checking for `null`: +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`: ```tsx import { useContext } from "react"; @@ -1620,7 +1620,7 @@ const MyComponent = () => { }; ``` -##### Not recommended alternatives +##### Type assertion as an alternative (not recommended) Another way to avoid having to check for `null` is to use type assertion to tell TypeScript you know the context is not `null`: diff --git a/docs/basic/getting-started/context.md b/docs/basic/getting-started/context.md index 6f81e6a5..5d8ba029 100644 --- a/docs/basic/getting-started/context.md +++ b/docs/basic/getting-started/context.md @@ -107,7 +107,7 @@ const useCurrentUser = () => { }; ``` -Now it's possible to access `currentUser.username` without checking for `null`: +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`: ```tsx import { useContext } from "react"; @@ -119,7 +119,7 @@ const MyComponent = () => { }; ``` -### Not recommended alternatives +### Type assertion as an alternative (not recommended) Another way to avoid having to check for `null` is to use type assertion to tell TypeScript you know the context is not `null`: From 88c2893b6af94511adb96090b99cca970910ebbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tammerg=C3=A5rd?= Date: Thu, 12 Jan 2023 16:31:45 +0100 Subject: [PATCH 4/6] Remove recommendation statement --- README.md | 2 +- docs/basic/getting-started/context.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ecf79d12..1b18f3bf 100644 --- a/README.md +++ b/README.md @@ -1620,7 +1620,7 @@ const MyComponent = () => { }; ``` -##### Type assertion as an alternative (not recommended) +##### Type assertion as an alternative Another way to avoid having to check for `null` is to use type assertion to tell TypeScript you know the context is not `null`: diff --git a/docs/basic/getting-started/context.md b/docs/basic/getting-started/context.md index 5d8ba029..bbcf0aac 100644 --- a/docs/basic/getting-started/context.md +++ b/docs/basic/getting-started/context.md @@ -119,7 +119,7 @@ const MyComponent = () => { }; ``` -### Type assertion as an alternative (not recommended) +### Type assertion as an alternative Another way to avoid having to check for `null` is to use type assertion to tell TypeScript you know the context is not `null`: From 226c1bdaec155eadaa2708b4700b1936060abbdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tammerg=C3=A5rd?= Date: Thu, 12 Jan 2023 16:43:11 +0100 Subject: [PATCH 5/6] Update recommendation part --- README.md | 2 +- docs/basic/getting-started/context.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1b18f3bf..1256e4ce 100644 --- a/README.md +++ b/README.md @@ -1648,7 +1648,7 @@ You can also use non-null assertion to get the same result: const CurrentUserContext = createContext(null!); ``` -However, it's recommended to avoid these kinds of assertions whenever possible. +When you don't know what do choose, prefer runtime checking and throwing over type asserting. diff --git a/docs/basic/getting-started/context.md b/docs/basic/getting-started/context.md index bbcf0aac..dae97826 100644 --- a/docs/basic/getting-started/context.md +++ b/docs/basic/getting-started/context.md @@ -147,4 +147,4 @@ You can also use non-null assertion to get the same result: const CurrentUserContext = createContext(null!); ``` -However, it's recommended to avoid these kinds of assertions whenever possible. +When you don't know what do choose, prefer runtime checking and throwing over type asserting. From 6a797aa8fb8f616bc5cee05390c455659a2eb632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tammerg=C3=A5rd?= Date: Thu, 12 Jan 2023 16:47:49 +0100 Subject: [PATCH 6/6] Fix typo --- README.md | 2 +- docs/basic/getting-started/context.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1256e4ce..457bd871 100644 --- a/README.md +++ b/README.md @@ -1648,7 +1648,7 @@ You can also use non-null assertion to get the same result: const CurrentUserContext = createContext(null!); ``` -When you don't know what do choose, prefer runtime checking and throwing over type asserting. +When you don't know what to choose, prefer runtime checking and throwing over type asserting. diff --git a/docs/basic/getting-started/context.md b/docs/basic/getting-started/context.md index dae97826..df19d8fc 100644 --- a/docs/basic/getting-started/context.md +++ b/docs/basic/getting-started/context.md @@ -147,4 +147,4 @@ You can also use non-null assertion to get the same result: const CurrentUserContext = createContext(null!); ``` -When you don't know what do choose, prefer runtime checking and throwing over type asserting. +When you don't know what to choose, prefer runtime checking and throwing over type asserting.