Сегодня я хочу вам представить относительно новую библиотеку для управления глобальным состоянием приложения – Recoil JS. Как и React является open-source проектом при поддержке разработчиков из Meta (запрещенная в России экстремистская организация). Пока что является экспериментальной, на момент написании статьи имеет версию 0.4.1.

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

Так же стоит отметить относительно малый размер библиотеки, хотя с новыми версиями это может измениться.

Мое ручное сравнение размера библиотек.
Мое ручное сравнение размера библиотек.

Основные принципы и примеры

На примере небольшого ToDo приложения я постараюсь показать функционал RecoilJS. Ссылка на код будет в конце статьи.

Recoil состояние работает на атомах и селекторах.

У нас есть React компоненты, которые лежат, так сказать, в слое самого приложения, и над всем расположен Recoil store, который состоит из похожих на React компоненты атомов. Атом должен быть с уникальным ключом, что помогает самому приложению, и в отладке. Если нужно создать какую-то группу атомов, нужно использовать atomFamily, который под одним ключом будет хранить несколько атомов. Ниже пример самого обычного атома, который мы будем использовать для определения темы приложения.

type Theme = 'light' | 'dark'

const theme = atom<Theme>({
    key: 'theme',
    default: 'light',
});

Атом может передавать свое состояние в любую часть React приложения при помощи useRecoilState, который по сути является аналогией useState. С помощью данного хука можно использовать состояние атома или изменить его. Если состояние изменится, то все компоненты, который используют данный атом, то есть подписаны на его изменение, тоже поменяют свое состояние на новое.

export const useFooterStyles = (): CSSProperties => {
    const appTheme = useRecoilValue(theme);
    return  {
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        backgroundColor: appTheme === 'light' ? '#AAD5FF' : '#4A7496',
    }
};

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

type ThemeLabelButton = 'Light Theme' | 'Dark Theme'

export const themeLabelButton = selector<ThemeLabelButton>({
    key: 'themeLabel',
    get: ({ get }) => {
        const appTheme = get(theme);
        return `${appTheme === 'light' ? 'Dark' : 'Light'} Theme`;
    }
})
const style = useSwitchThemeStyles();

return (
  <div style={style}>
    <Button
      style={style}
      type="primary"
      onClick={() => {
        setAppTheme((currVal) => currVal === 'light' ? 'dark' : 'light')
      }}
    >
      {appThemeLabel}
    </Button>
  </div>
)

Так же селектор можно использовать для асинхронных запросов. Для обработки ошибок разработчики библиотеки советуют использовать <ErrorBoundary>, а для индикатора загрузки либо React. Suspense, либо хук useRecoilValueLoadable. Ниже пример с React.Suspense. Ниже селектор для загрузки данных.

type ToDoList = { content: Array<ToDoListElem>; error: boolean }
type ToDoListElem = { userId: number; id: number; title: string; completed: boolean }

const toDoList = selector<ToDoList>({
    key: 'ToDoList',
    get: async () => {
        const res = await fetch('https://jsonplaceholder.typicode.com/users/1/todos');
        if (res.status !== 200) {
            return { content: [], error: true };
        }
        return { content: await res.json(), error: false };
    }
});

В компоненте он может использоваться, как и обычный селектор.

Если нужно отправить запрос в зависимости от параметра, то необходимо использовать selectorFamily с немного измененным get полем.

export const toDoListQuery = selectorFamily<ToDoList, { userId: number }>({
    key: 'ToDoListQuery',
    get: query => async () => {
        const res = await fetch(`https://jsonplaceholder.typicode.com/users/${query.userId}/todos`);
        if (res.status !== 200) {
            return { content: [], error: true };
        }
        return { content: await res.json(), error: false };
    }
});

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

export const Content = (): JSX.Element => {
    const style = useContentStyles();
    const [userId, setUserId] = useState<number>(1)
    const appToDoList = useRecoilValue(toDoListQuery({ userId }));

    return <Layout.Content style={style.content}>
        {appToDoList.content.map((elem) => <Card title={elem.title} style={style.item} headStyle={style.headItem}>
            {elem.completed ? 'Completed' : 'Not Completed'}
        </Card>)}
        <Button onClick={() => setUserId((prevState) => prevState + 1)}>Next</Button>
    </Layout.Content>
}

Так же достаточно много дополнительного функционала, по типу useEffect для атома, useRecoilCallback (альтернатива useCallback) со Snapshots, useRecoilTransaction для Batching-а и прочее. Сильно видна связь данной библиотеки и React-а.

В целом работу Recoil с React компонентами можно описать одной картинкой.

Заключение

Recoil еще слишком молодая библиотека и имеет много проблем, при неправильном использовании можно нагородить большое количество ошибок, к примеру, нужно сразу установить принцип выбора ключей в большом проекте (например `${nameOfPage}Theme`, а глобальные называть как обычно), нужно следить за тем, чтобы атом менялся либо через селектор, либо через свой собственный setRecoilState.

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

Ссылки

Проект

Официальная документация

Обзор на основные принципы библиотеки

Комментарии (5)


  1. markelov69
    22.03.2022 13:53
    +3

    Мда. Сколько угодно будем страдать фигней и не пользоваться getters/setters и Proxy. Уже много лет есть MobX, зачем делать эти бесполезные поделки времен мамонтов, когда c 2010 года есть getters/setters в JS объектах? А с 2015 года вообще есть Proxy.


  1. JustDont
    22.03.2022 14:08

    График с подписью "Мб" по оси значений вызывает просто таки умиление. Что именно вы им пытаетесь сказать? Что редакс с тулкитом весят 10 Мб? Может не стоит опускаться до столь беспощадного инфоцыганства?


  1. fomiash
    22.03.2022 16:11
    +2

    Recoil JS. Как и React является open-source проектом при поддержке разработчиков из Meta*

    *организация, признанная экстремистской на территории Российской Федерации

    На всякий случай рекомендовал бы добавить.


    1. JustDont
      22.03.2022 16:57

      Но ведь, но ведь… хабр вне политики!
      *и притопнул ножкой*


    1. sogarkov
      23.03.2022 09:07
      +1

      Опередили. Автор, не подставляй хабр.