Минималистичное управление состоянием (React)

Минималистичное управление состоянием (React)

От автора: 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.