От автора: React версии 16.3 представил новый API контекста. На мой взгляд, эта новая функция достаточно хороша для управления состоянием небольших и средних приложений. Недавно я написал небольшой проект, в котором использовал контекст в качестве основного источника данных для front-end. В этом посте я хотел бы поделиться полученными знаниями и подходом.
Новый API
Давайте бегло вспомним основные моменты.
1 |
const Context = React.createContext(initialData) |
Создает новый контекст. Можно иметь несколько контекстов с разными данными.
1 |
<Context.Provider value={data}/>; |
Принимает свойство ‘value’. Будет перерисовывать все связанные потребители при изменении данных.
1 |
<Context.Consumer/> |
Имеет доступ к данным провайдера. Существует два способа доступа потребителя к данным:
1:
1 2 3 4 5 6 7 |
class Modal extends React.Component { static contextType = AppContext; //… }class Cmp extends Component {render() { console.log(this.context); //...} } |
2:
1 2 3 4 5 6 7 |
render() { return ( <Consumer> {data => <div>{data.title}</div>} </Consumer> ) } |
Разница между новым и старым API
В старом API PureComponents и компоненты, которые реализовались shouldComponentUpdate, перерисовывались при изменении свойства или состояния. React не учитывает значение контекста. Такое поведение приводит к устареванию данных в контексте.
Вот пример использования старого API:
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 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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 в отдельный класс:
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 |
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:
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 |
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 каждая модель имеет доступ ко всему дереву данных. Вот полный пример:
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 |
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
Источник: //itnext.io
Редакция: Команда webformyself.