Перейти к содержанию

useContext

useContext - это хук React, который позволяет вам читать и подписываться на контекст из вашего компонента.

1
const value = useContext(SomeContext);

Описание

useContext(SomeContext)

Вызовите useContext на верхнем уровне вашего компонента для чтения и подписки на контекст.

1
2
3
4
5
6
import { useContext } from 'react';

function MyComponent() {
    const theme = useContext(ThemeContext);
    // ...
}

Параметры

  • SomeContext: Контекст, который вы ранее создали с помощью createContext. Сам контекст не хранит информацию, он только представляет тип информации, которую вы можете предоставить или прочитать из компонентов.

Возвращает

useContext возвращает значение контекста для вызывающего компонента. Оно определяется как value, переданное ближайшему SomeContext.Provider над вызывающим компонентом в дереве. Если такого провайдера нет, то возвращаемое значение будет defaultValue, которое вы передали в createContext для этого контекста. Возвращаемое значение всегда актуально. React автоматически перерисовывает компоненты, которые читают некоторый контекст, если он меняется.

Ограничения

  • Вызов useContext() в компоненте не влияет на провайдеров, возвращаемых из этого же компонента. Соответствующий <Context.Provider> должен находиться выше компонента, выполняющего вызов useContext().
  • React автоматически перерисовывает все дочерние компоненты, использующие определенный контекст, начиная с провайдера, получившего другое value. Предыдущее и последующее значения сравниваются с помощью сравнения Object.is. Пропуск повторных рендеров с помощью memo не мешает дочерним компонентам получать свежие значения контекста.
  • Если ваша система сборки выдает дубликаты модулей на выходе (что может произойти при использовании симлинков), это может нарушить контекст. Передача чего-либо через контекст работает только в том случае, если SomeContext, который вы используете для предоставления контекста, и SomeContext, который вы используете для его чтения, являются точно одним и тем же объектом, что определяется сравнением ===.

Использование

Передача данных глубоко в дерево

Вызовите useContext на верхнем уровне вашего компонента для чтения и подписки на контекст.

1
2
3
4
5
6
import { useContext } from 'react';

function Button() {
    const theme = useContext(ThemeContext);
    // ...
}

useContext возвращает значение контекста для контекста, который вы передали. Чтобы определить значение контекста, React просматривает дерево компонентов и находит ближайшего поставщика контекста выше для данного контекста.

Чтобы передать контекст Button, оберните его или один из его родительских компонентов в соответствующий провайдер контекста:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function MyPage() {
    return (
        <ThemeContext.Provider value="dark">
            <Form />
        </ThemeContext.Provider>
    );
}

function Form() {
    // ... renders buttons inside ...
}

Не имеет значения, сколько слоев компонентов находится между провайдером и Button. Когда Button в любом месте внутри Form вызывает useContext(ThemeContext), он получит "dark" в качестве значения.

Поиск ближайшего провайдера

useContext() всегда ищет ближайшего провайдера выше компонента, который его вызывает. Она ищет вверх и не рассматривает провайдеров в компоненте, из которого вы вызываете useContext().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { createContext, useContext } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
    return (
        <ThemeContext.Provider value="dark">
            <Form />
        </ThemeContext.Provider>
    );
}

function Form() {
    return (
        <Panel title="Welcome">
            <Button>Sign up</Button>
            <Button>Log in</Button>
        </Panel>
    );
}

function Panel({ title, children }) {
    const theme = useContext(ThemeContext);
    const className = 'panel-' + theme;
    return (
        <section className={className}>
            <h1>{title}</h1>
            {children}
        </section>
    );
}

function Button({ children }) {
    const theme = useContext(ThemeContext);
    const className = 'button-' + theme;
    return (
        <button className={className}>{children}</button>
    );
}

Обновление данных, переданных через контекст

Часто требуется, чтобы контекст менялся с течением времени. Чтобы обновить контекст, объедините его с состоянием. Объявите переменную состояния в родительском компоненте и передайте текущее состояние как значение контекста провайдеру.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function MyPage() {
    const [theme, setTheme] = useState('dark');
    return (
        <ThemeContext.Provider value={theme}>
            <Form />
            <Button
                onClick={() => {
                    setTheme('light');
                }}
            >
                Switch to light theme
            </Button>
        </ThemeContext.Provider>
    );
}

Теперь любой Button внутри провайдера будет получать текущее значение theme. Если вы вызовете setTheme для обновления значения theme, которое вы передаете провайдеру, все компоненты Button будут перерисованы с новым значением 'light'.

Примеры обновления контекста

1. Обновление значения через контекст

В этом примере компонент MyApp хранит переменную состояния, которая затем передается провайдеру ThemeContext. Установка флажка "Темный режим" обновляет состояние. Изменение предоставленного значения пересматривает все компоненты, использующие данный контекст.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
    const [theme, setTheme] = useState('light');
    return (
        <ThemeContext.Provider value={theme}>
            <Form />
            <label>
                <input
                    type="checkbox"
                    checked={theme === 'dark'}
                    onChange={(e) => {
                        setTheme(
                            e.target.checked
                                ? 'dark'
                                : 'light'
                        );
                    }}
                />
                Use dark mode
            </label>
        </ThemeContext.Provider>
    );
}

function Form({ children }) {
    return (
        <Panel title="Welcome">
            <Button>Sign up</Button>
            <Button>Log in</Button>
        </Panel>
    );
}

function Panel({ title, children }) {
    const theme = useContext(ThemeContext);
    const className = 'panel-' + theme;
    return (
        <section className={className}>
            <h1>{title}</h1>
            {children}
        </section>
    );
}

function Button({ children }) {
    const theme = useContext(ThemeContext);
    const className = 'button-' + theme;
    return (
        <button className={className}>{children}</button>
    );
}

Обратите внимание, что value="dark" передает строку "dark", а value={theme} передает значение переменной JavaScript theme с фигурными скобками JSX. Фигурные скобки также позволяют передавать контекстные значения, которые не являются строками.

2. Обновление объекта через контекст

В этом примере есть переменная состояния currentUser, которая содержит объект. Вы объединяете { currentUser, setCurrentUser } в один объект и передаете его вниз через контекст внутри value={}. Это позволяет любому компоненту ниже, например, LoginButton, читать и currentUser, и setCurrentUser, а затем вызывать setCurrentUser, когда это необходимо.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import { createContext, useContext, useState } from 'react';

const CurrentUserContext = createContext(null);

export default function MyApp() {
    const [currentUser, setCurrentUser] = useState(null);
    return (
        <CurrentUserContext.Provider
            value={{
                currentUser,
                setCurrentUser,
            }}
        >
            <Form />
        </CurrentUserContext.Provider>
    );
}

function Form({ children }) {
    return (
        <Panel title="Welcome">
            <LoginButton />
        </Panel>
    );
}

function LoginButton() {
    const { currentUser, setCurrentUser } = useContext(
        CurrentUserContext
    );

    if (currentUser !== null) {
        return <p>You logged in as {currentUser.name}.</p>;
    }

    return (
        <Button
            onClick={() => {
                setCurrentUser({ name: 'Advika' });
            }}
        >
            Log in as Advika
        </Button>
    );
}

function Panel({ title, children }) {
    return (
        <section className="panel">
            <h1>{title}</h1>
            {children}
        </section>
    );
}

function Button({ children, onClick }) {
    return (
        <button className="button" onClick={onClick}>
            {children}
        </button>
    );
}

3. Множественные контексты

В этом примере есть два независимых контекста. ThemeContext предоставляет текущую тему, которая является строкой, а CurrentUserContext содержит объект, представляющий текущего пользователя.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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 (
        <ThemeContext.Provider value={theme}>
            <CurrentUserContext.Provider
                value={{
                    currentUser,
                    setCurrentUser,
                }}
            >
                <WelcomePanel />
                <label>
                    <input
                        type="checkbox"
                        checked={theme === 'dark'}
                        onChange={(e) => {
                            setTheme(
                                e.target.checked
                                    ? 'dark'
                                    : 'light'
                            );
                        }}
                    />
                    Use dark mode
                </label>
            </CurrentUserContext.Provider>
        </ThemeContext.Provider>
    );
}

function WelcomePanel({ children }) {
    const { currentUser } = useContext(CurrentUserContext);
    return (
        <Panel title="Welcome">
            {currentUser !== null ? (
                <Greeting />
            ) : (
                <LoginForm />
            )}
        </Panel>
    );
}

function Greeting() {
    const { currentUser } = useContext(CurrentUserContext);
    return <p>You logged in as {currentUser.name}.</p>;
}

function LoginForm() {
    const { setCurrentUser } = useContext(
        CurrentUserContext
    );
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const canLogin = firstName !== '' && lastName !== '';
    return (
        <>
            <label>
                First name{': '}
                <input
                    required
                    value={firstName}
                    onChange={(e) =>
                        setFirstName(e.target.value)
                    }
                />
            </label>
            <label>
                Last name{': '}
                <input
                    required
                    value={lastName}
                    onChange={(e) =>
                        setLastName(e.target.value)
                    }
                />
            </label>
            <Button
                disabled={!canLogin}
                onClick={() => {
                    setCurrentUser({
                        name: firstName + ' ' + lastName,
                    });
                }}
            >
                Log in
            </Button>
            {!canLogin && <i>Fill in both fields.</i>}
        </>
    );
}

function Panel({ title, children }) {
    const theme = useContext(ThemeContext);
    const className = 'panel-' + theme;
    return (
        <section className={className}>
            <h1>{title}</h1>
            {children}
        </section>
    );
}

function Button({ children, disabled, onClick }) {
    const theme = useContext(ThemeContext);
    const className = 'button-' + theme;
    return (
        <button
            className={className}
            disabled={disabled}
            onClick={onClick}
        >
            {children}
        </button>
    );
}

4. Извлечение провайдеров в компонент

По мере роста вашего приложения ожидается, что у вас будет "пирамида" контекстов ближе к корню вашего приложения. В этом нет ничего плохого. Однако, если вам эстетически не нравится вложенность, вы можете извлечь провайдеров в один компонент. В этом примере MyProviders скрывает "сантехнику" и отображает переданные ему дочерние элементы внутри необходимых провайдеров. Обратите внимание, что состояния theme и setTheme нужны в самом MyApp, поэтому MyApp по-прежнему владеет этой частью состояния.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext(null);
const CurrentUserContext = createContext(null);

export default function MyApp() {
    const [theme, setTheme] = useState('light');
    return (
        <MyProviders theme={theme} setTheme={setTheme}>
            <WelcomePanel />
            <label>
                <input
                    type="checkbox"
                    checked={theme === 'dark'}
                    onChange={(e) => {
                        setTheme(
                            e.target.checked
                                ? 'dark'
                                : 'light'
                        );
                    }}
                />
                Use dark mode
            </label>
        </MyProviders>
    );
}

function MyProviders({ children, theme, setTheme }) {
    const [currentUser, setCurrentUser] = useState(null);
    return (
        <ThemeContext.Provider value={theme}>
            <CurrentUserContext.Provider
                value={{
                    currentUser,
                    setCurrentUser,
                }}
            >
                {children}
            </CurrentUserContext.Provider>
        </ThemeContext.Provider>
    );
}

function WelcomePanel({ children }) {
    const { currentUser } = useContext(CurrentUserContext);
    return (
        <Panel title="Welcome">
            {currentUser !== null ? (
                <Greeting />
            ) : (
                <LoginForm />
            )}
        </Panel>
    );
}

function Greeting() {
    const { currentUser } = useContext(CurrentUserContext);
    return <p>You logged in as {currentUser.name}.</p>;
}

function LoginForm() {
    const { setCurrentUser } = useContext(
        CurrentUserContext
    );
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const canLogin = firstName !== '' && lastName !== '';
    return (
        <>
            <label>
                First name{': '}
                <input
                    required
                    value={firstName}
                    onChange={(e) =>
                        setFirstName(e.target.value)
                    }
                />
            </label>
            <label>
                Last name{': '}
                <input
                    required
                    value={lastName}
                    onChange={(e) =>
                        setLastName(e.target.value)
                    }
                />
            </label>
            <Button
                disabled={!canLogin}
                onClick={() => {
                    setCurrentUser({
                        name: firstName + ' ' + lastName,
                    });
                }}
            >
                Log in
            </Button>
            {!canLogin && <i>Fill in both fields.</i>}
        </>
    );
}

function Panel({ title, children }) {
    const theme = useContext(ThemeContext);
    const className = 'panel-' + theme;
    return (
        <section className={className}>
            <h1>{title}</h1>
            {children}
        </section>
    );
}

function Button({ children, disabled, onClick }) {
    const theme = useContext(ThemeContext);
    const className = 'button-' + theme;
    return (
        <button
            className={className}
            disabled={disabled}
            onClick={onClick}
        >
            {children}
        </button>
    );
}

5. Масштабирование с помощью контекста и редуктора

В больших приложениях часто используется сочетание контекста с редьюсером для извлечения логики, связанной с некоторым состоянием, из компонентов. В этом примере вся "проводка" спрятана в TasksContext.js, который содержит редуктор и два отдельных контекста.

Прочитайте полное прохождение этого примера.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
    return (
        <TasksProvider>
            <h1>Day off in Kyoto</h1>
            <AddTask />
            <TaskList />
        </TasksProvider>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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 (
        <TasksContext.Provider value={tasks}>
            <TasksDispatchContext.Provider value={dispatch}>
                {children}
            </TasksDispatchContext.Provider>
        </TasksContext.Provider>
    );
}

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 },
];
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { useState, useContext } from 'react';
import { useTasksDispatch } from './TasksContext.js';

export default function AddTask() {
    const [text, setText] = useState('');
    const dispatch = useTasksDispatch();
    return (
        <>
            <input
                placeholder="Add task"
                value={text}
                onChange={(e) => setText(e.target.value)}
            />
            <button
                onClick={() => {
                    setText('');
                    dispatch({
                        type: 'added',
                        id: nextId++,
                        text: text,
                    });
                }}
            >
                Add
            </button>
        </>
    );
}

let nextId = 3;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import { useState, useContext } from 'react';
import {
    useTasks,
    useTasksDispatch,
} from './TasksContext.js';

export default function TaskList() {
    const tasks = useTasks();
    return (
        <ul>
            {tasks.map((task) => (
                <li key={task.id}>
                    <Task task={task} />
                </li>
            ))}
        </ul>
    );
}

function Task({ task }) {
    const [isEditing, setIsEditing] = useState(false);
    const dispatch = useTasksDispatch();
    let taskContent;
    if (isEditing) {
        taskContent = (
            <>
                <input
                    value={task.text}
                    onChange={(e) => {
                        dispatch({
                            type: 'changed',
                            task: {
                                ...task,
                                text: e.target.value,
                            },
                        });
                    }}
                />
                <button onClick={() => setIsEditing(false)}>
                    Save
                </button>
            </>
        );
    } else {
        taskContent = (
            <>
                {task.text}
                <button onClick={() => setIsEditing(true)}>
                    Edit
                </button>
            </>
        );
    }
    return (
        <label>
            <input
                type="checkbox"
                checked={task.done}
                onChange={(e) => {
                    dispatch({
                        type: 'changed',
                        task: {
                            ...task,
                            done: e.target.checked,
                        },
                    });
                }}
            />
            {taskContent}
            <button
                onClick={() => {
                    dispatch({
                        type: 'deleted',
                        id: task.id,
                    });
                }}
            >
                Delete
            </button>
        </label>
    );
}

Указание запасного значения по умолчанию

Если React не может найти поставщиков данного контекста в родительском дереве, значение контекста, возвращаемое useContext(), будет равно значению по умолчанию, которое вы указали при создании контекста:

1
const ThemeContext = createContext(null);

Значение по умолчанию никогда не изменяется. Если вы хотите обновить контекст, используйте его вместе с состоянием, как описано выше.

Часто вместо null можно использовать какое-то более значимое значение по умолчанию, например:

1
const ThemeContext = createContext('light');

Таким образом, если вы случайно отобразите какой-либо компонент без соответствующего провайдера, он не сломается. Это также поможет вашим компонентам хорошо работать в тестовой среде без установки большого количества провайдеров в тестах.

В примере ниже кнопка "Toggle theme" всегда светлая, потому что она находится вне любого провайдера контекста темы, а значение контекстной темы по умолчанию - 'light'. Попробуйте изменить тему по умолчанию на 'dark'.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext('light');

export default function MyApp() {
    const [theme, setTheme] = useState('light');
    return (
        <>
            <ThemeContext.Provider value={theme}>
                <Form />
            </ThemeContext.Provider>
            <Button
                onClick={() => {
                    setTheme(
                        theme === 'dark' ? 'light' : 'dark'
                    );
                }}
            >
                Toggle theme
            </Button>
        </>
    );
}

function Form({ children }) {
    return (
        <Panel title="Welcome">
            <Button>Sign up</Button>
            <Button>Log in</Button>
        </Panel>
    );
}

function Panel({ title, children }) {
    const theme = useContext(ThemeContext);
    const className = 'panel-' + theme;
    return (
        <section className={className}>
            <h1>{title}</h1>
            {children}
        </section>
    );
}

function Button({ children, onClick }) {
    const theme = useContext(ThemeContext);
    const className = 'button-' + theme;
    return (
        <button className={className} onClick={onClick}>
            {children}
        </button>
    );
}

Переопределение контекста для части дерева

Вы можете переопределить контекст для части дерева, обернув эту часть в провайдер с другим значением.

1
2
3
4
5
6
7
<ThemeContext.Provider value="dark">
    ...
    <ThemeContext.Provider value="light">
        <Footer />
    </ThemeContext.Provider>
    ...
</ThemeContext.Provider>

Вы можете вложить и переопределить провайдеров столько раз, сколько вам нужно.

Примеры переопределения контекста

1. Переопределение темы

Здесь кнопка внутри Footer получает другое значение контекста ("light"), чем кнопки снаружи ("dark").

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { createContext, useContext } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
    return (
        <ThemeContext.Provider value="dark">
            <Form />
        </ThemeContext.Provider>
    );
}

function Form() {
    return (
        <Panel title="Welcome">
            <Button>Sign up</Button>
            <Button>Log in</Button>
            <ThemeContext.Provider value="light">
                <Footer />
            </ThemeContext.Provider>
        </Panel>
    );
}

function Footer() {
    return (
        <footer>
            <Button>Settings</Button>
        </footer>
    );
}

function Panel({ title, children }) {
    const theme = useContext(ThemeContext);
    const className = 'panel-' + theme;
    return (
        <section className={className}>
            {title && <h1>{title}</h1>}
            {children}
        </section>
    );
}

function Button({ children }) {
    const theme = useContext(ThemeContext);
    const className = 'button-' + theme;
    return (
        <button className={className}>{children}</button>
    );
}

2. Автоматически вложенные заголовки

Вы можете "накапливать" информацию при вложении провайдеров контекста. В этом примере компонент Section отслеживает LevelContext, который определяет глубину вложенности раздела. Он считывает LevelContext из родительской секции и предоставляет своим дочерним секциям номер LevelContext, увеличенный на единицу. В результате компонент Heading может автоматически решать, какой из тегов <h1>, <h2>, <h3>, ..., использовать, основываясь на том, сколько компонентов Section вложено в него.

Прочитайте подробное описание этого примера.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
    return (
        <Section>
            <Heading>Title</Heading>
            <Section>
                <Heading>Heading</Heading>
                <Heading>Heading</Heading>
                <Heading>Heading</Heading>
                <Section>
                    <Heading>Sub-heading</Heading>
                    <Heading>Sub-heading</Heading>
                    <Heading>Sub-heading</Heading>
                    <Section>
                        <Heading>Sub-sub-heading</Heading>
                        <Heading>Sub-sub-heading</Heading>
                        <Heading>Sub-sub-heading</Heading>
                    </Section>
                </Section>
            </Section>
        </Section>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Section({ children }) {
    const level = useContext(LevelContext);
    return (
        <section className="section">
            <LevelContext.Provider value={level + 1}>
                {children}
            </LevelContext.Provider>
        </section>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Heading({ children }) {
    const level = useContext(LevelContext);
    switch (level) {
        case 0:
            throw Error(
                'Heading must be inside a Section!'
            );
        case 1:
            return <h1>{children}</h1>;
        case 2:
            return <h2>{children}</h2>;
        case 3:
            return <h3>{children}</h3>;
        case 4:
            return <h4>{children}</h4>;
        case 5:
            return <h5>{children}</h5>;
        case 6:
            return <h6>{children}</h6>;
        default:
            throw Error('Unknown level: ' + level);
    }
}
1
2
3
import { createContext } from 'react';

export const LevelContext = createContext(0);

Оптимизация рендеринга при передаче объектов и функций

Вы можете передавать любые значения через контекст, включая объекты и функции.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function MyApp() {
    const [currentUser, setCurrentUser] = useState(null);

    function login(response) {
        storeCredentials(response.credentials);
        setCurrentUser(response.user);
    }

    return (
        <AuthContext.Provider
            value={{ currentUser, login }}
        >
            <Page />
        </AuthContext.Provider>
    );
}

Здесь контекстное значение - это объект JavaScript с двумя свойствами, одно из которых - функция. Всякий раз, когда MyApp перерисовывается (например, при обновлении маршрута), это будет разный объект, указывающий на разную функцию, поэтому React также должен будет перерисовать все компоненты в глубине дерева, которые вызывают useContext(AuthContext).

В небольших приложениях это не является проблемой. Однако нет необходимости перерисовывать их, если базовые данные, такие как currentUser, не изменились. Чтобы помочь React воспользоваться этим фактом, вы можете обернуть функцию login в useCallback и обернуть создание объекта в useMemo. Это оптимизация производительности:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { useCallback, useMemo } from 'react';

function MyApp() {
    const [currentUser, setCurrentUser] = useState(null);

    const login = useCallback((response) => {
        storeCredentials(response.credentials);
        setCurrentUser(response.user);
    }, []);

    const contextValue = useMemo(
        () => ({
            currentUser,
            login,
        }),
        [currentUser, login]
    );

    return (
        <AuthContext.Provider value={contextValue}>
            <Page />
        </AuthContext.Provider>
    );
}

В результате этого изменения, даже если MyApp потребуется перерендеринг, компонентам, вызывающим useContext(AuthContext), не потребуется перерендеринг, если только currentUser не изменился.

Подробнее о useMemo и useCallback

Устранение неполадок

Мой компонент не видит значение от моего провайдера

Есть несколько распространенных способов, как это может произойти:

  1. Вы отображаете <SomeContext.Provider> в том же компоненте (или ниже), где вы вызываете useContext(). Переместите <SomeContext.Provider> выше и вне компонента, вызывающего useContext().
  2. Возможно, вы забыли обернуть ваш компонент <SomeContext.Provider>, или вы поместили его в другую часть дерева, чем думали. Проверьте правильность иерархии с помощью React DevTools.
  3. Возможно, вы столкнулись с какой-то проблемой сборки с вашим инструментарием, из-за которой SomeContext, как видно из предоставляющего компонента, и SomeContext, как видно из читающего компонента, являются двумя разными объектами. Это может произойти, например, если вы используете симлинки. Вы можете проверить это, присвоив им глобальные значения window.SomeContext1 и window.SomeContext2, а затем проверив в консоли, равно ли window.SomeContext1 === window.SomeContext2. Если они не одинаковы, исправьте эту проблему на уровне инструмента сборки.

Я всегда получаю undefined из моего контекста, хотя значение по умолчанию другое

У вас может быть провайдер без value в дереве:

1
2
3
4
// 🚩 Doesn't work: no value prop
<ThemeContext.Provider>
    <Button />
</ThemeContext.Provider>

Если вы забыли указать value, это все равно, что передать value={undefined}.

Вы также могли по ошибке использовать другое имя пропса:

1
2
3
4
// 🚩 Doesn't work: prop should be called "value"
<ThemeContext.Provider theme={theme}>
    <Button />
</ThemeContext.Provider>

В обоих этих случаях вы должны увидеть предупреждение от React в консоли. Чтобы исправить их, вызовите свойство value:

1
2
3
4
// ✅ Passing the value prop
<ThemeContext.Provider value={theme}>
    <Button />
</ThemeContext.Provider>

Обратите внимание, что значение по умолчанию из вашего вызова createContext(defaultValue) используется только если выше вообще нет подходящего провайдера. Если где-то в родительском дереве есть компонент <SomeContext.Provider value={undefined}>, компонент, вызывающий useContext(SomeContext) получит undefined в качестве значения контекста.

Источник — https://react.dev/reference/react/useContext

Комментарии