Привет, Хабр!
Во время разработки веб-приложений мы порой делаем запросы на сервер внутри useEffect прямо в компоненте с визуальным составляющим. Однако не всегда очевидно, что смешивание логики с интерфейсом может привести к усложнению кодовой базы.
В этой статье мы рассмотрим, как неправильное разделение ответственности может негативно сказаться на ваших компонентах, и какие подходы помогут избежать подобных проблем.
Основные проблемы отправки запросов внутри useEffect прямо в компонентах:
-
Сложно отслеживать состояние, так как различные запросы могут зависеть от разных состояний компонента. Это требует внимательности при обработке изменений состояния, чтобы избежать нежелательных повторных запросов.
-
Нарушается принцип разделения ответственности, когда логика запросов смешивается с логикой компонента. Это может сделать код менее читаемым и увеличить связанность между разными частями компонента.
Рассмотрим код со смешанной с визуалом логикой, попытаемся сделать его более лаконичным:
/YourComponent.jsx
import React, { useEffect, useState } from 'react'; import axios from 'axios'; const YourComponent = () => { const [todoId] = useState('1'); const [todoRequest, setTodoRequest] = useState({ data: null, isLoading: false, isError: false }); const [postId] = useState('1'); const [postsRequest, setPostsRequest] = useState({ data: null, isLoading: false, isError: false }); useEffect(() => { const fetchTodos = async () => { try { setTodoRequest((prev)=>({...prev, isLoading: true})); const response = await axios.get(`https://jsonplaceholder.typicode.com/todos/${todoId}`); setTodoRequest((prev)=>({...prev, data: response.data, isLoading: false})); } catch (error) { setTodoRequest((prev)=>({...prev, isLoading: false, isError: true})); } }; fetchTodos(); }, [todoId]); useEffect(() => { const fetchPosts = async () => { try { setPostsRequest((prev)=>({...prev, isLoading: true})); const response = await axios.get(`https://jsonplaceholder.typicode.com/posts/${postId}`); setPostsRequest((prev)=>({...prev, data: response.data, isLoading: false})); } catch (error) { setPostsRequest((prev)=>({...prev, isLoading: false, isError: true})); } }; fetchPosts(); }, [postId]); return ( <div> {todoRequest.isLoading && <p>Loading...</p>} <pre>{todoRequest.data && JSON.stringify(todoRequest.data, null, 2)}</pre> {todoRequest.isError && <h1>Не удалось загрузить задачи</h1>} {postsRequest.isLoading && <p>Loading...</p>} <pre>{postsRequest.data && JSON.stringify(postsRequest.data, null, 2)}</pre> {postsRequest.isError && <h1>Не удалость загрузить статьи</h1>} </div> ); }; export default YourComponent;
Код выглядит достаточно нагружено, и с увеличением количества запросов с различными параметрами будет всё больше походить на спагетти.
Как улучшить читаемость кода не избавляясь от useEffect?
Выносить всю логику отправки запросов в хуки!
Для более легкой поддержки и улучшения читаемости кода нужно как минимум вынести логику запросов в хуки, отделенные от компонента, соблюдая принцип разделения ответственности.
В коде выше довольно много повторяющегося кода, вынесем отслеживание состояния запроса в отдельный хук — useFetch:
/hooks/useFetch.jsx
import { useState, useEffect } from "react"; export const useFetch = ({ requestDeps, requestFn }) => { const [request, setRequest] = useState({ data: null, isLoading: false, isError: false, }); useEffect(() => { const fetchPosts = async () => { try { setRequest((prev) => ({ ...prev, isLoading: true })); const response = await requestFn(); setRequest((prev) => ({ ...prev, data: response.data, isLoading: false, })); } catch (error) { console.log(error); setRequest((prev) => ({ ...prev, isLoading: false, isError: true, })); } }; fetchPosts(); }, [...requestDeps]); return { data: request.data, isLoading: request.isLoading, isError: request.isError, }; };
useFetch принимает в себя функцию с запросом, и возвращает объект с:
-
Данными, возвращенными функцией-запросом
-
Состоянием ожидания ответа
-
Состоянием выполнения запроса с ошибкой
Теперь, взяв за основу useFetch, напишем хуки для получения поста и задачи:
/hooks/usePost.jsx
import { useFetch } from "./useFetch"; import axios from "axios"; export const usePost = ({ postId }) => { return useFetch({ requestDeps: [postId], requestFn: async () => { const data = await axios.get( `https://jsonplaceholder.typicode.com/posts/${postId}`, ); return data; }, }); };
/hooks/useTodo.jsx
import { useFetch } from "./useFetch"; import axios from "axios"; export const useTodo = ({ todoId }) => { return useFetch({ requestDeps: [todoId], requestFn: async () => { const data = await axios.get( `https://jsonplaceholder.typicode.com/todos/${todoId}`, ); return data; }, }); };
Теперь используем наши хуки в компоненте!
/YourComponent.jsx
import React from "react"; import { usePost } from "./hooks/usePost"; import { useTodo } from "./hooks/useTodo"; const YourComponent = () => { const { data: postData, isLoading: isPostLoading, isError: isPostFailed, } = usePost({ postId: "1" }); const { data: todoData, isLoading: isTodoLoading, isError: isTodoFailed, } = useTodo({ todoId: "1" }); return ( <div> {isTodoLoading && <p>Loading...</p>} <pre>{todoData && JSON.stringify(todoData, null, 2)}</pre> {isTodoFailed && <h1>Не удалось загрузить задачи</h1>} {isPostLoading && <p>Loading...</p>} <pre>{postData && JSON.stringify(postData, null, 2)}</pre> {isPostFailed && <h1>Не удалость загрузить статьи</h1>} </div> ); }; export default YourComponent;
Кода в самом компоненте стало в 2 раза меньше, и логика запросов никак не мешается с интерфейсом.
Использование React Query для отправки запросов
React Query — это библиотека, предназначенная для управления состоянием данных в приложении. Она упрощает выполнение запросов к серверу, управление кэшированием данных и обеспечивает интуитивные средства для обработки состояний загрузки и ошибок.
Руководство по установке: https://tanstack.com/query/latest/docs/framework/react/installation
React Query также освобождает нас от написания запросов в useEffect и даёт обширный набор инструментов взаимодействия с состоянием и валидацией запросов.
Перепишем наш код уже с использованием React Query:
useFetch удаляем за ненадобностью, useQuery позаботится за отслеживание состояния запроса вместо нас.
В корневом файле создадим клиент react-query:
/App.jsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import YourComponent from "./app/YourComponent"; const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 10000, }, }, }); export default function App() { return ( <QueryClientProvider client={queryClient}> <YourComponent /> </ QueryClientProvider> ); }
Внутри QueryClient мы можем задать базовые настройки, которые будут по умолчанию использовать все запросы useQuery. В данном случае мы задали по умолчанию staleTime равный 10000.
staleTime — время в миллисекундах, через которое данные считаются устаревшими. При надобности, мы можем прописать отдельным хукам свои значения staleTime.
Подробнее о QueryClient: https://tanstack.com/query/latest/docs/reference/QueryClient
Используем useQuery в наших хуках:
/hooks/usePost.jsx
import axios from "axios"; import { useQuery } from "@tanstack/react-query"; export const usePost = ({ postId }) => { return useQuery({ queryKey: ["post", postId], queryFn: async () => { const { data } = await axios.get( `https://jsonplaceholder.typicode.com/posts/${postId}`, ); return data; }, }); };
/hooks/useTodo.jsx
import axios from "axios"; import { useQuery } from "@tanstack/react-query"; export const useTodo = ({ todoId }) => { return useQuery({ queryKey: ["todo", todoId], queryFn: async () => { const { data } = await axios.get( `https://jsonplaceholder.typicode.com/todos/${todoId}`, ); return data; }, }); };
Компонент YourComponent не поменялся, так как useFetch возвращал похожие на useQuery поля.
Подробнее о useQuery: https://tanstack.com/query/latest/docs/framework/react/reference/useQuery
Пример с использованием invalidateQueries в React Query
Представим ситуацию, в которой после отправки POST запроса с созданием поста нам нужно обновить список всех постов.
Данный функционал можно реализовать с помощью React Query не прибегая к использованию useEffect в компоненте.
Создадим хук usePosts, через который будем получать посты:
/hooks/usePosts.jsx
import { useQuery } from "@tanstack/react-query"; import axios from "axios"; export const usePosts = () => { return useQuery({ queryKey: ["posts"], queryFn: async () => { const { data } = await axios.get( `https://jsonplaceholder.typicode.com/posts/`, ); return data; }, }); };
Создадим хук useCreatePost, с помощью которого будем создавать посты:
/hooks/useCreatePost.jsx
import { useMutation, useQueryClient } from "@tanstack/react-query"; import axios from "axios"; export const useCreatePost = () => { const queryClient = useQueryClient(); return useMutation({ mutationKey: ["create-post"], mutationFn: async ({ postTitle }) => { const { data } = await axios.post( `https://jsonplaceholder.typicode.com/posts`, { title: postTitle, }, ); return data; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["posts"] }); }, }); };
useMutation — хук, обеспечивающий удобный способ выполнения мутаций, таких как создание, обновление или удаление данных, и автоматическое управление состоянием загрузки, ошибок и результата мутаций.
queryClient.invalidateQueries используется для инвалидации (очистки) кэшированных данных для указанных запросов. При вызове этой функции React Query удаляет данные из кэша для указанных запросов, что приводит к их повторному выполнению при следующем обращении к данным. Довольно удобный механизм для обновления кэшированных данных, обеспечивающий актуальность информации. В массив queryKey передаём ключь запроса, который хотим инвалидировать после мутации.
Подробнее о useMutation: https://tanstack.com/query/latest/docs/framework/react/reference/useMutation
Подробнее о queryClient.invalidateQueries: https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientinvalidatequeries
Используем только что созданные хуки в компоненте:
/YourComponent.jsx
import React, { useState } from "react"; import { useCreatePost } from "./hooks/useCreatePost"; import { usePosts } from "./hooks/usePosts"; const YourComponent = () => { const [postTitle, setPostTitle] = useState(""); const { data: postsData, isLoading: isPostsLoading, isError: isPostsFailed, } = usePosts(); const createPost = useCreatePost(); const submitPost = () => createPost.mutate({ postTitle, }); return ( <div> <input value={postTitle} onChange={(e) => setPostTitle(e.target.value)} /> <button onClick={submitPost} disabled={!postTitle.length}> Создать пост </button> {isPostsLoading && <p>Loading...</p>} <pre>{postsData && JSON.stringify(postsData, null, 2)}</pre> {isPostsFailed && <h1>Не удалость загрузить статьи</h1>} </div> ); }; export default YourComponent;
Отправка запросов с помощью хука use в React 19
В новой версии React появится возможность делать запросы без использования useEffect!
Пример использования use из документации:
import { use } from 'react'; function MessageComponent({ messagePromise }) { const message = use(messagePromise); const theme = use(ThemeContext); // ...
Подробнее про use можно почитать в официальной документации.
Итоги
Внимательно планируйте и структурируйте ваши компоненты, чтобы избежать спагетти и кучи ошибок. Поддерживайте чистоту кода, разделяйте логику, группируйте компоненты по функциональности и следите за четкими зависимостями между ними.
Спасибо за внимание!
ссылка на оригинал статьи https://habr.com/ru/articles/796143/
Добавить комментарий