Преобразования данных React Query

от автора

Привет, на связи KOTELOV! Мы перевели эту статью, чтобы понять, как эффективно преобразовывать данные при работе с REST API и библиотекой react-query.

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

Но если вы работаете с REST, вы довольствуетесь тем, что возвращает бэкэнд. Так где лучше всего преобразовывать данные при работе с react-query? Универсальный ответ в разработке ПО применим и здесь: «Это зависит от обстоятельств». 

Разберем три подхода к преобразованию данных, их плюсы и минусы.

0. На бэкенде

Мой любимый подход, но везет с ним не всегда. Если бэкэнд возвращает данные именно в той структуре, которая вам нужна, то делать ничего не нужно. Кажется, что этого практически не бывает, но при работе с публичными REST API в корпоративных приложениях такое случается. 

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

🟢 Нельзя работать на фронтенде;

🔴 Не всегда можно использовать.

1. В queryFn

queryFn – это функция, которую вы передаете useQuery. Она работает так: вы вернете Promise, а полученные данные попадут в кэш запросов. Но это не значит, что вы должны обязательно возвращать данные в той структуре, которую предоставляет бэкенд. Вы можете преобразовать их перед этим:

// queryFn-transformation const fetchTodos = async (): Promise<Todos> => {   const response = await axios.get('todos')   const data: Todos = response.data   return data.map((todo) => todo.name.toUpperCase()) }  export const useTodosQuery = () =>   useQuery({     queryKey: ['todos'],     queryFn: fetchTodos,   })

На фронтенде вы можете работать с этими данными «как будто они пришли из бэкенда». Нигде в коде вы не будете работать с именами todo, которые не являются заглавными. У вас также не будет доступа к исходной структуре. Если вы посмотрите на react-query-devtools, вы увидите преобразованную структуру. Если вы посмотрите на данные полученные от сети, вы увидите оригинальную структуру. Это может сбить с толку, поэтому имейте это в виду.

Кроме того, react-query не может ничего оптимизировать. Каждый раз при выполнении выборки будет выполняться преобразование. Если это дорого, рассмотрите одну из других альтернатив. Некоторые компании также имеют общий слой api, который абстрагирует получение данных, поэтому у вас может не быть доступа к этому слою для выполнения преобразований.

🟢 Очень «близко к бэкенду» с точки зрения совместного размещения;

🟡 Преобразованная структура оказывается в кэше, поэтому у вас нет доступа к исходной структуре;

🔴 Выполняется при каждой выборке;

🔴 Нецелесообразно, если у вас есть общий слой api, который вы не можете свободно модифицировать.

2. В функции рендеринга

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

// render-transformation const fetchTodos = async (): Promise<Todos> => {   const response = await axios.get('todos')   return response.data }  const fetchTodos = async (): Promise<Todos> => {   const response = await axios.get('todos')   return response.data }  export const useTodosQuery = () => {   const queryInfo = useQuery({     queryKey: ['todos'],     queryFn: fetchTodos,   })   return {     ...queryInfo,     data: queryInfo.data?.map((todo) => todo.name.toUpperCase()),   } }

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

Будьте осторожны, чтобы определить ваши зависимости как можно более узко. data внутри queryInfo будут ссылочно стабильными, если только что-то действительно не изменилось (в этом случае вы захотите пересчитать ваше преобразование), но сам queryInfo не будет. Если вы добавите queryInfo в качестве зависимости, трансформация будет снова выполняться при каждом рендере:

// useMemo-dependencies export const useTodosQuery = () => {   const queryInfo = useQuery({     queryKey: ['todos'],     queryFn: fetchTodos   })   return {     ...queryInfo,     // не используйте useMemo     data: React.useMemo(       () => queryInfo.data?.map((todo) => todo.name.toUpperCase()),       [queryInfo]     ),     // ✅ корректно запоминает с помощью queryInfo.data     data: React.useMemo(       () => queryInfo.data?.map((todo) => todo.name.toUpperCase()),       [queryInfo.data]     ),   } }

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

Обновление

Поскольку в React Query отслеживаемые запросы включены по умолчанию с версии 4, распространение ...queryInfo больше не рекомендуется, поскольку оно вызывает геттеры для всех свойств.

🟢 Оптимизация через useMemo;

🟡 Точная структура не может быть проверена в devtools;

🔴 Более запутанный синтаксис;

🔴 Данные могут быть потенциально неопределенными;

🔴 Не рекомендуется использовать в отслеживаемых запросах.

3. Использование опции select

В версии 3 появились встроенные селекторы, которые также можно использовать для преобразования данных:

// select-transformation export const useTodosQuery = () =>   useQuery({     queryKey: ['todos'],     queryFn: fetchTodos,     select: (data) => data.map((todo) => todo.name.toUpperCase()),   })

Селекторы будут вызываться только в том случае, если data существуют, поэтому вам не нужно заботиться о undefined. Селекторы, подобные приведенному выше, также будут выполняться при каждом рендере, поскольку функциональная идентичность меняется (это встроенная функция).

Если ваше преобразование дорогостоящее, вы можете мемоизировать его либо с помощью useCallback, либо извлекая его в стабильную ссылку на функцию:

// select-memoizations const transformTodoNames = (data: Todos) =>   data.map((todo) => todo.name.toUpperCase())  export const useTodosQuery = () =>   useQuery({     queryKey: ['todos'],     queryFn: fetchTodos,     // ✅ использует стабильную ссылку на функцию     select: transformTodoNames,   })  export const useTodosQuery = () =>   useQuery({     queryKey: ['todos'],     queryFn: fetchTodos,     // ✅ запоминает с помощью useCallback     select: React.useCallback(       (data: Todos) => data.map((todo) => todo.name.toUpperCase()),       []     ),   })

Кроме того, с помощью опции select можно подписаться только на часть данных. Именно это делает данный подход уникальным. Рассмотрим следующий пример:

// select-partial-subscriptions export const useTodosQuery = (select) =>   useQuery({     queryKey: ['todos'],     queryFn: fetchTodos,     select,   })  export const useTodosCount = () =>   useTodosQuery((data) => data.length)  export const useTodo = (id) =>   useTodosQuery((data) => data.find((todo) => todo.id === id))

Здесь мы создали API типа useSelector, передав пользовательский селектор в наш useTodosQuery. Пользовательские хуки по-прежнему работают как и раньше, поскольку select будет undefined, если вы не передадите его, поэтому будет возвращено все состояние.

Но если вы передаете селектор, то вы подписываетесь только на результат функции селектора. Это довольно эффективное средство, поскольку оно означает, что даже если мы обновим имя todo, наш компонент, который подписывается только на счетчик через useTodosCount, не будет пересматриваться. Счетчик не изменился, поэтому react-query может решить не сообщать этому наблюдателю об обновлении!

🟢 Лучшие оптимизации;

🟢 Позволяет делать частичные подписки;

🟡 Структура может быть разной для каждого наблюдателя;

🟡 Структурное разделение выполняется дважды.

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


ссылка на оригинал статьи https://habr.com/ru/articles/824690/