React.js. Использование контекста

09.08.2021

Теги: FrontendJavaScriptReact.jsWeb-разработкаКомпонентТеория

В типичном React-приложении данные передаются сверху вниз (от родителя к дочернему компоненту) с помощью пропсов. Но когда пропсы надо передавать большому количеству потомков, это может быть очень неудобно. Контекст дает возможность делиться данными между компонентами без необходимости явно передавать пропсы через каждый уровень дерева. Контекст разработан для передачи данных, которые можно назвать «глобальными» для всего дерева React-компонентов (текущий пользователь или выбранный язык).

В примере ниже мы вручную передаём проп theme сверху вниз, чтобы стилизовать компонент Button:

import React from 'react';

export default class App extends React.Component {
    render() {
        return <Toolbar theme="dark" />
    }
}

class Toolbar extends React.Component {
    render() {
        return <Button theme={this.props.theme} />
    }
}

class Button extends React.Component {
    render() {
        return <button className={this.props.theme}>Button</button>
    }
}

Контекст позволяет нам избежать передачи пропа theme через промежуточный компонент Toolbar:

import React from 'react';

// Контекст позволяет передавать значение глубоко в дерево компонентов без передачи пропсов
// на каждом уровне. Создадим контекст для текущей темы со значением «light» по умолчанию.
const ThemeContext = React.createContext('light');

export default class App extends React.Component {
    render() {
        // Компонент Provider используется для передачи текущей темы вниз по дереву. Любой
        // компонент может использовать этот контекст и не важно, как глубоко он находится.
        return (
            <ThemeContext.Provider value="light">
                <Toolbar />
            </ThemeContext.Provider>
        );
    }
}

class Toolbar extends React.Component {
    render() {
        return <Button />
    }
}

class Button extends React.Component {
    // Определяем contextType, чтобы получить значение контекста. React найдёт выше по дереву
    // ближайший Provider-компонент, предоставляющий этот контекст, и использует его значение.
    static contextType = ThemeContext;
    render() {
        return <button className={this.context}>Button</button>
    }
}

Доступ к контексту через this.context возможен не только в рендер-методе, но и во всех методах жизненного цикла компонента.

Контекст для функциональных компонентов

Для функциональных компонентов можно использовать ThemeContext.Consumer:

import React from 'react';

// Контекст позволяет передавать значение глубоко в дерево компонентов без передачи пропсов
// на каждом уровне. Создадим контекст для текущей темы со значением «light» по умолчанию.
const ThemeContext = React.createContext('light');

export default class App extends React.Component {
    render() {
        return (
            <ThemeContext.Provider value="dark">
                <Toolbar />
            </ThemeContext.Provider>
        );
    }
}

class Toolbar extends React.Component {
    render() {
        return <Button />
    }
}

function Button() {
    return (
        <ThemeContext.Consumer>
            {theme => <button className={theme}>Button</button>}
        </ThemeContext.Consumer>
    );
}

Также для функциональных компонентов можно использовать хук useContext:

import React, {useContext} from 'react';

// Контекст позволяет передавать значение глубоко в дерево компонентов без передачи пропсов
// на каждом уровне. Создадим контекст для текущей темы со значением «light» по умолчанию.
const ThemeContext = React.createContext('light');

export default class App extends React.Component {
    render() {
        return (
            <ThemeContext.Provider value="dark">
                <Toolbar />
            </ThemeContext.Provider>
        );
    }
}

class Toolbar extends React.Component {
    render() {
        return <Button />
    }
}

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

Передача сложного контекста

В примерах выше мы передавали просто строку dark. Но можно в качестве контекста передать что-то посложнее — объект или функцию. Давайте изменим наш пример с контекстом темы и будем передавать не строку, а объект с тремя свойствами — themeName (строка), themeStyle (объект) и toggleTheme (функция).

[src]
    App.js
    [components]
        ThemeContext.js
        Toolbar.js
        Button.js
        Switcher.js
import React from 'react';
import {ThemeContextProvider} from './components/ThemeContext';
import Toolbar from './components/Toolbar';

export default function App() {
    return (
        <ThemeContextProvider>
            <Toolbar />
        </ThemeContextProvider>
    );
}
import React from 'react';
const {Provider, Consumer} = React.createContext();

class ThemeContextProvider extends React.Component {
    state = {
        themeName: "light"
    };

    themeStyle = {
        light: {
            border: "2px solid #ddd",
            color: "#000",
            backgroundColor: "#ddd",
        },
        dark: {
            border: "2px solid #333",
            color: "#fff",
            backgroundColor: "#333",
        }
    }

    toggleTheme = () => {
        this.setState(prevState => {
            return {themeName: prevState.themeName === "light" ? "dark" : "light"}}
        );
    };

    render() {
        // контекст, который будем передавать
        const value = {
            themeName: this.state.themeName,
            themeStyle: this.themeStyle[this.state.themeName],
            toggleTheme: this.toggleTheme
        }
        return (
            <Provider value={value}>
                {this.props.children}
            </Provider>
        );
    }
}

export {ThemeContextProvider, Consumer as ThemeContextConsumer};
import React from 'react';
import Button from './Button';
import Switcher from './Switcher';

export default class Toolbar extends React.Component {
    render() {
        return (
            <div>
                <Button />
                <Switcher />
            </div>
        );
    }
}
import {ThemeContextConsumer} from './ThemeContext';

export default function Button() {
    return (
        <ThemeContextConsumer>
            {context => (
                <button style={context.themeStyle}>
                    {context.themeName === 'dark' ? 'Это темная тема' : 'Это светлая тема'}
                </button>
            )}
        </ThemeContextConsumer>
    );
}
import {ThemeContextConsumer} from './ThemeContext';

export default function Switcher() {
    return (
        <ThemeContextConsumer>
            {context => (
                <button onClick={context.toggleTheme}>
                    Переключить на {context.themeName === 'dark' ? 'светлую' : 'темную'}
                </button>
            )}
        </ThemeContextConsumer>
    );
}

Переданный из <App/> контекст получают компоненты <Button/> и <Switcher/>. Первый компонент изменяет свой цвет, используя свойство объекта контекста themeStyle, а второй переключает тему, используя свойство объекта контекста toggleTheme (которое является функцией).

Вместо заключения

Общая схема передачи контекста от верхнего компонента и потребления контекста потомками внизу:

import React from 'react';

const {Provider, Consumer} = React.createContext();

export default function App() {
    return (
        <Provider value="for one and two">
            <Parent />
        </Provider>
    );
}

function Parent() {
    return (
        <div>
            <ChildOne />
            <ChildTwo />
        </div>
    );
}

function ChildOne() {
    return (
        <Consumer>
            {context => <div>Child one, context: {context}</div>}
        </Consumer>
    );
}

function ChildTwo() {
    return (
        <Consumer>
            {context => <div>Child two, context: {context}</div>}
        </Consumer>
    );
}

Компонент Provider принимает проп value, который будет передан во все компоненты, использующие этот контекст и являющиеся потомками этого компонента Provider. Один Provider может быть связан с несколькими компонентами, потребляющими контекст. Так же компоненты Provider могут быть вложены друг в друга, переопределяя значение контекста глубже в дереве.

Все потребители, которые являются потомками Provider, будут повторно рендериться, как только проп value у Provider изменится. Потребитель (включая .contextType и useContext) перерендерится при изменении контекста, даже если его родитель, не использующий данный контекст, блокирует повторные рендеры с помощью shouldComponentUpdate.

Поиск: JavaScript • React.js • Web-разработка • Frontend • Компонент • Теория • Контекст • Context • Provider • Consumer

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.