От автора: React версии 16.3 представил новый API контекста. На мой взгляд, эта новая функция достаточно хороша для управления состоянием небольших и средних приложений. Недавно я написал небольшой проект, в котором использовал контекст в качестве основного источника данных для front-end. В этом посте я хотел бы поделиться полученными знаниями и подходом.
Новый API
Давайте бегло вспомним основные моменты.
const Context = React.createContext(initialData)
Создает новый контекст. Можно иметь несколько контекстов с разными данными.
<Context.Provider value={data}/>;
Принимает свойство ‘value’. Будет перерисовывать все связанные потребители при изменении данных.
<Context.Consumer/>
Имеет доступ к данным провайдера. Существует два способа доступа потребителя к данным:
1:
class Modal extends React.Component { static contextType = AppContext; //… }class Cmp extends Component {render() { console.log(this.context); //...} }
2:
render() { return ( <Consumer> {data => <div>{data.title}</div>} </Consumer> ) }
Разница между новым и старым API
В старом API PureComponents и компоненты, которые реализовались shouldComponentUpdate, перерисовывались при изменении свойства или состояния. React не учитывает значение контекста. Такое поведение приводит к устареванию данных в контексте.
Вот пример использования старого API:
class App extends Component { static childContextTypes = { counter: PropTypes.object } constructor(props) { super(props); this.state = { count: 0, increment: this.increment }; } increment = () => this.setState({count: this.state.count + 1}); getChildContext() { return { counter: this.state } } render() { return ( <div className="App"> <header className="App-header"> Counter </header> {this.props.children} </div>); } } class Layout extends Component { render() { return ( <div> <Title /> <Increment/> </div> ); } } class Increment extends React.Component { static contextTypes = { counter: PropTypes.object, } render() { return <button onClick={this.context.counter.increment}> Inctement me</button> } } class Title extends React.Component { static contextTypes = { counter: PropTypes.object, } render = () => <h1>{this.context.counter.count}</h1> } export default function () { return ( <App> <Layout/> </App> ); }
Измените компонент Title для расширения PureComponent. Нажмите «Increment» несколько раз. <Title/> не будет перерисован, но значение контекста изменилось.
Минималистичное управление состоянием
Как я уже говорил, я использовал новый React Context API для управления состоянием в проекте. Почему я не использовал redux?
Во-первых, redux не нужен для небольших проектов. Просто представьте — действия, редукторы, резервирование для e2e-связи, connect(), объединение редукторов.
Мне понравилась идея создать приложение, используя только React. Вначале все мои данные и средства обновления были в одном файле — Store.
class Store extends Component { /* a lot of methods here */ render() { const updaters = {/*methods what*/}; const data = {/* this.state*/}; const value = { data, updaters } return ( <AppContext.Provider value={value}> {this.props.children} </AppContext.Provider> ); } }
Такая реализация покрывала все мои потребности. Но в какой-то момент я понял, что существует более 300 строк кода, чего вполне достаточно.
Поэтому я решил разделить данные в зависимости от их типа — Пользователь, Сообщения, Товары, Тема. Я постараюсь, чтобы пример оставался максимально простым. Сначала я переместил все пользовательские данные из состояния Store в отдельный класс:
class User { constructor(getState, rootUpdater) { this._getState = getState; this._rootUpdater = rootUpdater; this.name = ''; this.surname = ''; } _setValue = (value = {}) => { this._rootUpdater({ user: { setName: this.setName, setSurname: this.setSurname, ...this._getState().user, ...value } }); }; setName = (name = '') => { this._setValue({name}); } setSurname = (surname = '') => { this._setValue({surname}); } }
Каждая модель получает два важных параметра. getState — этот метод возвращает this.state компонента Store. rootUpdater — это метод this.setState из компонента Store. Затем я переделал компонент Store:
class Store extends Component { constructor(props) { super(props); this.rootUpdater = (data = {}) => { this.setState({ ...this.state, ...data }) }; this.getState = () => { return {...this.state}; }; const user = new User(this.getState, this.rootUpdater); this.state = {user}; } render() { return ( <Context.Provider value={this.state}> {this.props.children} </Context.Provider> ); } }
Представьте себе ситуацию, когда одной из моделей необходимо выполнить некоторые вычисления в зависимости от значений внутри другой модели. В обход метода this.getState каждая модель имеет доступ ко всему дереву данных. Вот полный пример:
import React, {Component, PureComponent, createContext} from 'react'; const Context = createContext(); class User { constructor(getState, rootUpdater) { this._getState = getState; this._rootUpdater = rootUpdater; this.name = ''; this.surname = ''; } _setValue = (value = {}) => { this._rootUpdater({ user: { setName: this.setName, setSurname: this.setSurname, ...this._getState().user, ...value } }); }; setName = (name = '') => { this._setValue({name}); } setSurname = (surname = '') => { this._setValue({surname}); } } class Store extends Component { constructor(props) { super(props); this.rootUpdater = (data = {}) => { this.setState({ ...this.state, ...data }) }; this.getState = () => { return this.state; }; const user = new User(this.getState, this.rootUpdater); this.state = {user}; } render() { return ( <Context.Provider value={this.state}> {this.props.children} </Context.Provider> ); } } function Layout() { return ( <div> <Title /> <Input /> </div> ); } class Title extends PureComponent { static contextType = Context; render() { return ( <div> <h3> name {this.context.user.name} </h3> <h3> surname {this.context.user.surname} </h3> </div> ); } } class Input extends PureComponent { static contextType = Context; constructor(props) { super(props); this.nameInput = React.createRef(); this.surnameInput = React.createRef(); } setName = (e) => { this.context.user.setName(e.target.value); } setSurname = (e) => { this.context.user.setSurname(e.target.value); } render() { return ( <div> <div> <p>change name </p> <input ref={this.nameInput} onChange={this.setName} /> </div> <div> <p>change name </p> <input ref={this.surnameInput} onChange={this.setSurname} /> </div> </div> ); } } export function App() { return ( <Store> <Layout /> </Store> ); }
Помните, что каждый вызов this._rootUpdater будет перерисовывать каждого подключенного потребителя. Подумайте об использовании нескольких контекстов, чтобы избежать ненужных повторных визуализаций.
Автор: Andrew Palatnyi
Источник: https://itnext.io
Редакция: Команда webformyself.