Вступление

Прошло много времени с тех пор, как я опубликовал несколько статей о библиотеках управления состоянием React. Вы могли увидеть мои учебные пособия с использованием двух самых популярных библиотек, с которыми я работал в большинстве своих инженерных проектов Frontend / Fullstack - Redux и MobX. Эти две библиотеки очень мощные, и у каждой из них есть свои плюсы и минусы, а также лучшие практики применения.

React постоянно развивается как технология, особенно с внедрением хуков, переходом к функциональным компонентам и принятием большинства парадигм функционального программирования. Таким образом, библиотеки управления состоянием также должны адаптироваться к этим изменениям, перейдя к хукам и функциональному программированию (то есть хукам MobX). Redux, по сути, с самого начала использовал функции над классами (редюсеры, функции connect, mapStateToProps и т. Д.).

Тем временем Facebook представил свою собственную библиотеку управления состоянием под названием Recoil, которая должна быть простой, минималистичной, с гораздо меньшим шаблоном и волшебством по сравнению с Redux и MobX. В соответствии с их мотивационным документом они уделили особое внимание асинхронному обновлению состояния, производному состоянию, разделению кода с распределенным состоянием и совместимости с новыми и будущими функциями React.

Цель этого руководства - взглянуть на новую блестящую библиотеку управления состоянием и посмотреть, как она выглядит и работает по сравнению с теми, которые использовались в предыдущих руководствах. Мы будем использовать приложение Todo с бэкэндом GraphQL API из моей старой избранной статьи. Это также позволит нам проверить, как Frontend на основе React / Recoil работает с относительно новым серверным API GraphQL.

Вы можете проверить рабочий код всего проекта в репозитории.

Описание приложения

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

Вторая часть - это фактический список задач для каждого пользователя, представленный в виде списка:

Задачи отображаются для конкретного пользователя, например, / todos / 1 /, и могут быть отфильтрованы по завершенному состоянию (Завершено, Не выполнено, Все).

Серверная часть

Для серверной части можно взять немного измененную версию GraphQL API из статьи React / GraphQL.

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

"dependencies": {
    "express": "^4.17.1",
    "express-graphql": "^0.9.0",
    "graphql": "^15.1.0",
    "nodemon": "^2.0.4"
  },
  "devDependencies": {
    "@babel/core": "^7.10.3",
    "@babel/preset-env": "^7.10.3",
    "@babel/register": "^7.10.3"
  }

Есть только пакеты graphql и express, а также несколько пакетов babel для правильного использования функций ES6 (например, импорта / экспорта) в нашем приложении Node JS.

Точкой входа является файл index.js, который запускает серверное приложение с предустановкой babel для распознавания модулей ES6.

Схемы GraphQL в основном идентичны схемам из старой статьи, за исключением замены lodash-методов на ванильный JS для уменьшения размера нашего приложения.

В схеме есть два поля - users и todos с UserType и TodoType соответственно. Мы можем фильтровать список Пользователи по четырем возможным параметрам, определенным в args. Сам фильтр реализован в функции разрешения (в данном случае это простой фильтр lodash).

То же самое и со списком Todos: мы потенциально можем фильтровать его по userId и по завершенному состоянию.

Чтобы запустить приложение на стороне сервера, вы можете перейти в папку `/ server` и запустить (проверено с версией узла 12.16.1)

npm install
node index.js

Затем просто откройте http: // localhost: 3001 / graphql в браузере Chrome и попробуйте выполнить несколько запросов, чтобы убедиться, что GraphQL API работает нормально.

А вот и Recoil - клиентское приложение

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

Сначала мы создаем скелетное приложение для реакции со старым добрым

npx create-react-app client

Затем установите новую блестящую библиотеку recoil, перейдя в папку клиента и запустив:

npm install recoil

А теперь давайте подробнее рассмотрим исходный код клиентского приложения. RecoilRoot - одна из основных концепций библиотеки управления состоянием отдачи.

import { RecoilRoot } from 'recoil';
<RecoilRoot>
    /* your app component */
</RecoilRoot>

Компонент RecoilRoot действует аналогично провайдеру в приложениях Redux или MobX, но не определяет какое-либо конкретное хранилище для передачи. Компоненты, которые необходимо связать с состоянием, должны иметь RecoilRoot где-нибудь в дереве родительских компонентов. Обычно хорошее место для этого - компонент приложения корневого уровня.

Итак, в нашем руководстве все дочерние элементы, отображаемые внутри компонента ‹App /›, должны иметь доступ к состоянию. Теперь посмотрим на само состояние.

Основная концепция состояния Recoil - это атом - источник истины для состояния приложения. Вы можете рассматривать атом как часть большого общего состояния, которое может использоваться компонентами. У каждого атома есть уникальный ключ и значение по умолчанию:

import { atom } from 'recoil';
export const userListState = atom({
    key: 'userListState',
    default: [],
});

В приведенном выше примере userListState по умолчанию является пустым массивом. Для представления состояния по умолчанию могут использоваться различные структуры данных.

export const userFormState = atom({
    key: 'userFormState',
    default: {
        first_name: '',
        last_name: '',
        department: '',
        country: '',        
    }
});

Вышеупомянутый атом представляет состояние формы поиска для фильтрации списка пользователей. Теперь нам нужно подумать, как реализовать саму логику фильтра. Один из вариантов - отправить еще один запрос GraphQL и вернуть отфильтрованный список пользователей со стороны серверной части. Чтобы избежать дополнительной нагрузки на наш API, а также в образовательных целях (поскольку Frontend - это основная тема учебника) мы реализуем фильтрацию на стороне клиента.

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

import { selector } from 'recoil';
import { userListState, userFormState } from './atom';
export const filteredUserListState = selector({
    key: 'filteredUserListState',
    get: ({get}) => {
        const users = get(userListState);
        const filter = {...get(userFormState)};        
        Object.keys(filter).forEach(key => !filter[key] && delete filter[key]);
        if (Object.keys(filter).length) {
            return users.filter(user => {
                for (let key in filter) {
                    if (filter[key] !== user[key]) {
                        return false;
                    }
                }
                return true;
            });
        }
        return users;
    },
});

В папке Todos есть аналогичный селектор для фильтрации элементов по состоянию завершения.

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

Соединение частей вместе

Теперь давайте посмотрим, как все это работает с компонентами React. В этом уроке мы постараемся следовать современной парадигме, основанной на хуках и функциональных компонентах.

В качестве небольшого подготовительного шага мы создаем небольшую служебную библиотеку, чтобы упростить обработку запросов API. Это в значительной степени скопировано из моего старого руководства.

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

import React, { useEffect, useState } from 'react';
import { useSetRecoilState, useRecoilValue } from 'recoil';

useSetRecoilState - используется для изменения состояния или атома отдачи

const setUsers = useSetRecoilState(userListState);

userRecoilValue - для использования значения без его изменения.

const filteredUsers = useRecoilValue(filteredUserListState);

В SearchForm используется еще один полезный хук useRecoilState - он позволяет как использовать, так и изменять состояние, аналогично старому доброму хуку React useState.

const [filterState,setFilterState] = useRecoilState(userFormState);

Теперь вернемся к нашему компоненту UserList.

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

    useEffect(() => {
        const getUsers = async () => {
            const users = await apiService.getUsers();
            setLoading(false);
            setUsers(users);        
        };
        getUsers();
    }, [setLoading, setUsers]);

После загрузки список сохраняется в бите общего состояния userListState. При изменении этого атома отфильтрованный селектор обновляется и возвращает состояние filterUsers или просто полный список пользователей, если не заданы параметры фильтра.

Компонент SearchForm используется для фильтрации результатов в списке пользователей. В этом руководстве форма имеет два состояния - внутреннее состояние, управляемое хуком response useState, который удерживает локальное временное состояние формы, когда пользователь изменяет значение:

    const [formState, setFormState] = useState({});
    /* .... */
    const handleChange = (event) => {
        const {name, value} = event.target;
        const newState = {...formState};
        newState[name] = value;
        setFormState(newState);
    }

И общее состояние - при отправке формы мы «синхронизируем» внутреннее состояние формы с нашим атомом общего состояния.

const [filterState,setFilterState] = useRecoilState(userFormState);
    const handleSubmit = (event) => {
        event.preventDefault();
        const newFilterState = {...filterState, ...formState};
        setFilterState(newFilterState);
    }

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

После изменения значения фильтра наш селектор повторно вычисляет значение, и мы видим список отфильтрованных пользователей. В значительной степени похож на декоратор computed в MobX:

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

Давай попробуем запустить!

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

npm install

(это установит все необходимые пакеты)

node index.js

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

Чтобы запустить клиентскую часть приложения, откройте терминал в новой вкладке, перейдите в папку / client и запустите

npm install

(это установит все необходимые пакеты)

npm start

Он запустит клиентское приложение в режиме разработки, чтобы вы могли внести некоторые изменения в код и сразу увидеть обновленный пользовательский интерфейс. Чтобы увидеть, как он работает, просто откройте браузер и перейдите по адресу http: // localhost: 3000 /.

Некоторые из моих мыслей и выводов

Recoil - относительно новая и уже довольно мощная библиотека государственного управления. Он не предоставляет полный шаблон действий, промежуточного программного обеспечения и редукторов, вместо этого он предоставляет только хранилище для общего состояния (атомов) и набор хуков для подключения компонентов к этому состоянию (аналогично собственному хуку useState ).

Recoil имеет концепцию производного или вычисляемого состояния (селекторов), аналогичную декоратору MobX computed. Производное состояние может быть синхронным или асинхронным, а также может быть связано с компонентами через хуки.

У Recoil, вероятно, более легкая кривая обучения по сравнению с Redux (точно) или MobX для освоения его основных функций. Но обратите внимание, что по сравнению с этими двумя хорошо зарекомендовавшими себя инструментами вам придется самостоятельно реализовать довольно много логики и шаблонов проектирования. В этом отношении он выглядит знакомым для пользователей MobX, потому что MobX фокусируется только на общем хранилище состояний и действиях, не заставляя вас использовать полную предопределенную архитектуру. Хотя Recoil не дает вам полного набора функций MobX, он потенциально может дать вам возможность достичь той же функциональности с гораздо меньшим шаблоном, если правильно скомбинировать его с хуками React. Не требует установки большого количества дополнительных пакетов или поддержки экспериментального декоратора - вам просто понадобится сама Recoil :)

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

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

Давайте будем следить за этим проектом и посмотрим, как он постоянно развивается, становится стабильным и используется во все большем количестве реальных проектов :)

Следующие темы для решения

  • Использование асинхронных селекторов (т.е. авторизованный в данный момент пользователь)
  • Сохранение состояния (экспериментальная возможность сохранять состояние в некотором хранилище и наблюдать изменения в сохраненных атомах)
  • Возможные шаблоны проектирования и структуры приложений с помощью Recoil
  • Модульное тестирование React - приложения Recoil

Спасибо за прочтение! Увидимся в следующих статьях о React.