Мы рассмотрим несколько базовых примеров использования библиотеки управления состояниями Valtio в сочетании с React.
Суть этой библиотеки заключается в том, чтобы позволить нам подписываться на проксированное состояние через хук «snapshot» в компоненте React (но не только). Когда мы возвращаем «снимок» от компонента, любая мутация проксированного состояния заставит компонент отрисоваться. Правило таково: читать только из snap
, а писать только в состояние. Кроме того, действия идемпотентны, поэтому бесполезного рендеринга не будет.
import { proxy, useSnapshot } from 'valtio'
import { derive } from 'valtio/utils'
Во-первых, мы оборачиваем состояние с помощью proxy
. Состояний может быть много. Для примера здесь мы рассмотрим следующее состояние:
const state = proxy({
index: 1,
text: null,
message: null
})
Пользовательский хук useSnapshot
создает неизменяемый объект из состояния для передачи компоненту React:
const snap = useSnapshot(state)
Если нам нужно только поле «index», мы можем деструктурировать снимок:
const { index } = useSnapshot(state)
- Пример 1: не реактивный
- Пример 2: реактивный, чтение из привязки.
- Пример 3: «распыляйте» свои компоненты
- Пример 4: запись в состояние, чтение из состояния, еще раз
- Пример 5: мутируем состояние, отображаем с привязкой, снова.
- Пример 6: реакция на fetch данных
- Совет: пространство имен state.
- Пример 7: с SSE на внешние события
Пример 1: не реактивный
Этот компонент не является реактивным, так как мы читаем из изменяемого объекта — состояния.
const Comp1 = ({store}) => <pre>{JSON.stringify(store)}</pre>
<Comp1 store={state}/>
Вместо этого выполните следующее:
Пример 2: реактивный, чтение из привязки.
Этот компонент является реактивным, поскольку мы читаем из snap, неизменяемого объекта, поэтому изменения состояния будут зафиксированы.
const Comp2 = ({store}) => {
const snap useSnapshot(store)
return <pre>{JSON.stringify(snap)}</pre>
}
Пример 3: «распыляйте» свои компоненты
Чтобы ограничить рендеринг, «распыляйте» компоненты
const Comp31 = ({store}) => {
const {index} = useSnapshot(store)
return <>{index}</>
}
const Comp32 = ({store}) => {
const {text} = useSnapshot(store)
return <>{text}</>
}
и используйте его следующим образом:
<Comp31 store={state}/>
<Comp32 store={state}/>
Первый компонент будет отображаться, если мы изменим поле «index» в состоянии, но не будет отображаться при изменении поля «text» и наоборот.
Пример 4: запись в состояние, чтение из состояния, еще раз
Пишите в state — таким образом мутируйте его — и читайте из snap. В частности, используйте состояние в обратных вызовах, а не в привязках.
const Comp4 = ({ store }) => {
const { index } = useSnapshot(store);
return (
<p>
<button onClick={() => ++store.index}>
Increment
</button>
{" "}{index}
</p>
);
};
Пример 5: мутируем состояние, отображаем с привязкой, снова.
Мы мутируем состояние и отображаем некоторые модификации привязки.
const double = nb => nb * 2
const useTriple = (store) => {
const index = useSnapshot(store)
return index * 2
}
const Comp5 = ({store}) => {
const { index } = useSnapshot(store)
const triple = useTriple(store)
return(
<>
<button onClick={() => ++store.index}>{" "}
{index}{" "}{double(index)}{" "}{triple}
</>
)
}
Это будет обновлять состояние при каждом клике и отображать его плюс некоторые вычисления.
Пример 6: реакция на fetch
данных
Предположим, нам нужно заполнить поле с помощью api. Например, получить список «пользователей» под определенным index
из бэкенда. Если мы находимся на одной странице с этим компонентом для заполнения, например, когда мы выбираем его, мы используем useEffect
и обновляем наше локальное состояние для отображения компонента.
Ниже мы будем использовать Valtio для той же цели.
Рассмотрим состояние ниже:
export const commentState = proxy({
comments: null,
setComments: async () =>
(comments.comments = await fetchComments(store.index.value)),
});
и функцию «извлечения», которая может быть примерно такой:
export const fetchComments = async (id) => {
const data = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}/comments`);
return data.json();
};
Мы можем определить кнопку действия в нашем компоненте, которая будет запускать выборку, подписываться с моментальным снимком на состояние «комментарии» и использовать его для рендеринга:
const Comp6 = ({store}) => {
const { comments } = useSnapshot(store)
return(
<>
<button onClick={()=> commentState.setComments()}>Fetch</button>
{comments}
</>
)
}
и использовать ее:
<Comp6 store={commentState}/>
Теперь мы хотим, чтобы наш компонент реагировал на «внешние» проксированные изменения, т.е. изменения, вызванные не внутри компонента (как предыдущая кнопка), а от другого компонента. И снова мы будем опираться на мутацию состояния. Например, предположим, что мы выбрали «индекс», который фиксируется в состоянии «state» (наше первое состояние). Мы вводим «производную» на состояние «state» через get
:
export const userState = derive({
derUsers: async (get) => {
const list = await fetchComments(get(state.index).value);
return list?.map((c) => c.email);
},
});
Осталось использовать это внутри компонента:
const Comp6bis = ({ store }) => {
const { derUsers } = useSnapshot(store);
return <pre>{JSON.stringify(derUsers)}</pre>
};
Поскольку мы выполняем асинхронный вызов, нам нужно приостановить работу компонента:
<React.Suspense fallback={'Loading...'}>
<Comp6bis store={userState} />
</React.Suspense>
Этот компонент будет обновляться всякий раз, когда мы будем изменять значение индекса.
Совет: пространство имен state.
Вместо:
const state = ({
index: null,
users: null
})
используйте:
const state = ({
index: { value: null },
text: null
})
Идея в том, что вы можете использовать get(state.index).value
и ограничить взаимодействие или нежелательный рендеринг.
Пример 7: с SSE
на внешние события
Мы рассмотрим этот пример, поскольку он требует меньше настроек, чем websockets. Предположим, что бэкенд или API посылает Server Sent Events на фронтенд. Сервер SSE передает данные по HTTP в виде потока (где тип события по умолчанию — «сообщение»):
"event: message n data: xxxn id: uuid4nn"
и сообщение отправляется с заголовками:
headers = {
"Content-Type": "text/event-stream",
Connection: "keep-alive",
};
Затем мы реализуем функцию Javascript, которая использует интерфейс Server-Sent-Event со слушателем событий SSE.
Мы можем обработать это в useEffect
:
const Comp6 = () => {
const [msg, setMsg] = React.useState(null);
const handleMsg = (e) => {
setMsg(e.data)
}
React.useEffect(() => {
const source = new EventSource(process.env.REACT_APP_SSE_URL);
source.addEventListener('message', (e) => handleMsg(e)
return () => {
source.removeEventListener("message", handleMsg);
source.close()
};
}, []);
return <>{msg}</>
}
Мы можем сделать то же самое с Valtio, используя derive
. Мы строим производную от состояния «state», которая сохраняет содержимое сообщений в состояние «state»:
export const sseEvents = derive({
getMsg: (get) => {
const evtSource = new EventSource('http://localhost:4000/sse');
evtSource.addEventListener('message', (e) =>
get(state).sse = e.data
)
}
});
где наше состояние:
const state = proxy({
index: null,
[...],
sse: null,
})
Наш компонент будет:
const Comp7 = ({store}) => {
const { sse } = useSnapshot(store);
return <p>{sse}</p>;
}
Используйте его:
<Comp7 store={state}/>
Здесь мы реализуем поддельный эмиттер SSE с помощью Elixir.