Способ, которым React использует компоненты и односторонний поток данных, делает его идеальным для описания структуры пользовательских интерфейсов. Однако его инструменты для работы с состоянием преднамеренно просты, чтобы напомнить нам, что React — это просто представление в традиционной архитектуре Model-View-Controller .
Ничто не мешает нам создавать большие приложения только с помощью React, но мы быстро обнаружим, что для простоты нашего кода нам необходимо управлять нашим состоянием в другом месте.
Хотя не существует официального решения для работы с состоянием приложения, есть некоторые библиотеки, которые особенно хорошо соответствуют парадигме React. В этой статье мы свяжем React с двумя такими библиотеками и используем их для создания простого приложения.
Redux
Redux — это крошечная библиотека, которая действует как контейнер для нашего состояния приложения, объединяя идеи от Flux и Elm . Мы можем использовать Redux для управления любым типом состояния приложения, при условии соблюдения следующих рекомендаций:
- наше государство хранится в одном магазине
- изменения происходят от действий, а не мутаций
В основе хранилища Redux лежит функция, которая принимает текущее состояние приложения и действие и объединяет их для создания нового состояния приложения. Мы называем эту функцию редуктором .
Наши компоненты React будут отвечать за отправку действий в наш магазин, и, в свою очередь, наш магазин сообщит компонентам, когда им необходимо выполнить повторную визуализацию.
ImmutableJS
Поскольку Redux не позволяет нам изменять состояние приложения, это может быть полезно для обеспечения этого путем моделирования состояния приложения с неизменяемыми структурами данных.
ImmutableJS предлагает нам ряд неизменяемых структур данных с изменяющимися интерфейсами, и они реализованы эффективным способом , вдохновленным реализациями в Clojure и Scala.
демонстрация
Мы собираемся использовать React с Redux и ImmutableJS, чтобы создать простой список задач, который позволит нам добавлять задачи и переключать их между полными и неполными.
Код доступен в репозитории на GitHub .
Настроить
Мы начнем с создания папки проекта и инициализации файла package.json
с помощью npm init
. Затем мы установим зависимости, которые нам понадобятся.
npm install --save react react-dom redux react-redux immutable npm install --save-dev webpack babel-core babel-loader babel-preset-es2015 babel-preset-react
Мы будем использовать JSX и ES2015 , поэтому мы скомпилируем наш код с Babel , и мы собираемся сделать это как часть процесса связывания модулей с Webpack .
Сначала мы создадим нашу конфигурацию Webpack в webpack.config.js
:
module.exports = { entry: './src/app.js', output: { path: __dirname, filename: 'bundle.js' }, module: { loaders: [ { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', query: { presets: [ 'es2015', 'react' ] } } ] } };
Наконец, мы расширим наш package.json
, добавив скрипт npm для компиляции нашего кода с исходными картами:
"script": { "build": "webpack --debug" }
Нам нужно будет запускать npm run build
каждый раз, когда мы хотим скомпилировать наш код.
Реагировать и Компоненты
Прежде чем мы реализуем какие-либо компоненты, может быть полезно создать несколько фиктивных данных. Это помогает нам понять, что нам понадобится для визуализации наших компонентов:
const dummyTodos = [ { id: 0, isDone: true, text: 'make components' }, { id: 1, isDone: false, text: 'design actions' }, { id: 2, isDone: false, text: 'implement reducer' }, { id: 3, isDone: false, text: 'connect components' } ];
Для этого приложения нам понадобятся только два компонента React, <Todo />
и <TodoList />
.
// src/components.js import React from 'react'; export function Todo(props) { const { todo } = props; if(todo.isDone) { return <strike>{todo.text}</strike>; } else { return <span>{todo.text}</span>; } } export function TodoList(props) { const { todos } = props; return ( <div className='todo'> <input type='text' placeholder='Add todo' /> <ul className='todo__list'> {todos.map(t => ( <li key={t.id} className='todo__item'> <Todo todo={t} /> </li> ))} </ul> </div> ); }
На этом этапе мы можем протестировать эти компоненты, создав файл index.html
в папке проекта и заполнив его следующей разметкой. (Вы можете найти простую таблицу стилей на GitHub ):
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="style.css"> <title>Immutable Todo</title> </head> <body> <div id="app"></div> <script src="bundle.js"></script> </body> </html>
Нам также понадобится точка входа приложения в src/app.js
// src/app.js import React from 'react'; import { render } from 'react-dom'; import { TodoList } from './components'; const dummyTodos = [ { id: 0, isDone: true, text: 'make components' }, { id: 1, isDone: false, text: 'design actions' }, { id: 2, isDone: false, text: 'implement reducer' }, { id: 3, isDone: false, text: 'connect components' } ]; render( <TodoList todos={dummyTodos} />, document.getElementById('app') );
Скомпилируйте код с помощью npm run build
, затем перейдите в браузер к файлу index.html
и убедитесь, что он работает.
Redux и неизменный
Теперь, когда мы довольны пользовательским интерфейсом, мы можем начать думать о его состоянии. Наши фиктивные данные — отличное место для начала, и мы можем легко перевести их в коллекции ImmutableJS:
import { List, Map } from 'immutable'; const dummyTodos = List([ Map({ id: 0, isDone: true, text: 'make components' }), Map({ id: 1, isDone: false, text: 'design actions' }), Map({ id: 2, isDone: false, text: 'implement reducer' }), Map({ id: 3, isDone: false, text: 'connect components' }) ]);
Карты ImmutableJS не работают так же, как объекты JavaScript, поэтому нам необходимо внести некоторые незначительные изменения в наши компоненты. Везде, где был доступ к свойству, прежде чем (например, todo.id
) должен был стать вызовом метода ( todo.get('id')
).
Разработка действий
Теперь, когда мы выяснили форму и структуру, мы можем начать думать о действиях, которые будут ее обновлять. В этом случае нам понадобятся только два действия: одно для добавления нового задания, а другое для переключения существующего.
Давайте определим некоторые функции для создания этих действий:
// src/actions.js // succinct hack for generating passable unique ids const uid = () => Math.random().toString(34).slice(2); export function addTodo(text) { return { type: 'ADD_TODO', payload: { id: uid(), isDone: false, text: text } }; } export function toggleTodo(id) { return { type: 'TOGGLE_TODO', payload: id } }
Каждое действие — это просто объект JavaScript со свойствами типа и полезной нагрузки. Свойство type помогает нам решить, что делать с полезной нагрузкой, когда мы обработаем действие позже.
Разработка редуктора
Теперь, когда мы знаем форму нашего состояния и действия, которые его обновляют, мы можем построить наш редуктор. Как напоминание, редуктор — это функция, которая принимает состояние и действие, а затем использует их для вычисления нового состояния.
Вот начальная структура нашего редуктора:
// src/reducer.js import { List, Map } from 'immutable'; const init = List([]); export default function(todos=init, action) { switch(action.type) { case 'ADD_TODO': // … case 'TOGGLE_TODO': // … default: return todos; } }
Обработка действия ADD_TODO
довольно проста, так как мы можем использовать метод .push () , который вернет новый список с приложением todo в конце:
case 'ADD_TODO': return todos.push(Map(action.payload));
Обратите внимание, что мы также преобразуем объект todo в неизменяемую карту, прежде чем он будет помещен в список.
Более сложное действие, которое нам нужно выполнить, это TOGGLE_TODO
:
case 'TOGGLE_TODO': return todos.map(t => { if(t.get('id') === action.payload) { return t.update('isDone', isDone => !isDone); } else { return t; } });
Мы используем .map (), чтобы перебрать список и найти задачу, id
которой соответствует действию. Затем мы вызываем .update () , который принимает ключ и функцию, затем возвращает новую копию карты, где значение ключа заменяется результатом передачи начального значения в функцию обновления.
Это может помочь увидеть буквальную версию:
const todo = Map({ id: 0, text: 'foo', isDone: false }); todo.update('isDone', isDone => !isDone); // => { id: 0, text: 'foo', isDone: true }
Соединяя все
Теперь у нас есть готовые действия и редуктор, мы можем создать магазин и подключить его к нашим компонентам React:
// src/app.js import React from 'react'; import { render } from 'react-dom'; import { createStore } from 'redux'; import { TodoList } from './components'; import reducer from './reducer'; const store = createStore(reducer); render( <TodoList todos={store.getState()} />, document.getElementById('app') );
Нам нужно, чтобы наши компоненты знали об этом магазине. Мы будем использовать response-redux, чтобы упростить этот процесс. Это позволяет нам создавать контейнеры с поддержкой магазина, которые обертывают наши компоненты, так что нам не нужно менять наши первоначальные реализации.
Нам понадобится контейнер вокруг нашего компонента <TodoList />
. Давайте посмотрим, как это выглядит:
// src/containers.js import { connect } from 'react-redux'; import * as components from './components'; import { addTodo, toggleTodo } from './actions'; export const TodoList = connect( function mapStateToProps(state) { // … }, function mapDispatchToProps(dispatch) { // … } )(components.TodoList);
Мы создаем контейнеры с функцией подключения . Когда мы вызываем connect()
, мы передаем две функции, mapStateToProps()
и mapDispatchToProps()
.
Функция mapStateToProps
принимает текущее состояние магазина в качестве аргумента (в нашем случае — список задач), а затем ожидает, что возвращаемое значение будет объектом, который описывает отображение из этого состояния в подпорки для нашего упакованного компонента:
function mapStateToProps(state) { return { todos: state }; }
Это может помочь визуализировать это на экземпляре обернутого компонента React:
<TodoList todos={state} />
Нам также нужно будет предоставить функцию mapDispatchToProps
, которая передается методу dispatch
магазина, чтобы мы могли использовать ее для отправки действий нашим создателям действий:
function mapDispatchToProps(dispatch) { return { addTodo: text => dispatch(addTodo(text)), toggleTodo: id => dispatch(toggleTodo(id)) }; }
Снова, это могло бы помочь визуализировать все эти реквизиты вместе на экземпляре нашего обернутого компонента React:
<TodoList todos={state} addTodo={text => dispatch(addTodo(text))} toggleTodo={id => dispatch(toggleTodo(id))} />
Теперь, когда мы сопоставили наш компонент с создателями действий, мы можем вызвать их из прослушивателей событий:
export function TodoList(props) { const { todos, toggleTodo, addTodo } = props; const onSubmit = (event) => { const input = event.target; const text = input.value; const isEnterKey = (event.which == 13); const isLongEnough = text.length > 0; if(isEnterKey && isLongEnough) { input.value = ''; addTodo(text); } }; const toggleClick = id => event => toggleTodo(id); return ( <div className='todo'> <input type='text' className='todo__entry' placeholder='Add todo' onKeyDown={onSubmit} /> <ul className='todo__list'> {todos.map(t => ( <li key={t.get('id')} className='todo__item' onClick={toggleClick(t.get('id'))}> <Todo todo={t.toJS()} /> </li> ))} </ul> </div> ); }
Контейнеры автоматически подпишутся на изменения в хранилище, и они будут повторно отображать упакованные компоненты всякий раз, когда их сопоставленные реквизиты изменяются.
Наконец, нам нужно сообщить контейнерам о хранилище, используя компонент <Provider />
:
// src/app.js import React from 'react'; import { render } from 'react-dom'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; import reducer from './reducer'; import { TodoList } from './containers'; // ^^^^^^^^^^ const store = createStore(reducer); render( <Provider store={store}> <TodoList /> </Provider>, document.getElementById('app') );
Вывод
Нельзя отрицать, что экосистема вокруг React и Redux может быть довольно сложной и пугающей для новичков, но хорошая новость заключается в том, что почти все эти концепции могут быть перенесены. Мы едва коснулись поверхности архитектуры Redux, но уже увидели достаточно, чтобы помочь нам начать изучение архитектуры Elm или подобрать библиотеку ClojureScript, такую как Om или Re-frame . Точно так же мы видели лишь небольшую часть возможностей с неизменяемыми данными, но теперь мы лучше подготовлены к тому, чтобы начать изучать такие языки, как Clojure или Haskell .
Независимо от того, исследуете ли вы состояние разработки веб-приложений или весь день пишете на JavaScript, опыт работы с архитектурами на основе действий и неизменяемыми данными уже становится жизненно важным навыком для разработчиков, и сейчас это прекрасное время для изучения предметы первой необходимости.