React Context API уже некоторое время существует как экспериментальная функция, но только в версии React 16.3.0 ее стало безопасно использовать в продакшене. В приведенной ниже статье будут показаны два основных приложения для интернет-магазина, одно из которых создано с помощью Context API, а другое — без него.

Этот новый API решает одну серьезную проблему — детализация реквизита. Даже если вы не знакомы с этим термином, если вы работали над приложением React.js, это, вероятно, случалось с вами. Prop Drill — это обработка получения данных от компонента A к компоненту Z путем передачи их через несколько слоев промежуточных компонентов React. Компонент будет получать реквизиты косвенно, и вам придется следить за тем, чтобы все работало правильно.

Давайте рассмотрим, как бы вы справлялись с распространенными проблемами без React Context API.

App.js

class App extends Component {
  state = {
      cars: {
          car001: { name: 'Honda', price: 100 },
          car002: { name: 'BMW', price: 150 },
          car003: { name: 'Mercedes', price: 200 }
      }
  };
  incrementCarPrice = this.incrementCarPrice.bind(this);
  decrementCarPrice = this.decrementCarPrice.bind(this);

  incrementCarPrice(selectedID) {
      // a simple method that manipulates the state
      const cars = Object.assign({}, this.state.cars);
      cars[selectedID].price = cars[selectedID].price + 1;
      this.setState({
          cars
      });
  }

  decrementCarPrice(selectedID) {
      // a simple method that manipulates the state
      const cars = Object.assign({}, this.state.cars);
      cars[selectedID].price = cars[selectedID].price - 1;
      this.setState({
          cars
      });
  }

  render() {
      return (
          <div className="App">
              <header className="App-header">
                  <img src={logo} className="App-logo" alt="logo" />
                  <h1 className="App-title">Welcome to my web store</h1>
              </header>
              {/* Pass props twice */}
              <ProductList
                  cars={this.state.cars}
                  incrementCarPrice={this.incrementCarPrice}
                  decrementCarPrice={this.decrementCarPrice}
              />
          </div>
      );
  }
}

Список товаров .js

const ProductList = props => (
    <div className="product-list">
        <h2>Product list:</h2>
        {/* Pass props twice */}
        <Cars
            cars={props.cars}
            incrementCarPrice={props.incrementCarPrice}
            decrementCarPrice={props.decrementCarPrice}
        />
        {/* Other potential product categories which we will skip for this demo: */}
        {/* <Electronics /> */}
        {/* <Clothes /> */}
        {/* <Shoes /> */}
    </div>
);

export default ProductList;

Cars.js

const Cars = props => (
    <Fragment>
        <h4>Cars:</h4>
        {/* Finally we can use data */}
        {Object.keys(props.cars).map(carID => (
            <Car
                key={carID}
                name={props.cars[carID].name}
                price={props.cars[carID].price}
                incrementPrice={() => props.incrementCarPrice(carID)}
                decrementPrice={() => props.decrementCarPrice(carID)}
            />
        ))}
    </Fragment>
);

Car.js

const Cars = props => (
    <Fragment>
        <p>Name: {props.name}</p>
        <p>Price: ${props.price}</p>
        <button onClick={props.incrementPrice}>&uarr;</button>
        <button onClick={props.decrementPrice}>&darr;</button>
    </Fragment>
);

Конечно, это не лучший способ обработки ваших данных, но я надеюсь, что он демонстрирует, почему детализация реквизита — отстой. Итак, как Context API может помочь нам избежать этого?

Представляем контекстный интернет-магазин

Давайте проведем рефакторинг приложения и продемонстрируем, на что оно способно. В двух словах, Context API позволяет вам иметь центральное хранилище, в котором хранятся ваши данные (да, прямо как в Redux). Магазин можно вставить в любой компонент напрямую. Вы можете избавиться от посредников!

Рефакторинг довольно прост — нам не нужно вносить какие-либо изменения в структуру компонентов. Однако нам нужно создать несколько новых компонентов — поставщика и потребителя.

1. Инициализируйте контекст

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

MyContext.js

import React from 'react';

// this is the equivalent to the createStore method of Redux
// https://redux.js.org/api/createstore

const MyContext = React.createContext();

export default MyContext;

2. Создайте провайдера

Как только это будет сделано, мы можем импортировать контекст и использовать его для создания нашего провайдера, который мы называем MyProvider. В нем мы инициализируем состояние некоторыми значениями, которыми вы можете поделиться через value prop нашего компонента провайдера. В нашем примере мы разделяем this.state.cars вместе с парой методов, управляющих состоянием. Думайте об этих методах как о редьюсерах в Redux.

MyProvider.js

import MyContext from './MyContext';

class MyProvider extends Component {
    state = {
        cars: {
            car001: { name: 'Honda', price: 100 },
            car002: { name: 'BMW', price: 150 },
            car003: { name: 'Mercedes', price: 200 }
        }
    };

    render() {
        return (
            <MyContext.Provider
                value={{
                    cars: this.state.cars,
                    incrementPrice: selectedID => {
                        const cars = Object.assign({}, this.state.cars);
                        cars[selectedID].price = cars[selectedID].price + 1;
                        this.setState({
                            cars
                        });
                    },
                    decrementPrice: selectedID => {
                        const cars = Object.assign({}, this.state.cars);
                        cars[selectedID].price = cars[selectedID].price - 1;
                        this.setState({
                            cars
                        });
                    }
                }}
            >
                {this.props.children}
            </MyContext.Provider>
        );
    }
}

Чтобы сделать провайдер доступным для других компонентов, нам нужно обернуть им наше приложение (да, прямо как в Redux). Пока мы на этом, мы можем избавиться от состояния и методов, потому что теперь они определены в MyProvider.js.

App.js

class App extends Component {
    render() {
        return (
            <MyProvider>
                <div className="App">
                    <header className="App-header">
                        <img src={logo} className="App-logo" alt="logo" />
                        <h1 className="App-title">Welcome to my web store</h1>
                    </header>
                    <ProductList />
                </div>
            </MyProvider>
        );
    }
}

3. Создайте потребителя

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

Cars.js

const Cars = () => (
    <MyContext.Consumer>
        {context => (
            <Fragment>
                <h4>Cars:</h4>
                {Object.keys(context.cars).map(carID => (
                    <Car
                        key={carID}
                        name={context.cars[carID].name}
                        price={context.cars[carID].price}
                        incrementPrice={() => context.incrementPrice(carID)}
                        decrementPrice={() => context.decrementPrice(carID)}
                    />
                ))}
            </Fragment>
        )}
    </MyContext.Consumer>
);

Что мы забыли? Список продуктов! Вот где выгода становится очевидной. Мы не передаем никаких данных или методов. Компонент упрощен, потому что ему нужно отобразить только несколько компонентов.

ProductList.js

const ProductList = () => (
    <div className="product-list">
        <h2>Product list:</h2>
        <Cars />
        {/* Other potential product categories which we will skip for this demo: */}
        {/* <Electronics /> */}
        {/* <Clothes /> */}
        {/* <Shoes /> */}
    </div>
);

В ходе этой статьи я провел несколько сравнений между Redux и Context API. Одним из самых больших преимуществ Redux является тот факт, что ваше приложение может иметь центральное хранилище, к которому можно получить доступ из любого компонента. С новым Context API у вас есть эта функция по умолчанию. Было много шумихи вокруг того, что Context API сделает Redux устаревшим.

Это может быть верно для тех из вас, кто использует Redux только для возможностей центрального хранилища. Если это единственная функция, для которой вы его использовали, теперь вы можете заменить его на Context API и избежать детализации без использования сторонних библиотек.