Опановуємо CRUD в Next.js. Поради для інтеграції та оптимізації
Вітаю! Я CTO та співзасновник компанії Brocoders. Також я є технічним ментором Startup Wise Guys та маю свій стартап-досвід. В Brocoders ми займаємось розробкою веб та мобільних застосунків. Також розробляємо опенсорс-рішення для стандартизації і прискорення розробки вебзастосунків bcboilerplates.com і намагаємось ділитись своїм досвідом в україномовній спільноті програмістів. Ну і про англомовних не забуваємо.
Цей матеріал народився з нашого написання фронтенд-бойлерплейту та бажання поділитись цим досвідом.
У світі веб-розробки операції CRUD є основними будівельними блоками та мають вирішальне значення для керування даними. Вони присутні практично в усіх програмах, від простих сайтів до складних корпоративних рішень.
Користувачі NestJS Boilerplate вже мали змогу оцінити і взяти у використання новий потужний інструмент CLI, який дозволяє автоматично створювати ресурси та їх властивості. Завдяки ньому можна створити всі CRUD операції і додати до них необхідні поля, не написавши жодного рядка коду вручну. Оскільки ми вже не одночасно оголошували, екосистема bc boilerplates має повністю сумісний з ним Extensive-React-Boilerplate для забезпечення повноцінного функціоналу, який, в принципі, може бути цілком незалежним рішенням, тож погляньмо тепер на роботу з CRUD-операціями з фронтентенд-сторони.
Як ми знаємо, абревіатура CRUD означає створення, читання, оновлення та видалення, тому ця концепція представляє основні операції, які ви можете виконувати з будь-якими даними. Розглянемо роботу з CRUD-операціями на прикладі користувача адміністративної панелі, де реалізована можливість додавати, редагувати та видаляти нових користувачів, а також отримувати про них інформацію.
Всі наведені нижче кастомні React-хуки, робота з обробкою даних у React Query, пагінація, обробка помилок тощо, уже є імплантованими в Extensive-React-Boilerplate. І ви, звісно, можете цим скористатися. А нижче ми через призму нашого досвіду розглянемо реалізацію цих пунктів.
Операція створення (Create Operation)
Сценарій використання: надсилання даних для створення нового ресурсу (наприклад, реєстрація користувача, додавання нового продукту).
Реалізація: збір даних із форми, надсилання POST-запиту до сервера, обробка відповіді та оновлення інтерфейсу відповідно.
Розглянемо приклад. Створенням нового користувача реалізується надсиланням POST-запиту до API. У наведеному нижче фрагменті хук usePostUserService
використовується для інкапсуляції цієї логіки. Ми вказали структуру даних для створення нового користувача, визначивши типи запиту та відповіді. Але пропустимо цю частину, щоб можна було зосередитись.
Отже, ми створимо спеціальний хук usePostUserService
, який використовує хук useFetch
для надсилання запиту POST. Він приймає дані користувача як вхідні дані та надсилає їх до API:
function usePostUserService() { const fetch = useFetch(); return useCallback( (data: UserPostRequest, requestConfig?: RequestConfigType) => { return fetch(`${API_URL}/v1/users`, { method: "POST", body: JSON.stringify(data), ...requestConfig, }).then(wrapperFetchJsonResponse<UserPostResponse>); }, [fetch] ); }
Функцію wrapperFetchJsonResponse
ми розглянемо пізніше у розділі «Обробка помилок».
Операції читання (Read Operations)
Сценарій використання: отримання й відображення списку ресурсів або одного ресурсу (наприклад, профілі користувачів, списки продуктів).
Реалізація: надсилання GET-запиту для отримання даних, обробка станів завантаження та помилок, рендеринг даних у UI.
Читання даних включає GET-запити до API для отримання інформації про користувачів. Це може бути отримання всіх користувачів із пагінацією, фільтрами та сортуванням або отримання одного користувача за ID.
У нашому прикладі читання даних передбачає виконання запитів GET до API для отримання даних користувача. Це може включати вибірку всіх користувачів із розбивкою на сторінки, фільтрами та сортуванням або вибірку одного користувача за ідентифікатором після визначення запиту (UsersRequest
) і типів відповіді (UsersResponse
).
Щоб отримати всіх користувачів у спеціальному хуку useGetUsersService
, ми надсилаємо запит GET із параметрами запиту для розбиття на сторінки, фільтрів і сортування:
function useGetUsersService() { const fetch = useFetch(); return useCallback( (data: UsersRequest, requestConfig?: RequestConfigType) => { const requestUrl = new URL(`${API_URL}/v1/users`); requestUrl.searchParams.append("page", data.page.toString()); requestUrl.searchParams.append("limit", data.limit.toString()); if (data.filters) { requestUrl.searchParams.append("filters", JSON.stringify(data.filters)); } if (data.sort) { requestUrl.searchParams.append("sort", JSON.stringify(data.sort)); } return fetch(requestUrl, { method: "GET", ...requestConfig, }).then(wrapperFetchJsonResponse<UsersResponse>); }, [fetch] ); }
Для отримання одного користувача хук useGetUserService
надсилає запит GET, щоб отримати користувача за ідентифікатором:
function useGetUserService() { const fetch = useFetch(); return useCallback( (data: UserRequest, requestConfig?: RequestConfigType) => { return fetch(`${API_URL}/v1/users/${data.id}`, { method: "GET", ...requestConfig, }).then(wrapperFetchJsonResponse<UserResponse>); }, [fetch] ); }
Операція оновлення (Update Operation)
Сценарій використання: редагування наявного ресурсу (наприклад, оновлення інформації про користувача чи редагування публікації в блозі).
Реалізація: зберіть оновлені дані, надішліть запит PUT або PATCH на сервер, обробіть відповідь і оновіть інтерфейс користувача.
Здійснимо оновлення наявного користувача, яке передбачає надсилання запиту PATCH до API з його оновленими даними. Для цього в спеціальному хуку usePatchUserService
ми надсилаємо запит PATCH з ідентифікатором користувача та оновленими даними після визначення запиту UserPatchRequest
і типів відповіді UserPatchResponse
:
function usePatchUserService() { const fetch = useFetch(); return useCallback( (data: UserPatchRequest, requestConfig?: RequestConfigType) => { return fetch(`${API_URL}/v1/users/${data.id}`, { method: "PATCH", body: JSON.stringify(data.data), ...requestConfig, }).then(wrapperFetchJsonResponse<UserPatchResponse>); }, [fetch] ); }
Примітка: використання PATCH замість PUT є більш поширеним для часткового оновлення даних, тоді як PUT зазвичай використовується для повного оновлення ресурсу.
Операція видалення (Delete Operation)
Сценарій використання: видалення існуючого ресурсу (наприклад, видалення користувача або видалення елемента зі списку).
Реалізація: надсилання DELETE-запиту до сервера, обробка відповіді оновлення інтерфейсу користувача, щоб відобразити видалення.
У нашому наступному прикладі видалення користувача передбачає надсилання запиту DELETE до вашого API з ідентифікатором користувача. Після визначення запиту (UsersDeleteRequest
) і типів відповіді (UsersDeleteResponse
) у хуку useDeleteUsersService
передається запит DELETE для видалення користувача за ідентифікатором.
export function useDeleteUsersService() { const fetch = useFetch(); return useCallback( (data: UsersDeleteRequest, requestConfig?: RequestConfigType) => { return fetch(`${API_URL}/v1/users/${data.id}`, { method: "DELETE", ...requestConfig, }).then(wrapperFetchJsonResponse<UsersDeleteResponse>); }, [fetch] ); }
Після успішного видалення не забуваємо оновлювати локальний стан для відображення актуальних даних.
Ці хуки абстрагують складність створення HTTP-запитів і обробки відповідей. Використання такого підходу забезпечує чисту кодову базу, оскільки логіка отримання даних інкапсульована та придатна для повторного використання у ваших компонентах.
Отримання даних у Next.js
Гаразд, ми розібралися з прикладами обробки CRUD-операцій, а тепер детальніше розглянемо методи отримання даних, які пропонує Next.js. Адже він, як фреймворк, додає свої функції та оптимізації поверх React. Зрозуміло, що Next.js крім CSR (відтворення на стороні клієнта) надає розширені функції, як-то SSR, SSG, вбудовані маршрути API та гібридне відтворення. Отже, обговоримо спільні риси та відмінності в отриманні даних у Next.js і в React.
Оскільки програми React є виключно клієнтськими, вибірка даних відбувається на клієнті після початкового завантаження сторінки. Для динамічних сторінок, яким потрібно отримувати дані кожного разу, коли сторінка завантажується, краще використовувати SSR, у цьому випадку дані вибираються на сервері під час запиту. У випадку SSG, який підходить для статичних сторінок, де дані не змінюються часто, дані вибираються під час створення. Отже, метод getStaticProps
допомагає нам отримувати дані під час створення (SSG). Якщо нам потрібно попередньо відобразити сторінки на основі динамічних маршрутів і даних, отриманих під час збірки, це дозволяє зробити метод getStaticPaths
. Він використовується в поєднанні з getStaticProps
для створення динамічних маршрутів під час створення. Слід зазначити, що, починаючи з Next 14, ми можемо робити запити безпосередньо в компонентах без цих методів, відсилаючи ще більше до досвіду роботи з простим React.
Отримання даних на стороні клієнта за допомогою useQuery можна використовувати для інтерактивних компонентів, які потребують отримання даних на стороні клієнта. З початковим станом, гідрованим з серверної стороною. Для отримання даних, які часто змінюються, або для додавання інтерактивності на стороні клієнта корисна стратегія useSWR. Це хук React для отримання даних на стороні клієнта з кешуванням і повторною перевіркою, який зазвичай дає змогу отримувати їх після початкового завантаження сторінки. Тим не менш, він не отримує дані під час створення або на сервері для SSR, але він може повторно перевірити та отримати нові дані, коли потрібно.
Щоб узагальнити інформацію про наведені вище методи, можемо поглянути на таблицю, яка містить повний огляд різних методів отримання даних у Next.js, висвітлюючи їхні відповідні параметри та випадки використання.
Використання React Query з Next.js
React Query надає хуки для отримання, кешування, синхронізації та оновлення стану сервера, що робить його чудовим інструментом для обробки даних у програмах React і Next.js. Основні переваги його використання:
- Ефективне отримання даних: забезпечує кешування та фонову синхронізацію даних, зменшуючи надлишкові мережеві запити.
- Автоматичне повторне отримання: дані можна автоматично повторно отримати у фоновому режимі, коли вони стають застарілими, гарантуючи, що інтерфейс користувача завжди відображатиме останню інформацію.
- Інтегрована обробка помилок: вбудована підтримка обробки помилок і повторних спроб, що полегшує керування мережевими збоями та помилками сервера.
- Оптимістичні оновлення: хук useMutation надає ізоморфні оновлення, забезпечуючи простий спосіб обробки як оптимістичних змін інтерфейсу користувача, так і логіки відкату, якщо запит сервера не вдається.
- Простота інтеграції з Next.js: його можна легко інтегрувати з іншими методами отримання даних Next.js, такими як
getStaticProps
абоgetServerSideProps
(за потреби). - Перевірка запитів і мутацій: інструмент ReactQueryDevtools надає можливість переглядати статус, дані, помилки та інші деталі всіх активних запитів і мутацій, а також спостерігати за оновленням станів запиту в режимі реального часу під час роботи програми.
QueryClientProvider
QueryClientProvider — це компонент провайдера контексту, який постачає екземпляр QueryClient до дерева компонентів React (він необхідний для використання таких хуків, як useQuery). Потрібно розмістити його в корені дерева компонентів і налаштувати глобальні параметри для запитів і мутацій, як-от поведінка повторних спроб, час кешування тощо. Після цього він ініціалізує клієнт React Query і робить його доступним у всій програмі .
import ReactQueryDevtools from "@/services/react-query/react-query-devtools"; ... export default function RootLayout({ ... }) { return ( <html lang={language} dir={dir(language)}> <body> <InitColorSchemeScript /> <QueryClientProvider client={queryClient}> <ReactQueryDevtools initialIsOpen={false} /> ... </QueryClientProvider> </body> </html> ); }
Отже, React Query дає проєкту:
- Централізоване налаштування для всіх запитів і мутацій.
- Легке налаштування та інтегрування в існуючі програми React.
- Вмикає такі функції, як: кешування, фонове повторне отримання та визнання запитів недійсними.
ReactQueryDevtools
Іншою важливою функцією React Query є ReactQueryDevtools — інструмент розробки для перевірки та налагодження станів React Query. Його можна легко додати до вашої програми та отримати доступ через розширення браузера або як компонент, як у попередньому прикладі.
Під час розробки інструменти ReactQueryDevtools можна використовувати для перевірки окремих запитів і мутацій, щоб зрозуміти, чому певні запити попередньо завантажуються, і відстежувати стан кешу запитів, а також спостерігати, як він змінюється з часом.
Пагінація та нескінченна прокрутка
Щоб реалізувати елементи керування розбивкою на сторінки або нескінченне прокручування за допомогою функцій, ідеально підходить useInfiniteQuery. Спершу ми генеруємо унікальні ключі для кешування та отримання запитів у React Query. Метод by створює унікальний ключ на основі параметрів сортування та фільтрації.
const usersQueryKeys = createQueryKeys(["users"], { list: () => ({ key: [], sub: { by: ({ sort, filter, }: { filter: UserFilterType | undefined; sort?: UserSortType | undefined; }) => ({ key: [sort, filter], }), }, }), });
Для цього ми скористаємося функцією useInfiniteQuery
з React Query і використаємо хук useGetUsersService
, описаний вище в «Read Operations».
export const useUserListQuery = ({ sort, filter, }: { filter?: UserFilterType | undefined; sort?: UserSortType | undefined; } = {}) => { const fetch = useGetUsersService(); const query = useInfiniteQuery({ queryKey: usersQueryKeys.list().sub.by({ sort, filter }).key, initialPageParam: 1, queryFn: async ({ pageParam, signal }) => { const { status, data } = await fetch( { page: pageParam, limit: 10, filters: filter, sort: sort ? [sort] : undefined, }, { signal, } ); if (status === HTTP_CODES_ENUM.OK) { return { data: data.data, nextPage: data.hasNextPage ? pageParam + 1 : undefined, }; } }, getNextPageParam: (lastPage) => { return lastPage?.nextPage; }, gcTime: 0, }); return query; };
Тут queryFn отримує дані користувача на основі параметрів поточної сторінки, фільтра та сортування, функція getNextPageParam
визначає наступну сторінку для отримання на основі відповіді останньої сторінки. Коли користувач прокручує або запитує додаткові дані, useInfiniteQuery
автоматично отримує наступний набір даних на основі параметра nextPage
— так і відбувається нескінченне прокручування. Час кешу для запиту встановлюється параметром gcTime
.
Загалом React Query надає комплексне рішення для керування та налагодження стану сервера в програмах React. QueryClientProvider
забезпечує централізовану та узгоджену конфігурацію для всіх запитів і мутацій, тоді як ReactQueryDevtools
пропонує потужні інструменти для перевірки та розуміння поведінки запитів під час розробки.
Обробка помилок
Реалізація CRUD-операцій завжди потребує належної обробки помилок, щоб забезпечити зручність для користувача та надійність застосунку. Серверні помилки зазвичай пов’язані з невдалою обробкою запиту клієнта, помилками в коді сервера, перевантаженням ресурсів, неправильним налаштуванням інфраструктури чи збоями в зовнішніх сервісах.
Для обробки помилок Extensive-React-Boilerplate пропонує скористатися функцією wrapperFetchJsonResponse
:
async function wrapperFetchJsonResponse<T>( response: Response ): Promise<FetchJsonResponse<T>> { const status = response.status as FetchJsonResponse<T>["status"]; return { status, data: [ HTTP_CODES_ENUM.NO_CONTENT, HTTP_CODES_ENUM.SERVICE_UNAVAILABLE, HTTP_CODES_ENUM.INTERNAL_SERVER_ERROR, ].includes(status) ? undefined : await response.json(), }; }
Висновок
У цій статті ми розглянули основні операції CRUD, дослідили методи отримання даних у NextJS і заглибились у використання React Query для керування станом, окресливши також можливості QueryClientProvider і ReactQueryDevtools для налагодження та оптимізації отримання даних. Крім того, обговорили впровадження розбиття на сторінки та нескінченного прокручування для обробки великих наборів даних. І звернули увагу на обробку помилок, щоб навчитись робити програми більш стійкими та забезпечити безперебійну роботу користувача.
Дотримуючись прикладів і методів, викладених у цій статті, ви будете добре підготовлені до обробки операцій CRUD у своїх проектах NextJS. Крім того, ви можете використовувати наш шаблон Extensive-React-Boilerplate для свого проєкту. Він має повністю сумісний бекенд nestjs-boilerplate, який реалізовує можливість працювати з операціями CRUD за лічені хвилини, без жодного рядка коду за допомогою CLI. Продовжуйте експериментувати, залишайтеся в курсі найкращих методів і ласкаво просимо спробувати цей шаблон, якщо ви вважаєте його корисним.
Наша команда BC Boilerplates завжди шукає шляхи покращення розвитку. Ми хотіли б почути ваші думки на GitHub або в коментарях нижче.
Дякую за цей матеріал Олені Власенко і Владу Щепотіну 🇺🇦
1 коментар
Додати коментар Підписатись на коментаріВідписатись від коментарів