From 299dad85bef2aca0e94bbf67a671588c0ddf8f55 Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 24 Feb 2022 22:00:02 +0000 Subject: [PATCH] [Beta] useContext API (#4378) * [Beta] useContext API * Simplify + add * Tweaks * Softer * Tweaks * Small tweaks --- beta/src/pages/apis/usecontext.md | 1395 +++++++++++++++++++++++++++++ beta/src/pages/apis/useref.md | 1 + beta/src/sidebarReference.json | 4 + 3 files changed, 1400 insertions(+) create mode 100644 beta/src/pages/apis/usecontext.md diff --git a/beta/src/pages/apis/usecontext.md b/beta/src/pages/apis/usecontext.md new file mode 100644 index 00000000..66e9bc89 --- /dev/null +++ b/beta/src/pages/apis/usecontext.md @@ -0,0 +1,1395 @@ +--- +title: useContext +--- + + + +`useContext` is a React Hook that lets you read and subscribe to [context](/learn/passing-data-deeply-with-context) from your component. + +```js +const value = useContext(SomeContext) +``` + + + +- [Usage](#usage) + - [Passing data deeply into the tree](#passing-data-deeply-into-the-tree) + - [Updating data passed via context](#updating-data-passed-via-context) + - [Specifying a fallback default value](#specifying-a-fallback-default-value) + - [Overriding context for a part of the tree](#overriding-context-for-a-part-of-the-tree) + - [Optimizing re-renders when passing objects and functions](#optimizing-re-renders-when-passing-objects-and-functions) +- [Reference](#reference) + - [`useContext(SomeContext)`](#usecontext) +- [Troubleshooting](#troubleshooting) + - [My component doesn't see the value from my provider](#my-component-doesnt-see-the-value-from-my-provider) + - [I am always getting undefined from my context although the default value is different](#i-am-always-getting-undefined-from-my-context-although-the-default-value-is-different) + +## Usage {/*usage*/} + + +### Passing data deeply into the tree {/*passing-data-deeply-into-the-tree*/} + +Call `useContext` at the top level of your component to read and subscribe to [context](/learn/passing-data-deeply-with-context). + +```js [[2, 4, "theme"], [1, 4, "ThemeContext"]] +import { useContext } from 'react'; + +function Button() { + const theme = useContext(ThemeContext); + // ... +``` + +`useContext` returns the context value for the context you passed. To determine the context value, React searches the component tree and finds **the closest context provider above** for that particular context. + +To pass context to a `Button`, wrap it or one of its parent components into the corresponding context provider: + +```js [[1, 3, "ThemeContext"], [2, 3, "\"dark\""], [1, 5, "ThemeContext"]] +function MyPage() { + return ( + +
+ + ); +} + +function Form() { + // ... renders buttons inside ... +} +``` + +It doesn't matter how many layers of components there are between the provider and the `Button`. When a `Button` *anywhere* inside of `Form` calls `useContext(ThemeContext)`, it will receive `"dark"` as the value. + + + +`useContext()` always looks for the closest provider *above* the component that calls it. It searches upwards and **does not** consider providers in the component from which you're calling `useContext()`. + + + + + +```js +import { createContext, useContext } from 'react'; + +const ThemeContext = createContext(null); + +export default function MyApp() { + return ( + + + + ) +} + +function Form() { + return ( + + + + + ); +} + +function Panel({ title, children }) { + const theme = useContext(ThemeContext); + const className = 'panel-' + theme; + return ( +
+

{title}

+ {children} +
+ ) +} + +function Button({ children }) { + const theme = useContext(ThemeContext); + const className = 'button-' + theme; + return ( + + ); +} +``` + +```css +.panel-light, +.panel-dark { + border: 1px solid black; + border-radius: 4px; + padding: 20px; +} +.panel-light { + color: #222; + background: #fff; +} + +.panel-dark { + color: #fff; + background: rgb(23, 32, 42); +} + +.button-light, +.button-dark { + border: 1px solid #777; + padding: 5px; + margin-right: 10px; + margin-top: 10px; +} + +.button-dark { + background: #222; + color: #fff; +} + +.button-light { + background: #fff; + color: #222; +} +``` + +
+ +--- + +### Updating data passed via context {/*updating-data-passed-via-context*/} + +Often, you'll want the context to change over time. To update context, you need to combine it with [state](/apis/usestate). Declare a state variable in the parent component, and pass the current state down as the context value to the provider. + +```js {2} [[1, 4, "ThemeContext"], [2, 4, "theme"], [1, 11, "ThemeContext"]] +function MyPage() { + const [theme, setTheme] = useState('dark'); + return ( + + + + + ); +} +``` + +Now any `Button` inside of the provider will receive the current `theme` value. If you call `setTheme` to update the `theme` value that you pass to the provider, all `Button` components will re-render with the new `'light'` value. + + + +--- + +### Updating a value via context {/*updating-a-value-via-context*/} + +In this example, the `MyApp` component holds a state variable which is then passed to the `ThemeContext` provider. Checking the "Dark mode" checkbox updates the state. Changing the provided value re-renders all the components using that context. + + + +```js +import { createContext, useContext, useState } from 'react'; + +const ThemeContext = createContext(null); + +export default function MyApp() { + const [theme, setTheme] = useState('light'); + return ( + + + + + ) +} + +function Form({ children }) { + return ( + + + + + ); +} + +function Panel({ title, children }) { + const theme = useContext(ThemeContext); + const className = 'panel-' + theme; + return ( +
+

{title}

+ {children} +
+ ) +} + +function Button({ children }) { + const theme = useContext(ThemeContext); + const className = 'button-' + theme; + return ( + + ); +} +``` + +```css +.panel-light, +.panel-dark { + border: 1px solid black; + border-radius: 4px; + padding: 20px; + margin-bottom: 10px; +} +.panel-light { + color: #222; + background: #fff; +} + +.panel-dark { + color: #fff; + background: rgb(23, 32, 42); +} + +.button-light, +.button-dark { + border: 1px solid #777; + padding: 5px; + margin-right: 10px; + margin-top: 10px; +} + +.button-dark { + background: #222; + color: #fff; +} + +.button-light { + background: #fff; + color: #222; +} +``` + +
+ +Note that `value="dark"` passes the `"dark"` string, but `value={theme}` passes the value of the JavaScript `theme` variable with [JSX curly braces](/learn/javascript-in-jsx-with-curly-braces). Curly braces also let you pass context values that aren't strings. + + + +### Updating an object via context {/*updating-an-object-via-context*/} + +In this example, there is a `currentUser` state variable which holds an object. You combine `{ currentUser, setCurrentUser }` into a single object and pass it down through the context inside the `value={}`. This lets any component below, such as `LoginButton`, read both `currentUser` and `setCurrentUser`, and then call `setCurrentUser` when needed. + + + +```js +import { createContext, useContext, useState } from 'react'; + +const CurrentUserContext = createContext(null); + +export default function MyApp() { + const [currentUser, setCurrentUser] = useState(null); + return ( + + + + ); +} + +function Form({ children }) { + return ( + + + + ); +} + +function LoginButton() { + const { + currentUser, + setCurrentUser + } = useContext(CurrentUserContext); + + if (currentUser !== null) { + return

You logged in as {currentUser.name}.

; + } + + return ( + + ); +} + +function Panel({ title, children }) { + return ( +
+

{title}

+ {children} +
+ ) +} + +function Button({ children, onClick }) { + return ( + + ); +} +``` + +```css +label { + display: block; +} + +.panel { + border: 1px solid black; + border-radius: 4px; + padding: 20px; + margin-bottom: 10px; +} + +.button { + border: 1px solid #777; + padding: 5px; + margin-right: 10px; + margin-top: 10px; +} +``` + +
+ + + +### Multiple contexts {/*multiple-contexts*/} + +In this example, there are two independent contexts. `ThemeContext` provides the current theme, which is a string, while `CurrentUserContext` holds the object representing the current user. + + + +```js +import { createContext, useContext, useState } from 'react'; + +const ThemeContext = createContext(null); +const CurrentUserContext = createContext(null); + +export default function MyApp() { + const [theme, setTheme] = useState('light'); + const [currentUser, setCurrentUser] = useState(null); + return ( + + + + + + + ) +} + +function WelcomePanel({ children }) { + const {currentUser} = useContext(CurrentUserContext); + return ( + + {currentUser !== null ? + : + + } + + ); +} + +function Greeting() { + const {currentUser} = useContext(CurrentUserContext); + return ( +

You logged in as {currentUser.name}.

+ ) +} + +function LoginForm() { + const {setCurrentUser} = useContext(CurrentUserContext); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const canLogin = firstName !== '' && lastName !== ''; + return ( + <> + + + + {!canLogin && Fill in both fields.} + + ); +} + +function Panel({ title, children }) { + const theme = useContext(ThemeContext); + const className = 'panel-' + theme; + return ( +
+

{title}

+ {children} +
+ ) +} + +function Button({ children, disabled, onClick }) { + const theme = useContext(ThemeContext); + const className = 'button-' + theme; + return ( + + ); +} +``` + +```css +label { + display: block; +} + +.panel-light, +.panel-dark { + border: 1px solid black; + border-radius: 4px; + padding: 20px; + margin-bottom: 10px; +} +.panel-light { + color: #222; + background: #fff; +} + +.panel-dark { + color: #fff; + background: rgb(23, 32, 42); +} + +.button-light, +.button-dark { + border: 1px solid #777; + padding: 5px; + margin-right: 10px; + margin-top: 10px; +} + +.button-dark { + background: #222; + color: #fff; +} + +.button-light { + background: #fff; + color: #222; +} +``` + +
+ + + +### Extracting providers to a component {/*extracting-providers-to-a-component*/} + +As your app grows, it is expected that you'll have a "pyramid" of contexts closer to the root of your app. There is nothing wrong with that. However, if you dislike the nesting aesthetically, you can extract the providers into a single component. In this example, `MyProviders` hides the "plumbing" and renders the children passed to it inside the necessary providers. Note that the `theme` and `setTheme` state is needed in `MyApp` itself, so `MyApp` still owns that piece of the state. + + + +```js +import { createContext, useContext, useState } from 'react'; + +const ThemeContext = createContext(null); +const CurrentUserContext = createContext(null); + +export default function MyApp() { + const [theme, setTheme] = useState('light'); + return ( + + + + + ); +} + +function MyProviders({ children, theme, setTheme }) { + const [currentUser, setCurrentUser] = useState(null); + return ( + + + {children} + + + ); +} + +function WelcomePanel({ children }) { + const {currentUser} = useContext(CurrentUserContext); + return ( + + {currentUser !== null ? + : + + } + + ); +} + +function Greeting() { + const {currentUser} = useContext(CurrentUserContext); + return ( +

You logged in as {currentUser.name}.

+ ) +} + +function LoginForm() { + const {setCurrentUser} = useContext(CurrentUserContext); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const canLogin = firstName !== '' && lastName !== ''; + return ( + <> + + + + {!canLogin && Fill in both fields.} + + ); +} + +function Panel({ title, children }) { + const theme = useContext(ThemeContext); + const className = 'panel-' + theme; + return ( +
+

{title}

+ {children} +
+ ) +} + +function Button({ children, disabled, onClick }) { + const theme = useContext(ThemeContext); + const className = 'button-' + theme; + return ( + + ); +} +``` + +```css +label { + display: block; +} + +.panel-light, +.panel-dark { + border: 1px solid black; + border-radius: 4px; + padding: 20px; + margin-bottom: 10px; +} +.panel-light { + color: #222; + background: #fff; +} + +.panel-dark { + color: #fff; + background: rgb(23, 32, 42); +} + +.button-light, +.button-dark { + border: 1px solid #777; + padding: 5px; + margin-right: 10px; + margin-top: 10px; +} + +.button-dark { + background: #222; + color: #fff; +} + +.button-light { + background: #fff; + color: #222; +} +``` + +
+ + + +### Scaling up with context and a reducer {/*scaling-up-with-context-and-a-reducer*/} + +In larger apps, it is common to combine context with a [reducer](/apis/usereducer) to extract the logic related to some state out of components. In this example, all the "wiring" is hidden in the `TasksContext.js`, which contains a reducer and two separate contexts. + +Read a [full walkthrough](/learn/scaling-up-with-reducer-and-context) of this example. + + + +```js App.js +import AddTask from './AddTask.js'; +import TaskList from './TaskList.js'; +import { TasksProvider } from './TasksContext.js'; + +export default function TaskApp() { + return ( + +

Day off in Kyoto

+ + +
+ ); +} +``` + +```js TasksContext.js +import { createContext, useContext, useReducer } from 'react'; + +const TasksContext = createContext(null); + +const TasksDispatchContext = createContext(null); + +export function TasksProvider({ children }) { + const [tasks, dispatch] = useReducer( + tasksReducer, + initialTasks + ); + + return ( + + + {children} + + + ); +} + +export function useTasks() { + return useContext(TasksContext); +} + +export function useTasksDispatch() { + return useContext(TasksDispatchContext); +} + +function tasksReducer(tasks, action) { + switch (action.type) { + case 'added': { + return [...tasks, { + id: action.id, + text: action.text, + done: false + }]; + } + case 'changed': { + return tasks.map(t => { + if (t.id === action.task.id) { + return action.task; + } else { + return t; + } + }); + } + case 'deleted': { + return tasks.filter(t => t.id !== action.id); + } + default: { + throw Error('Unknown action: ' + action.type); + } + } +} + +const initialTasks = [ + { id: 0, text: 'Philosopher’s Path', done: true }, + { id: 1, text: 'Visit the temple', done: false }, + { id: 2, text: 'Drink matcha', done: false } +]; +``` + +```js AddTask.js +import { useState, useContext } from 'react'; +import { useTasksDispatch } from './TasksContext.js'; + +export default function AddTask({ onAddTask }) { + const [text, setText] = useState(''); + const dispatch = useTasksDispatch(); + return ( + <> + setText(e.target.value)} + /> + + + ); +} + +let nextId = 3; +``` + +```js TaskList.js +import { useState, useContext } from 'react'; +import { useTasks, useTasksDispatch } from './TasksContext.js'; + +export default function TaskList() { + const tasks = useTasks(); + return ( +
    + {tasks.map(task => ( +
  • + +
  • + ))} +
+ ); +} + +function Task({ task }) { + const [isEditing, setIsEditing] = useState(false); + const dispatch = useTasksDispatch(); + let taskContent; + if (isEditing) { + taskContent = ( + <> + { + dispatch({ + type: 'changed', + task: { + ...task, + text: e.target.value + } + }); + }} /> + + + ); + } else { + taskContent = ( + <> + {task.text} + + + ); + } + return ( + + ); +} +``` + +```css +button { margin: 5px; } +li { list-style-type: none; } +ul, li { margin: 0; padding: 0; } +``` + +
+ + + +
+ +--- + +### Specifying a fallback default value {/*specifying-a-fallback-default-value*/} + +If React can't find any providers of that particular context in the parent tree, the context value returned by `useContext()` will be equal to the default value that you specified when you [created that context](/api/createcontext): + +```js [[1, 1, "ThemeContext"], [3, 1, "null"]] +const ThemeContext = createContext(null); +``` + +The default value **never changes**. If you want to update context, use it with state as [described above](#updating-data-passed-via-context). + +Often, instead of `null`, there is some more meaningful value you can use as a default, for example: + +```js [[1, 1, "ThemeContext"], [3, 1, "light"]] +const ThemeContext = createContext('light'); +``` + +This way, if you accidentally render some component without a corresponding provider, it won't break. This also helps your components work well in a test environment without setting up a lot of providers in the tests. + +In the example below, the "Toggle theme" button is always light because it's **outside any theme context provider** and the default context theme value is `'light'`. Try editing the default theme to be `'dark'`. + + + +```js +import { createContext, useContext, useState } from 'react'; + +const ThemeContext = createContext('light'); + +export default function MyApp() { + const [theme, setTheme] = useState('light'); + return ( + <> + + + + + + ) +} + +function Form({ children }) { + return ( + + + + + ); +} + +function Panel({ title, children }) { + const theme = useContext(ThemeContext); + const className = 'panel-' + theme; + return ( +
+

{title}

+ {children} +
+ ) +} + +function Button({ children, onClick }) { + const theme = useContext(ThemeContext); + const className = 'button-' + theme; + return ( + + ); +} +``` + +```css +.panel-light, +.panel-dark { + border: 1px solid black; + border-radius: 4px; + padding: 20px; + margin-bottom: 10px; +} +.panel-light { + color: #222; + background: #fff; +} + +.panel-dark { + color: #fff; + background: rgb(23, 32, 42); +} + +.button-light, +.button-dark { + border: 1px solid #777; + padding: 5px; + margin-right: 10px; + margin-top: 10px; +} + +.button-dark { + background: #222; + color: #fff; +} + +.button-light { + background: #fff; + color: #222; +} +``` + +
+ +--- + +### Overriding context for a part of the tree {/*overriding-context-for-a-part-of-the-tree*/} + +You can override the context for a part of the tree by wrapping that part in a provider with a different value. + +```js {3,5} + + ... + +