Recoil
Recoil - это инст румент для управления состоянием React-приложений. Он позволяет создавать граф потока данных (data-flow graph), распространяющихся от атомов (atoms) через селекторы (selectors) (чистые функции) в компоненты. Атомы - это единицы состояния, на которые компоненты могут подписываться. Селекторы преобразуют состояние синхронным или асинхронным способом.
Атомы (atoms)
Атомы - это единицы состояния. Они являются обновляемыми и на них можно подписываться: при обновлении атома, каждый подписанный на него компонент перерисовывается с новым значением. Они могут создаваться во время выполнения. Атомы могут использоваться вместо локального состояния компонента. Если одни и те же атомы используются несколькими компонентами, состояние, содержащееся в таких атомах, является распределенным.
Атомы создаются с помощью функции atom()
:
const fontSizeState = atom({
key: 'fontSizeState',
default: 14
})
Атому передается уникальный ключ, который используется для отладки, обеспечения согласованности и в некоторых продвинутых API, позволяющих получать карту всех атомов, используемых в приложении. Данные ключи должны быть глобально уникальными, в противном случае, выбрасывается исключение. Как и состояние компонента, они также имеют значение по умолчанию.
Для чтения и записи в атом из компонента используется хук useRecoilState()
. Это как useState()
, но состояние может распределяться между несколькими компонентами:
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState)
return (
<button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>
Увеличить размер шрифта
</button>
)
}
Нажатие кнопки приводит к увеличению размера шрифта на единицу. Другие компоненты также могут использовать это состояние:
function Text() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState)
return <p style={{fontSize}}>Этот текст также будет увеличиваться в размерах</p>
}
Селекторы (selectors)
Селектор - это чистая функция, принимающая атомы или другие селекторы. При обновлении переданных атомов или селекторов, функция селектора вычисляется повторно. Компоненты могут подписываться на селекторы также, как на атомы, они будут перерисовываться при изменении селекторов.
Селекторы используются для вычисления производных данных на основе состояния. Это позволяет избежать избыточности состояния, поскольку в атомах хранится минимальный набор состояния, все остальное эффективно вычисляется с помощью функций. Поскольку се лекторы следят за подписанными на них компонентами, подход, основанный на функциях, является очень эффективным.
Атомы и селекторы имеют одинаковый интерфейс и являются взаимозаменяемыми.
Селекторы определяются с помощью функции selector()
:
const fontSizeLabelState = selector({
key: 'fontSizeLabelState',
get: ({ get }) => {
const fontSize = get(fontSizeState)
const unit = 'px'
return `${fontSize}${unit}`
}
})
Свойство get
- это функция, подлежащая вычислению. Она может извлекать значения из атомов и д ругих селекторов с помощью аргумента get
. При доступе к другому атому или селектору, создается зависимость: обновление атома или селектора-зависимости приводит к обновлению селектора.
В приведенном примере селектор fontSizeLabelState
имеет одну зависимость: атом fontSizeState
. Концептуально, fontSizeLabelState
представляет собой чистую функцию, принимающую fontSizeState
на вход и возвращающую форматированную подпись размера шрифта на выходе.
Селекторы можно читать с помощью хука useRecoilValue()
, принимающего атом или селектор в качестве аргумента и возвращающего соответствующее значение. Мы не используем useRecoilState()
, поскольку селектор fontSizeLabelState
доступен только для чтения:
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState)
const fontSizeLabel = useRecoilValue(fontSizeLabelState)
return (
<>
<div>Текущий размер шрифта: {fontSizeLabel}</div>
<button onClick={() => setFontSize(fontSize + 1)} style={{fontSize}}>
Увеличить размер шрифта
</button>
</>
)
}
Нажатие на кнопку теперь приводит к следующему: во-первых, увеличивается размер шрифта кнопки, во-вторых, обновляется подпись, отражающая текущий размер шрифта.
Начало работы
Create React App
Recoil
- это библиотека для управления состоянием в React-приложениях, поэтому использование Recoil
предполагает установку React
. Самый простой способ это сделать заключается в использова нии Create React App
:
yarn create react-app my-app
# или
npm init create-app my-app
# или
npx create-react-app my-app
Установка
yarn add recoil
# или
npm i recoil
RecoilRoot
Компоненты, использующие состояние Recoil
должны быть обернуты в RecoilRoot
. Подходящим местом для этого является корневой компонент приложения:
import React from 'react'
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
} from 'recoil'
const App() => (
<RecoilRoot>
<CharacterCounter />
</RecoilRoot>
)
Атом
Атом представляет собой часть состояния. Атомы доступны для любого компонента. Компоненты, извлекающие значение из атома, неявно на него подписываются: обновление атома приводит к повторному рендерингу подписанных на него компонентов:
const textState = atom({
key: 'textState', // уникальный ID (по сравнению с другими атомами/селекторами)
default: '' // дефолтное (начальное) значение
})
Для извлечения и записи значений в атомы компоненты должны использовать хук useRecoilState()
:
const CharacterCounter = () => (
<div>
<TextInput />
<CharacterCount />
</div>
)
function TextInput() {
const [text, setText] = useRecoilState(textState)
const onChange = (event) => {
setText(event.target.value)
}
return (
<div>
<input type="text" value={text} onChange={onChange} />
<br />
Текст: {text}
</div>
)
}
Селектор
Селектор представляет собой часть производного состояния. Производное состояние - это преобразованный вариант исходного состояния. Вы можете думать о производном состоянии как о результате передачи оригинального состояния в чистую функцию, модифицирующую состояние каким-либо образом:
const charCountState = selector({
key: 'charCountState', // уникальный ID
get: ({get}) => {
const text = get(textState)
return text.length
},
})
Для извлечения значения из charCharacterState
используется хук useRecoilValue()
:
function CharacterCount() {
const count = useRecoilValue(charCountState)
return <>Количество символов: {count}</>
}
Туториал
В данном туториале мы создадим простое приложение - список задач. Функционал нашего приложения будет следующим:
- Добавление задач в список
- Редактирование задач
- Удаление задач
- Фильтрация задач
- Отображение полезной статистики
Предполагается, что вы установили React
и Recoil
, а такж е обернули корневой компонент в RecoilRoot
.
Атомы
Атомы содержат источник истины для состояния приложения. Для нашей тудушки источником истины будет массив объектов, где каждый объект - это задача (элемент списка).
Создаем атом списка todoListState
с помощью функции atom()
:
const todoListState = atom({
key: 'todoListState',
default: []
})
Мы передаем атому уникальный ключ и устанавливаем пустой массив в качестве дефолтного значения. Для извлечения значений из атома мы используем хук useRecoilValue()
в компоненте TodoList
:
function TodoList() {
const todos = useRecoilValue(todoListState)
return (
<>
<TodoListStats />
<TodoListFilters />
<TodoItemCreator />
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</>
)
}
Для создания новой задачи необходимо получить функцию-сеттер для обновления состояния todoListState
. Для этого в компоненте TodoListCreator
воспользуемся хуком useSetRecoilState()
:
function TodoItemCreator() {
const [text, setText] = useState('')
const setTodos = useSetRecoilState(todoListState)
const addTodo = () => {
const trimmed = text.trim()
if (!trimmed) return
const newTodo = {
id: getId(),
text: trimmed,
isComplete: false
}
setTodos((oldTodos) => oldTodos.concat(newTodo))
}
const changeText = ({target: {value}}) => {
setText(value)
}
return (
<div>
<input type="text" value={text} onChange={changeText} />
<button onClick={addTodo}>Add</button>
</div>
)
}
// утилита для генерации уникального ID
let id = 0
const getId = () => id++
Обратите внимание, что мы используем обновляющую форму сеттера, поэтому имеем возможность создать новый список на основе старого.
Компонент TodoItem
будет отображать значение элемента списка, позволяя изменять текст задачи и удалять ее из списка. Для извлечения значения из todoListState
и получения сеттера для обновления текста, выполнения и удаления задачи мы снова используем useRecoilState()
:
function TodoItem({ todo }) {
const [todos, setTodos] = useRecoilState(todoListState)
const { id, text, isComplete } = todo
const toggleTodo = () => {
const newTodos = todos.map(todo => todo.id === id ? {...todo, isComplete: !todo.isComplete} : todo)
setTodos(newTodos)
}
const updateTodo = ({target: {value}}) => {
const trimmed = value.trim()
if (!trimmed) return
const newTodos = todos.map(todo => todo.id === id ? {...todo, text: value} : todo)
setTodos(newTodos)
}
const deleteTodo = () => {
const newTodos = todos.filter(todo => todo.id !== id)
setTodos(newTodos)
}
return (
<div>
<input
type="checkbox"
checked={isComplete}
onChange={toggleTodo}
/>
<input type="text" value={text} onChange={updateTodo} />
<button onClick={deleteTodo}>X</button>
</div>
)
}
Селекторы
Селектор представляет собой часть производного состояния.
Производное состояние - мощная концепция, позволяющая динамически генерировать данные на основе других данных. В контексте списка задач следующие данные являются производными:
- Отфильтрованный список задач: является производным от оригинального списка посредством создания нового списка с задачами, отфильтрованными по определенному критерию (такому как отметка о выполнении)
- Статистика списка задач: является производным от оригинального списка посредством вычисления полезных атрибутов списка, таких как общее количество задач в списке, количество и процент выполненных задач
Для реализации отфильтрованного списка необходимо выбрать критерий, значение которого может быть сохранено в атоме. Мы будет использовать следующие фильтры: "Show All", "Show Completed" и "Show Active". Значением по умолчанию будет "Show All":
const todoListFilterState = atom({
key: 'todoListFilterState',
default: 'Show All'
})
С помощью todoListFilterState
и todoListState
мы можем создать селектор filteredTodoListState
, генерирующий отфильтрованный список:
const filteredTodoListState = selector({
key: 'filteredTodoListState',
get: ({ get }) => {
const filter = get(todoListFilterState)
const todos = get(todoListState)
switch (filter) {
case 'Show Completed':
return todos.filter(todo => todo.isComplete)
case 'Show Active':
return todos.filter(todo => !todo.isComplete)
default:
return todos
}
}
})
filteredTodoListState
имеет две зависимости: todoListFilterState
и todoListState
: селектор повторно вычисляется при изменении любой из них.
Для того, чтобы отображать отфильтрованный список задач, необходимо внести небольшое изменение в компонент TodoList
:
function TodoList() {
const todos = useRecoilValue(filteredTodoListState)
return (
<>
<TodoListStats />
<TodoListFilters />
<TodoItemCreator />
{todos.map((todo) => (
<TodoItem todo={todo} key={todo.id} />
))}
</>
)
}
Обратите внимание, что в UI отображаются все задачи, поскольку дефолтным значением todoListFilterState
является "Show All". Для того, чтобы иметь возможность изменять фильтр, необходимо реализовать компонент TodoListFilters
:
function TodoListFilters() {
const [filter, setFilter] = useRecoilState(todoListFilterState)
const changeFilter = ({target: {value}}) => {
setFilter(value)
}
return (
<>
<span>Фильтр:</span>
<select value={filter} onChange={updateFilter}>
<option value="Show All">Все</option>
<option value="Show Completed">Выполненные</option>
<option value="Show Active">Активные</option>
</select>
</>
)
}
С помощью нескольких строк кода иы реализовали фильтрацию списка задач! Аналогичный подход будет использован для реализации компонента TodoListStats
.
Мы хотим отображать следующую статистику:
- Общее количество задач
- Количество выполненных задач
- Количество активных задач
- Процент выполненных задач
Мы могли бы создать селектор для каждого значения, но проще создать один селектор, возвращающий объект со всеми данными. Мы назовем его todoListStatsState
:
const todoListStatsState = selector({
key: 'todoListStatsState',
get: ({ get }) => {
const todos = get(todoListState)
const total = todos.length
const completed = todos.filter((todo) => todo.isComplete).length
const active = total - completed
const percent = total === 0 ? 100 : Math.round((active / total) * 100) + '%'
return {
total,
completed,
active,
percent
}
}
})
Для извлечения значений из todoListStatsState
мы снова воспользуемся useRecoilValue()
:
function TodoLIstStats() {
const {
total,
completed,
active,
percent
} = useRecoilValue(todoListStatsState)
return (
<ul>
<li>Общее количество задач: {total}</li>
<li>Количество выполненных задач: {completed}</li>
<li>Количество активных задач: {active}</li>
<li>Процент выполненных задач: {percent}</li>
</ul>
)
}
Таким образом, мы легко и просто реализовали список задач, отвечающий всем заявленным требованиям.
Асинхронное получение данных
Recoil
обеспечивает возможность связывать состояние и производное состояние с компонентами через граф потока данных. При этом, функции графа могут быть асинхронными. Это позволяет использовать асинхронные функции в синхронном методе render()
компонентов. Recoil
позволяет смешивать синхронные и асинхронные функции в графе потока данных селекторов. Для этого достаточно вернуть промис вместо значения в колбеке get()
селектора. Поскольку асинхронные селекторы остаются селекторами, другие селекторы могут полагаться на них для дальнейшего преобразования данных.
Обратите внимание, что селекторы являются "идемпотентными" функциями: для указанного набора входных данных они всегда должны возвращать одинаковый результат (по крайней мере, в течение жизненного цикла приложения). Это важно, поскольку вычисления селекторов могут кэшироваться, перезапускаться или выполняться несколько раз. Учитывая изложенное, селекторы - это хороший способ моделирования доступных только для чтения запросов к базам данных. Для мутируемых данных можно использовать Query Refresh
, а для синхронизации мутируемого состояния, сохранения состояния или выполнения других побочных эффектов - экспериментальный Atom Effects API
.
Синхронный пример
Ниже приводится пример синхронного получения имени пользователя с помощью атома и селектора:
const currentUserIDState = atom({
key: 'CurrentUserID',
default: 1,
})
const currentUserNameState = selector({
key: 'CurrentUserName',
get: ({get}) => {
return tableOfUsers[get(currentUserIDState)].name
},
})
function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameState)
return <div>{userName}</div>
}
function MyApp() {
return (
<RecoilRoot>
<CurrentUserInfo />
</RecoilRoot>
)
}
Асинхронный пример
Если имя пользователя хранится в БД, нам требуется строка запроса (query). Все, что нам нужно сделать, это вернуть промис или воспользоваться асинхронной функцией. При изменении любой зависимости селектор будет вычислен повторно и выполнит новый запрос. Результаты кэшируются, поэтому запрос будет выполнен только один раз для каждого случая.
const currentUserNameQuery = selector({
key: 'CurrentUserName',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
})
return response.name
},
})
function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery)
return <div>{userName}</div>
}
Интерфейс селектора остается прежним, поэтому использующему его компоненту не нужно заботиться о том, с чем он имеет дело, с синхронным состоянием атома, производным состоянием селектора или асинхро нным запросом.
Однако, поскольку функция render()
является синхронной, может получиться так, что компонент будет отрендерен до разрешения промиса. Recoil
специально спроектирован для работы с React Suspense
для обработки данных, находящихся на стадии получения. Оборачивание компонента в предохранитель Suspense
позволит отображать резервный UI до получения данных:
function MyApp() {
return (
<RecoilRoot>
<React.Suspense fallback={<div>Загрузка...</div>}>
<CurrentUserInfo />
</React.Suspense>
</RecoilRoot>
)
}
Обработка ошибок
Но что если запрос завершился ошибкой? Селекторы могут выбрасывать исключения, которые будут переданы дальше при попытке компонента использовать это значение. Такие исключения могут быть обработаны с помощью ErrorBoundary
(предохранителя). Например:
const currentUserNameQuery = selector({
key: 'CurrentUserName',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
})
if (response.error) {
throw response.error
}
return response.name
},
})
function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery)
return <div>{userName}</div>
}
function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Загрузка...</div>}>
<CurrentUserInfo />
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
)
}
Запросы с параметрами
Порой может потребоваться отправить запрос с дополнительными параметрами, не зависящими от производного состояния. Например, может потребоваться отправить запрос на основе пропов компонента. Это можно сделать с помощью утилиты selectorFamily()
:
const userNameQuery = selectorFamily({
key: 'UserName',
get: userID => async () => {
const response = await myDBQuery({userID})
if (response.error) {
throw response.error
}
return response.name
},
})
function UserInfo({userID}) {
const userName = useRecoilValue(userNameQuery(userID))
return <div>{userName}</div>
}
function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Загрузка...</div>}>
<UserInfo userID={1}/>
<UserInfo userID={2}/>
<UserInfo userID={3}/>
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
)
}
Граф потока данных
Моделируя запросы в виде селекторов, мы можем смешивать состояние, производное состояние и запросы. При обновлении состояния, граф автоматически обновляется, что приводит к повторному рендерингу компонентов.
В приведенном ниже примере рендерится имя текущего пользователя и список его друзей. При клике по имени одного из друзей, он становится текущим пользователем, а имя и список автоматически обновляются:
const currentUserIDState = atom({
key: 'CurrentUserID',
default: null,
})
const userInfoQuery = selectorFamily({
key: 'UserInfoQuery',
get: userID => async () => {
const response = await myDBQuery({userID})
if (response.error) {
throw response.error
}
return response
},
})
const currentUserInfoQuery = selector({
key: 'CurrentUserInfoQuery',
get: ({get}) => get(userInfoQuery(get(currentUserIDState))),
})
const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({get}) => {
const {friendList} = get(currentUserInfoQuery)
return friendList.map(friendID => get(userInfoQuery(friendID)))
},
})
function CurrentUserInfo() {
const currentUser = useRecoilValue(currentUserInfoQuery)
const friends = useRecoilValue(friendsInfoQuery)
const setCurrentUserID = useSetRecoilState(currentUserIDState)
return (
<div>
<h1>{currentUser.name}</h1>
<ul>
{friends.map(friend =>
<li key={friend.id} onClick={() => setCurrentUserID(friend.id)}>
{friend.name}
</li>
)}
</ul>
</div>
)
}
function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Загрузка...</div>}>
<CurrentUserInfo />
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
)
}
Параллельные запросы
В приведенном примере friendsInfoQuery
использует запрос для получения информации о каждом друге. Это происходит в цикле. Если поисковая таблица небольшая, тогда все в порядке. Если вычисления являются дорогими, тогда можно использовать утилиту waitForAll()
для одновременного выполнения запросов. Данная вспомогательная функция принимает массив и именованый объект зависимостей:
const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({ get }) => {
const { friendList } = get(currentUserInfoQuery)
const friends = get(waitForAll(
friendList.map(friendID => userInfoQuery(friendID))
))
return friends
},
})
Для дополнительного обновления UI частичными данными можно использовать waitForNone()
:
const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({ get }) => {
const { friendList } = get(currentUserInfoQuery)
const friendLoadables = get(waitForNone(
friendList.map(friendID => userInfoQuery(friendID))
))
return friendLoadables
.filter(({state}) => state === 'hasValue')
.map(({contents}) => contents)
},
})
Предварительные запросы
В целях повышения производительности можно выполнять запросы перед рендерингом. Изменим приведенный выше пример для получения информации о следующем пользователе сразу после нажатия пользователем соответствующей кнопки:
function CurrentUserInfo() {
const currentUser = useRecoilValue(currentUserInfoQuery)
const friends = useRecoilValue(friendsInfoQuery)
const changeUser = useRecoilCallback(({snapshot, set}) => userID => {
snapshot.getLoadable(userInfoQuery(userID)) // предварительный запрос
set(currentUserIDState, userID) // меняем текущего пользователя для начала нового рендеринга
})
return (
<div>
<h1>{currentUser.name}</h1>
<ul>
{friends.map(friend =>
<li key={friend.id} onClick={() => changeUser(friend.id)}>
{friend.name}
</li>
)}
</ul>
</div>
)
}
Запрос дефолтных значений атома
Обычно, атомы используются для хранения локального обновляемого состояния. Однако, для запроса дефолтных значений атома можно использовать селектор:
const currentUserIDState = atom({
key: 'CurrentUserID',
default: selector({
key: 'CurrentUserID/Default',
get: () => fetchCurrentUserID(),
}),
})
Асинхронные запросы без React Suspense
Для обработки асинхронный запросов, находящихся на стадии разрешения, не обязательно использовать Suspense
. Вместо этого, для определения статуса в процессе рендеринга можно использовать хук useRecoilValueLoadable()
:
function UserInfo({userID}) {
const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID))
switch (userNameLoadable.state) {
case 'hasValue':
return <div>{userNameLoadable.contents}</div>
case 'loading':
return <div>Загрузка...</div>
case 'hasError':
throw userNameLoadable.contents
}
}
Обновление запроса
При использовании селекторов для моделирования запросов данных, важно помнить о том, что вычисление селектора всегда должно заканчиваться предоставлением согласованного значения для соответствующего состояния. Селекторы представляют состояние, производное от состояния атома или другого селектора. Поэтому селекторы должны быть идемпотентными по отношению к входным данным, поскольку они могут кэшироваться и выполняться неоднократно. На практике это означает, что один и тот же селектор не должен использоваться для выполнения запроса, когда мы ожидаем, что результаты будут разными в зависимости от жизненного цикла приложения.
Существует несколько паттернов для работы с мутирующими данными:
Использование идентификатора запроса
Вычисление селектора должно завершаться согласованным значением для определенного состояния на основе входных данных (зависимое состояние или параметры семьи (family parameters)). Поэтому в селектор можно добавлять ID запроса в качестве параметра семьи или зависимости:
const userInfoQueryRequestIDState = atomFamily({
key: 'UserInfoQueryRequestID',
default: 0,
})
const userInfoQuery = selectorFamily({
key: 'UserInfoQuery',
get: userID => async ({get}) => {
get(userInfoQueryRequestIDState(userID)) // Добавляем ID запроса в качестве зависимости
const response = await myDBQuery({userID})
if (response.error) {
throw response.error
}
return response
},
})
function useRefreshUserInfo(userID) {
setUserInfoQueryRequestID = useSetRecoilState(userInfoQueryRequestIDState(userID))
return () => {
setUserInfoQueryRequestID(requestID => requestID + 1)
}
}
function CurrentUserInfo() {
const currentUserID = useRecoilValue(currentUserIDState)
const currentUserInfo = useRecoilValue(userInfoQuery(currentUserID))
const refreshUserInfo = useRefreshUserInfo(currentUserID)
return (
<div>
<h1>{currentUser.name}</h1>
<button onClick={refreshUserInfo}>Обновить</button>
</div>
)
}
Использование атома
Другой возможностью является использование атома вместо селектора для моделирования результатов запроса. Это позволяет императивно обновлять состояние атома новыми результатами запроса на основе выбранной стратегии обновления:
const userInfoState = atomFamily({
key: 'UserInfo',
default: userID => fetch(userInfoURL(userID)),
})
// Компонент для обновления запроса
function RefreshUserInfo({userID}) {
const refreshUserInfo = useRecoilCallback(({set}) => async id => {
const userInfo = await myDBQuery({userID})
set(userInfoState(userID), userInfo)
}, [userID])
// Обновлять информацию о пользователе каждую секунду
useEffect(() => {
const intervalID = setInterval(refreshUserInfo, 1000)
return () => clearInterval(intervalID)
}, [refreshUserInfo])
return null
}
Одним из недостатков данного подхода является то, что в настоящее время атомы не поддерживают получение промиса в качестве нового значения, поэтому отсутствует возможность использовать Suspense
в ожидании разрешения запроса.
Атомарные эффекты (Atom Effects)
Атомарные эффекты - это новый экспериментальный API для управления побочными эффектами и инициализации атомов. Он может использоваться для сохранения, синхронизации состояния, управления историей, логгирования и т.д. Атомарные эффекты определяются как часть определения атома, поэтому каждый атом может иметь собственные эффекты. Данный API находится в разработке, поэтому помечен как _UNSTABLE
.
Атомарные эффекты подключаются к атомам с помощью опции effects_UNSTABLE
. Каждый атом может иметь массив эффектов, которые вызываются при инициализации атома. Атомы инициализируются при первом использовании в RecoilRoot
, но могут инициализироваться повторно после очистки. Атомарный эффект может возвращать обработчик очистки (cleanup handler) для выполнения побочных эффектов, связанных с очисткой:
const myState = atom({
key: 'MyKey',
default: null,
effects_UNSTABLE: [
() => {
...effect1...
return () => ...cleanup effect1...
},
() => { ...effect2... }
]
})
Семьи атомов также поддерживают параметризованные и непараметризованные эффекты:
const myStateFamily = atomFamily({
key: 'MyKey',
default: null,
effects_UNSTABLE: param => [
() => {
...effect1, использующий параметр...
return () => ...cleanup effect 1...
},
() => { ...effect2, использующий параметр... },
],
})
Сравнение с эффектами React
Атомарные эффекты могут быть реализованы с помощью useEffect()
. Тем не менее, атомы создаются за пределами контекста React
, поэтому управлять эффектами из React-компонентов может быть непросто, особенно в случае динамически создаваемых атомов. Данный подход также нельзя использоваться для инициализации начального значения атома или при рендеринге на стороне сервера. Использование атомарных эффектов также позволяет держать определение атома и его эффектов в одном месте:
const myState = atom({key: 'Key', default: null})
function MyStateEffect(): {
const [value, setValue] = useRecoilState(myState)
useEffect(() => {
// Эффект запускается при изменении значения атома
store.set(value)
store.onChange(setValue)
return () => { store.onChange(null) } // Эффект очистки
}, [value])
return null
}
function MyApp(): {
return (
<div>
<MyStateEffect />
</div>
)
}