Тип Поста

Создаем приложение “Органайзер” с помощью React, Redux и Immutable.js

Создаем приложение “Органайзер” с помощью React, Redux и Immutable.js

React – это JavaScript библиотека от компании Facebook, использующая раздельные компоненты для построения приложения и одностороннее связывание данных, что делает её отличным вариантом для создания пользовательских интерфейсов.

Тем не менее, возможности библиотеки для работы с различными состояниями данных приложения, сознательно были упрощены разработчиками. Поэтому, всегда стоить помнить о том, что React всего лишь реализует архитектуру Вид в традиционной связке Модель-Вид-Контроллер.

По большому счету, нет ничего, что могло бы помешать созданию больших приложений с помощью React, но сперва нужно изучить определенные инструменты, которые позволять управлять данными приложения и поддерживать структуру нашего кода, сохраняя её в простоте и порядке.

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

В сегодняшней статье мы подружим React с двумя такими библиотеками и создадим небольшое приложение.

Redux

Redux - это очень маленькая библиотека, которая реализует контейнер для данных нашего приложения, благодаря объединению идей от таких библиотек, как Flux и Elm. С помощью Redux, мы можем управлять любым состоянием данных, при условии соблюдения следующих принципов:

  1. Состояние данных сохраняется в одиночном хранилище
  2. Данные изменяются с помощью действий (actions)

Ядром одиночного хранилища Redux, является функция, которая принимает текущее состояние данных приложения, нужное действие и комбинирует их для создания нового состояния приложения. Такая функция называется – редуктором.

Наши React-компоненты будут отвечать за отправку действий в хранилище, и, в свою очередь, наше хранилище будет говорить компонентам, когда им нужно будет сделать пересборку DOM-дерева.

ImmutableJS

По причине того, что Redux не позволяет нам изменять состояние приложения, необходимо использовать неизменяемые структуры данных. ImmutableJS предоставляет нам некоторое количество неизменяемых структур данных с изменяемыми интерфейсами, которые выполняют свои функции весьма эффективным способом.

Установка модулей и настройка сборки проекта

Для начала в директории нашего проекта инициализируем package.json командой npm init. Затем установим все зависимости, которые нам могут понадобиться:

npm i --save react react-dom redux react-redux immutable
npm i --save-dev webpack babel-loader babel-preset-es2015 babel-preset-react

В ходе написания кода проекта, мы будем использовать JavaScript XML-подобный синтаксис (JSX) и последнюю версию ECMAScript 2015. Поэтому, для нашего кода понадобится транспайлер 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',
        query: { presets: [ 'es2015', 'react' ] }
      }
    ]
  }
};

И, наконец, добавим в файл package.json скрипт npm для сборки нашего проекта из исходного кода:

"script": {
  "build": "webpack --debug"
}

Обратите внимание, что в данном случае необходимо выполнять команду npm run build каждый раз после внесения изменений в исходный код проекта.

React и его компоненты

До того, как мы начнем реализовывать React-компоненты, будет полезным создать небольшую заготовку с каким-то набором данных для отображения их на странице. Это позволит визуализировать рендер наших компонентов.

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' }
];

Для нашего приложения нужны будут только два компонента - ‹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( todo => (
          <li key={ todo.id } className='todo__item'>
            <Todo todo={ todo }/>
          </li>
        ) )}
      </ul>
    </div>
  );
}

Для тестирование нашего компонента создадим файл index.html в корневой директории проекта и заполним его разметкой. И немного стилей тоже лишними не будут.

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>React ToDo-App</title>

  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div id="app"></div>

  <script src="bundle.js"></script>
</body>
</html>

И конечно же, необходимо создать входную точку для нашего приложения в файле 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. Результат выполнения показан на скрине:

react-redux-todo-01

Redux и ImmutableJS

Теперь, когда мы создали начальный пользовательский интерфейс, самое время подумать о данных приложения. Наша заготовка с данными отлично подойдет для преобразования её в ImmutableJS коллекцию. Внесем изменения в файле app.js:

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' ).

Проектирование действий (actions)

Итак, у нас получилась некоторая структура приложения, теперь стоит подумать о том, какие действия будут влиять и обновлять её. В нашем случае, достаточно будет два действия – одно для добавления новой записи в органайзер, второе для того, чтобы отметить выполнение записи. Давайте определим функции, которые будут отвечать за эти действия.

Создадим файл src/actions.js и добавим в него код:

// небольшой костыль для генерации уникальных id
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 (тип) действия и payload (нужные нам данные). Свойство 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(), который вернет нам список, с добавленной в конце новой записью:

case 'ADD_TODO':
  return todos.push( Map( action.payload ) );

Заметьте, что мы конвертируем объект с данными в неизменяемую структуру, прежде чем, поместим его в todos список.

Более сложная обработка нужна для действия TOGGLE_TODO:

case 'TOGGLE_TODO':
  return todos.map( todo =>
  {
    if( todo.get( 'id' ) === action.payload )
      return todo.update( 'isDone', isDone => !isDone );
    else
      return todo;
  } );

В данном случае, мы используем метод .map() для итерации по каждому элементу массива, чтобы найти тот элемент, id которого совпадает с id действия. Затем мы вызываем метод update(), который принимает ключ и функцию, после чего возвращает новую копию структуры, в которой значение ключа isDone заменено возвращаемым результатом работы функции.

Для лучшего понимания, возможно, стоит разобрать этот момент на отдельном примере:

const todo = Map( { id: 0, text: 'foo', isDone: false } );
todo.update( 'isDone', isDone => !isDone );
// Результат работы метода update(): { id: 0, text: 'foo', isDone: true }

Связывая все вместе

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

Изменим файл 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' )
);

Нам необходимо сделать так, чтобы наши компоненты узнали об этом хранилище. Для этого мы будем использовать библиотеку react-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() принимает в качестве аргумента данные о текущем состоянии хранилища (в нашем случае это список todos), после чего возвращает объект, содержащий структуру состояния нашего компонента:

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 ) ) } />

Теперь, когда наши компоненты знают о действиях, мы можем вызвать их из слушателя событий.

Добавим файл components.js:

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='Добавить запись'
             onKeyDown={ onSubmit } />

      <ul className='todo__list'>
        { todos.map( todo => (
          <li key={ todo.get( 'id' ) }
              className='todo__item'
              onClick={ toggleClick( todo.get( 'id' ) ) } >

            <Todo todo={ todo.toJS() } />
          </li>
        ) )}
      </ul>
    </div>
  );
}

Наши контейнеры будут автоматически подписаны на изменения в хранилище, и они произведут новый рендер компонента всякий раз, когда его свойства изменятся.

В заключение нам нужно сделать наш контейнер связанным с хранилищем, используя ‹Provider /› компонент.

Добавим в файл 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 архитектуры, но даже этого будет достаточно для начала более глубокого погружения в изучение этого инструмента.

Финальный результат работы написанного нами приложения:

react-redux-todo-02

Исходные коды проекта доступны по ссылке: https://github.com/sbogdanov108/react_todo

При создании статьи были использованы следующие источники:

  1. How to Build a Todo App Using React, Redux, and Immutable.js
  2. React
  3. Model-View-Controller
  4. Redux
  5. Flux
  6. Immutable collections for JavaScript
Поделиться

5 комментариев

Вера Скажите, а если допустим в dummyTodos русские символы, как сделать чтобы они выводились на страницу в нормальном виде (проблема с кодировкой)? Пробовала поставить в loaders { test: /\.js?$/, loader: 'unicode-loader' } но тогда возникает проблема с текстом в элементах.
Slava Без immutable тут никак? Не могу понять какой профит в использовании immutable
Евгений Спасибо. Самый лучший и простой ввод в редукс на русском. Продолжайте.
Сергей Богданов Пожалуйста! Рад, что вам пригодилась статья!

Оставить комментарий

Вы можете использовать следующие HTML-теги:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>
Обязательно к заполнению