Как использовать Flux для управления состоянием в ReactJS - объяснение на примере

Если вы недавно начали работать над ReactJS, вам может быть интересно, как управлять состоянием в React, чтобы ваше приложение могло масштабироваться.

Чтобы решить эту проблему государственного управления, многие компании и люди разработали различные решения. Facebook, разработавший ReactJS, предложил решение под названием Flux .

Возможно, вы слышали о Redux, если работали над интерфейсными технологиями, такими как AngularJS или EmberJS . ReactJS также имеет библиотеку для реализации Redux.

Но прежде чем изучать Redux, я бы посоветовал вам пройти через Flux и понять его. После этого попробуйте Redux. Я говорю это потому, что Redux - это более продвинутая версия Flux. Если концепции Flux ясны, вы можете изучить redux и интегрировать его в свое приложение.

Что такое флюс?

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

Вам интересно, что не так с существующей платформой MVC? Представьте, что приложение вашего клиента масштабируется. У вас есть взаимодействие между многими моделями и представлениями. Как бы это выглядело?

Отношения между компонентами усложняются. Масштабировать приложение становится сложно. Facebook столкнулся с той же проблемой. Чтобы решить эту проблему, они создали однонаправленный поток данных .

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

Просмотр: этот компонент отображает пользовательский интерфейс. Всякий раз, когда с ним происходит какое-либо взаимодействие с пользователем (например, событие), он запускает действие. Также, когда Магазин сообщает представлению о том, что произошло какое-то изменение, он повторно отображает себя. Например, если пользователь нажимает кнопку « Добавить» .

Действие: обрабатываются все события. Эти события передаются компонентом представления. Этот уровень обычно используется для вызовов API. Как только действие выполнено, оно отправляется с помощью диспетчера. Действие может быть чем-то вроде добавления сообщения, удаления сообщения или любого другого взаимодействия с пользователем.

Общая структура полезной нагрузки для отправки события следующая:

{ actionType: "", data: { title: "Understanding Flux step by step", author: "Sharvin" } }

Ключ actionType является обязательным и используется диспетчером для передачи обновлений в соответствующее хранилище. Также известно использование констант для хранения имени значения для ключа actionType, чтобы избежать опечаток. Данные содержат информацию о событии, которую мы хотим отправить из Action в Store. Имя для этого ключа может быть любым.

Диспетчер: это центральный хаб и реестр одиночек. Он отправляет полезную нагрузку из Actions в Store. Также гарантирует отсутствие каскадных эффектов при отправке действия в магазин. Это гарантирует, что никакие другие действия не произойдут до того, как уровень данных завершит операции обработки и сохранения.

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

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

Магазин: хранит состояние приложения и является слоем данных этого шаблона. Не считайте это моделью от MVC. У приложения может быть один или несколько магазинов приложений. Магазины обновляются, потому что у них есть обратный вызов, зарегистрированный с помощью диспетчера.

Эмиттер событий узла используется для обновления хранилища и трансляции обновления для просмотра. Представление никогда напрямую не обновляет состояние приложения. Он обновляется в связи с изменениями в магазине.

Это только часть Flux, которая может обновлять данные. В магазине реализованы следующие интерфейсы:

  1. EventEmitter удлиняется сообщить мнение , что хранилище данных были обновлены.
  2. Добавлены такие слушатели, как addChangeListener и removeChangeListener .
  3. emitChange используется для выдачи изменения.

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

Что ж, Flux дает таблице следующие ключевые преимущества с помощью однонаправленного потока данных:

  1. Код становится достаточно ясным и понятным.
  2. Легко тестируется с помощью модульного теста.
  3. Можно создавать масштабируемые приложения.
  4. Предсказуемый поток данных.
Примечание . Единственный недостаток Flux - это то, что нам нужно написать какой-то шаблон. Помимо шаблона, нам нужно написать небольшой код при добавлении компонентов в существующее приложение.

Шаблон приложения

Чтобы узнать, как реализовать поток в ReactJS, мы создадим страницу сообщений. Здесь мы отобразим все сообщения. Шаблон приложения доступен в этом коммите. Мы будем использовать это как шаблон для интеграции Flux поверх него.

Чтобы клонировать код из этого коммита, используйте следующую команду:

git clone //github.com/Sharvin26/DummyBlog.git
git checkout 0d56987b2d461b794e7841302c9337eda1ad0725

Нам потребуется модуль response-router-dom и bootstrap . Чтобы установить эти пакеты, используйте следующую команду:

npm install [email protected] [email protected] 

После этого вы увидите следующее приложение:

Чтобы понять Flux подробно, мы реализуем только страницу сообщений GET . Как только это будет сделано, вы поймете, что процесс одинаков для POST , EDIT и DELETE .

Здесь вы увидите следующую структуру каталогов:

+-- README.md +-- package-lock.json +-- package.json +-- node_modules +-- .gitignore +-- public | +-- index.html +-- src | +-- +-- components | +-- +-- +-- common | +-- +-- +-- +-- NavBar.js | +-- +-- +-- PostLists.js | +-- +-- pages | +-- +-- +-- Home.js | +-- +-- +-- NotFound.js | +-- +-- +-- Posts.js | +-- index.js | +-- App.js | +-- db.json
Примечание: мы добавили сюда db.json  файл. Это фиктивный файл данных. Поскольку мы не хотим создавать API и вместо этого сосредоточимся на Flux, мы будем извлекать данные из этого файла.

Базовый компонент нашего приложения - это index.js. Здесь мы отрендерили App.jsвнутреннюю часть index.htmlобщего каталога с помощью методов render и getElementById . App.jsИспользуется для конфигурирования маршрутов.

We are also adding NavBar component at the top of the other so it will be available for all the components.

Inside the pages directory we have 3 files =>Home.js, Posts.js, and NotFound.js. Home.js  is simply used to display the Home component. When a user routes to a URL which doesn't exist, then NotFound.js renders.

The Posts.js is the parent component and it is used to get the data from the db.json file. It passes this data to the PostLists.js under the components directory. This component is a dumb component and it only handles the UI. It gets the data as props from its parent component (Posts.js) and displays it in the form of cards.

Now that we are clear about how our blog app is working we will start with integrating Flux on top of it.

Integrating Flux

Install Flux using the following command:

npm install [email protected]

To integrate Flux in our application we will divide this section into 4 subsections:

  1. Dispatcher
  2. Actions
  3. Stores
  4. View

Note: The complete code is available at this repository.

Dispatcher

First, create two new folders named actions and stores under the src directory. After that create a file named appDispatcher.js  under the same src directory.

Note: From now all the files which are related to Flux will have Camel casing as they are not ReactJS components.

Go to the appDispatcher.js and copy-paste the following code:

import { Dispatcher } from "flux"; const dispatcher = new Dispatcher(); export default dispatcher; 

Here we are importing the Dispatcher from the flux library that we installed, creating a new object and exporting it so that our actions module can use it.

Actions

Now go to the actions directory and create two files named actionTypes.js and postActions.js.  In the actionTypes.js we will define the constants that we require in postActions.js and store module.

The reason behind defining constants is that we don't want to make typos. You don't have to define constants but it is generally considered a good practice.

// actionTypes.js export default { GET_POSTS: "GET_POSTS", }; 

Now inside the postActions.js, we will retrieve the data from db.json and use the dispatcher object to dispatch it.

//postActions.js import dispatcher from "../appDispatcher"; import actionTypes from "./actionTypes"; import data from "../db.json"; export function getPosts() { dispatcher.dispatch({ actionTypes: actionTypes.GET_POSTS, posts: data["posts"], }); } 

Here in the above code, we have imported the dispatcher object, actionTypes constant, and data. We are using a dispatcher object's dispatch method to send the data to the store. The data in our case will be sent in the following format:

{ actionTypes: "GET_POSTS", posts: [ { "id": 1, "title": "Hello World", "author": "Sharvin Shah", "body": "Example of blog application" }, { "id": 2, "title": "Hello Again", "author": "John Doe", "body": "Testing another component" } ] }

Stores

Now we need to build the store which will act as a data layer for storing the posts. It will have an event listener to inform the view that something has changed, and will register using dispatcher with the actions to get the data.

Go to the store directory and create a new file called postStore.js.  Now first, we will import EventEmitter from the Events package. It is available in the NodeJS by default. We will also import the dispatcher object and actionTypes constant file here.

import { EventEmitter } from "events"; import dispatcher from "../appDispatcher"; import actionTypes from "../actions/actionTypes"; 

We will declare the constant of the change event and a variable to hold the posts whenever the dispatcher passes it.

const CHANGE_EVENT = "change"; let _posts = [];

Now we will write a class that extends the EventEmitter as its base class. We will declare the following methods in this class:

addChangeListener: It uses the NodeJS EventEmitter.on. It adds a change listener that accepts the callback function.

removeChangeListener: It uses the NodeJS EventEmitter.removeListener. Whenever we don't want to listen for a specific event we use the following method.

emitChange: It uses the NodeJS EventEmitter.emit. Whenever any change occurs, it emits that change.

This class will also have a method called getPosts which returns the variable _posts that we have declared above the class.

Below the variable declaration add the following code:

class PostStore extends EventEmitter { addChangeListener(callback) { this.on(CHANGE_EVENT, callback); } removeChangeListener(callback) { this.removeListener(CHANGE_EVENT, callback); } emitChange() { this.emit(CHANGE_EVENT); } getPosts() { return _posts; } }

Now create the store object of our PostStore class. We will export this object so that we can use it in the view.

const store = new PostStore();

After that, we will use the dispatcher's register method to receive the payload from our Actions component.

To register for the specific event, we need to use the actionTypes value and determine which action has occurred and process the data accordingly. Add the following code below the object declaration:

dispatcher.register((action) => { switch (action.actionTypes) { case actionTypes.GET_POSTS: _posts = action.posts; store.emitChange(); break; default: } });

We will export the object from this module so others can use it.

export default store;

View

Now we will update our view to send the event to postActions  whenever our Posts page is loaded and receive the payload from the postStore. Go to Posts.js under the pages directory. You'll find the following code inside the useEffect method:

useEffect(() => { setposts(data["posts"]); }, []);

We will change how our useEffect reads and updates the data. First, we will use the addChangeListener method from the postStore class and we will pass an onChange callback to it. We will set the postsstate value to have a return value of the getPosts method from the postStore.js file.

At the start, the store will return an empty array as there is no data available. So we will call a getPostsmethod from the postActions.js. This method will read the data and pass it to the store. Then the store will emit the change and addChangeListener will listen to the change and update the value of the posts  in its onChange callback.

If this seems confusing don't worry – check out the flow chart below which makes it easier to understand.

Remove the old code and update the following code inside Posts.js:

import React, { useState, useEffect } from "react"; import PostLists from "../components/PostLists"; import postStore from "../stores/postStore"; import { getPosts } from "../actions/postActions"; function PostPage() { const [posts, setPosts] = useState(postStore.getPosts()); useEffect(() => { postStore.addChangeListener(onChange); if (postStore.getPosts().length === 0) getPosts(); return () => postStore.removeChangeListener(onChange); }, []); function onChange() { setPosts(postStore.getPosts()); } return ( ); } export default PostPage; 

Here you'll find that we have also removed the import and also we are using setPosts inside our callback instead of useEffect method. The return () => postStore.removeChangeListener(onChange); is used to remove the listener once the user leaves that page.

После этого перейдите на страницу блога, и вы обнаружите, что наше приложение для блога работает. Единственное отличие состоит в том, что теперь вместо чтения данных в методе useEffect мы читаем их в действиях, сохраняем в хранилище и отправляем компонентам, которым они нужны.

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

И мы закончили !! Я надеюсь, что этот урок прояснил идею Flux, и вы сможете использовать его в своих проектах.

Не стесняйтесь связываться со мной в Twitter и Github.