React с менеджером состояний Valtio, несколько примеров с fetch и SSE


Мы рассмотрим несколько базовых примеров использования библиотеки управления состояниями 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: не реактивный

Этот компонент не является реактивным, так как мы читаем из изменяемого объекта — состояния.

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>
};
Enter fullscreen mode Выйти из полноэкранного режима

Поскольку мы выполняем асинхронный вызов, нам нужно приостановить работу компонента:

<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.

Оцените статью
devanswers.ru
Добавить комментарий