Різниця між fetch та useFetch в NuxtJs 3, детальний розбір та інструкція з прикладами коду

В Nuxtjs 3 сильно змінилася модель отримання данних. Якщо в Nuxtjs 2 на всі випадки життя був Axios, та його ітерації, які ви могли створювати, то з виходом Nuxtjs3 це стало зовсім не актуально. Тому що зʼявилися методи для отримання данних, такі як -  useFetch, useLazyFetch, useAsyncData та useLazyAsyncData. З поганого тут тільки те, що всі ці методи можливо юзати тільки в SSR, та навіщо взагалі це робити, якщо є простий $fetch. Зараз у всьому розберемося.

Де можна отримувати дані:

Поперше треба зрозуміти, що useFetch, useAsyncData, useLazyFetch та useLazyAsyncData, можна використовувати тільки всередині <script setup>, тому що ці функції тісно звʼязані з Payload Nuxtjs та використовують контекст Composition Api. Ви звісно можете їх використовувати за межею setup, і nuxt буде намагатися опрацювати цей випадок, але це буде не правильно, більш того в деяких випадках це може викликати проблеми, як з route handling, так і з базовим hot reload при розробці. А внутрішні реактивні змінні ціх методів просто перестануть працювати. Тому всі ці 4 метода можна використовувати тільки в середені setup.

Чому треба використовувати саме useFetch та useAsyncData замість $fetch, або axios?

Для того, щоб це осмислити, треба зрозуміти що таке гідація, і як рендериться додаток. 

Гідрація в Nuxt.js 3  загалом описує процес, де статична розмітка, яка була згенерована на сервері (SSR - Server-Side Rendering), оживає на клієнтській стороні завдяки JavaScript. Коли ваш браузер завантажує HTML-сторінку, що була згенерована на сервері, вона виглядає статичною та неінтерактивною. Однак, як тільки відповідні JavaScript-скрипти завантажуються та виконуються, вони "оживляють" статичний HTML, додаючи інтерактивність та динаміку. Цей процес називається гідрацією.

У Nuxt.js 3, гідрація відіграє ключову роль у процесі універсального рендерингу (Universal Rendering), де сторінка спочатку рендериться на сервері, а потім гідрується на клієнті.  При універсальному рендерингу ваш код виконується спочатку на сервері для генерації HTML-сторінки, а потім ще раз на клієнті для гідрації сторінки. Якщо ви виконуєте запит у вашому компоненті без перевірок на те, де він виконується (на сервері чи на клієнті), він може бути викликаний і в тому, і в іншому контексті, відповідно запит відбудеться двічі. І як ви розумієте, коли один реквест виконується двічі, або в деяких випадках навіть тричі, це дуже погано для продуктивності системи.

Такого не буде відбуватися при правильному використанні useFetch та useAsyncData, тому що вони розуміють контекст Nuxt, та розуміють на якій стадії зараз відбувається рендерінг. Тобто використання ціх методів, буквально вирішує проблему дублювання реквестів, з якою ви зіткнетесь, якщо будете використовувати сторонні бібліотеки.

По друге. Якщо у вас FullStack додаток, і ваш backend виконується на стороні NuxtJS в server/api. То у вас буквально нема іншого вибору, окрім як використовувати useFetch та useAsyncData. Тому як, вони розуміють коли сервер отримав та обробив данні, коли сервер перезаписав кукі всередені (наприклад потрібні для сесій та авторизацій), і розуміє коли ставати реквест на очікування, а коли віддавати данні. 

Базовий приклад, якщо ви будете намагатися спарсити кукі у яких є захист на стороні сервера, а після будете робити реквест через звичайний fetch або axios. То сервер мало того, що викличе ваш запит двічі, так ще в першому випадку віддасть вам некорректну інформацию, тому що на етапі першого реквесту на сервері ще нама ніяких кукі. Та event недоступний.

export default defineEventHandler(async (event) => {
  const serverCookies = parse(event.node.req.headers.cookie || '');
  const token = serverCookies.authToken;
  const reqToken = token ? await verifyToken(token) : false;
  return token ? reqToken ? reqToken : false : false
})

Тому ви зіткнетесь з випадком, коли перший запит поверне вам undefined, а другий який вже буде на стороні клієнта та  поверне вам token, що може викликати некорректний рендерінг вашого додатку.

Тому useFetch, useAsyncData можна використовувти тільки на стороні <script setup>, і потрібно використовувати тільки їх, якщо ви обмінюєтеся данними зі свої сервером. 

Чому useFetch не працює корректно на стороні клієнта

Друга проблема з якою ви зіткнетеся, що useFetch не буде корректно працювати, якщо ви будете викликати його в середені метода, який не викликається на етапі рендеренгу компонента або сторінки. Тобто, якщо ви створите функцію, або метод, який буде викликатися при кліку на кнопку, або в будь якому випадку, вже після відданої клієнту сторінки, то useFetch не буде корректно працювати. Його требе рееструвати на початку, але про це пізніше. 

Справа в тому, що useFetch оптимізований для використання в контексті SSR (Server-Side Rendering) або під час ініціалізації компонента в Nuxt 3. useFetch призначений для виконання запитів на етапі серверного рендерингу або в хуках життєвого циклу, які викликаються при ініціації компонента, і не рекомендується для використання в реактивних подіях, таких як кліки по кнопці.

Для випадків, коли вам потрібно виконати запит до API у відповідь на дії користувача, такі як клік по кнопці, ви повинні використовувати $fetch. $fetch — це утиліта в Nuxt 3, яка дозволяє вам виконувати запити до API з клієнтської сторони, і вона ідеально підходить для викликів, які повинні бути зроблені динамічно у відповідь на взаємодію користувача. Але вона не має повного контролю над контекстом, тому не завжди підходить для використання при першому SSR рендері.

<template>
  <div>
    <h1>Дані завантажені через useFetch:</h1>
    <p v-if="dataUseFetch">{{ dataUseFetch }}</p>

    <button @click="fetchDataOnClick">Завантажити додаткові дані через $fetch</button>
    <h2>Дані завантажені через $fetch:</h2>
    <p v-if="dataDollarFetch">{{ dataDollarFetch }}</p>
  </div>
</template>

<script setup>
// Використання useFetch для автоматичного запиту даних при завантаженні сторінки
const { data: dataUseFetch } = useFetch('/api/static-data')

const dataDollarFetch = ref(null)

// Функція для виконання запиту через $fetch при кліку на кнопку
async function fetchDataOnClick() {
  dataDollarFetch.value = await $fetch('/api/dynamic-data')
}
</script>

У цьому прикладі, як ви бачите данні для рендерінгу сторінки використовують useFetch, а їх оновлення вже через $fetch. Це є правильне рішення рішення. Другим буде відкладене завантаження, але про це пізніше.

Що таке useAsyncData

В принципі це практично теж саме, що і useFetch. Варіант його використання абсолютно такий же. Тільки в середині <script setup> і тільки для SSR рендеру. Але в нього є одне велике - але. Він автоматично серіалізує і кешує результати на стороні сервера для оптимального SEO та швидкості сторінки. Хук useAsyncData ідеально підходить для завантаження даних, які потрібно отримати перед рендерингом сторінки, наприклад, для отримання даних статті блогу, які потрібно відобразити користувачу.

Основна різниця між useFetch і useAsyncData полягає у їх використанні та оптимізації. useFetch більш універсальний і може використовуватися для будь-яких асинхронних запитів у вашому застосунку, компонентах, міксинах, тоді як useAsyncData оптимізований для завантаження даних, необхідних на етапі генерації сторінки, з автоматичним кешуванням і серіалізацією для покращення SEO і швидкості завантаження.

Для того, щоб він корректно працював потрібно вказати унікальний ключ, який і буде ключем кешування, а також треба враховувати що useAsyncData приймає promise, який буде виконувати, тому не теба писати await перед $fetch:

<template>
  <div>
    <div v-if="data.value">{{ data.value.title }}</div>
    <div v-else-if="error.value">{{ error.value.message }}</div>
    <div v-else>Loading...</div>
  </div>
</template>

<script setup>
const { data, error } = useAsyncData('unique-key-for-cache-data', () => {
  return $fetch('https://api.example.com/data')
});
</script>

Як рендерити сторінку на SSR тільки перший раз, а всі інші переходи, щоб відбувався на стороні клієнта. Як це було з методом async fetch() в Nuxtjs 2.

Для цього і потрібні useLazyFetch та useLazyAsyncData. Фактично це тіж самі хуки, але вони мають відкладене завантаження, і повторюють логіку реалізації хука async fetch() з Nuxtjs 2. Тобто коли користувач заходить на сайт, перше завантаження всього сайту буде відбуватися на стороні сервера, що віддасть повний html, але при повторному переході в середені сайта на цю ж сторінку, буде відбуватися вже рендерінг на стороні клієнта. Це дуже виграшна ситуація, тому що користувач спочатку переходить на стороінку, а після бачить якийсь прелоадер, завантаження. Що створює ілюзію швидкості сайту. 

Щоб наглядно зрозуміти як це працює, яб рекомендував вам створити тестовий файл - наприклад /server/api/test.js:

export default defineEventHandler(async (event) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("hahaha")
    }, 3000)
  }).catch((error) => {
    return createError({ statusCode: 400, statusMessage: error.message });
  });
});

Він буде просто повертати "hahaha" через 3 секунди. І також створіть сторінку, яка буде виконувати цей запит:

<template lang="pug">
div
  p Data: {{ data }}
  p Pending: {{ pending }}
  p Error: {{ error?.value?.statusMessage }}
  p Error: {{ error?.value?.statusCode }}
</template>

<script setup>
const {data, pending, error, execute, refresh, status } = useLazyFetch('/api/test')
</script>

При такому виконанні ви помітете, що при переході між сторінками, у вас відразу буде здійснюватися перехід без очікування. При цьому pending буде в true, до закінчення завантаження. Але при першому рендерінгу сайта, коли користувач тільки на нього зайшов, все буде відбуватися на SSR. 

Як ви помітили з прикладу, useFetch, та useLazyFetch - повертає цілу низку змінних та функцій:

  • data: Данні які повернуться від сервера
  • pending: Індикатор завантаження, буде в true поки запит виконується і стане false після його завершення
  • error: Поверне помилку, якщо вона станеться (тепер вам не треба писати try catch, або promise catch, тому що це все вже вбудовану в будь яку з цих чотирьох методів)
  • refresh: Метод, який дає можливість перевиконати запит, з будь якої функції. Корисно в тій ситуації, коли вам потрібно виконати рекваст повтрно, або оновити його при певних обставинах.
  • execute: Теж сам, що і refresh, але дає можливість змінити параметри запиту.

І ось знову повернемося до використання useFetch на стороні клієнта. Відразу приклад, як використовувати useFetch при кліку на кнопку:

<template>
  <div>
    <div v-if="pending">Loading...</div>
    <div v-else-if="error">Error: {{ error }}</div>
    <div v-else>{{ data }} </div>
    <button @click="updateData">Update Data</button>
    <button @click="refreshData">Just refresh request</button>
  </div>
</template>

<script setup>
const { data, execute, pending, error, refresh } = useFetch('/api/data', {
  method: 'POST',
  body: {
    initial: 'data'
  }
});

const refreshData = () => refresh())

const updateData = () => {
  // Використовуємо execute для відправки нового запиту з оновленими даними
  execute({
    method: 'POST',
    body: {
      updated: 'data'
    }
  });
};
</script>

Як ви бачити, для того, щоб оновити параметри реквесту і викликати його знову, просто оновити їх, або вивести прелоадер, тепер не потрібно створювати ніяких змінних, всі вони вже вкладені в useFetch та useLazyFetch. Данні в ціх змінних оновлюються реактивно, тому вам не потрібно ні переназначати їх, ні додатково оброляти. В цьому відбувається магія NuxtJs 3.

useLazyAsyncData пацює так само, але по аналогії з простим useAsyncData, кешує данні на сервері і створена для рендерінгу сторінок, а не компонентів. 

Тепер поговоримо про параметри, які ви можете вказати в будь яку з цих методів:

Server

server: За стандартом в true. Вказує для Nuxt де виконувати запит, на стороні сервера, або клієнта. Якщо встановити в false, то запит буде виконуватись як onMounted()

Lazy

lazy: Просто перетворюэ useFetch в useLazyFetch.

Immediate

immediate: Дуже важливий параметр, який вимикає автоматичне виконання запута, якщо встановити на false. Тобто ви можете зарееструвати  запит, але перший раз виконати його за допомогою execute() в будь якому методі або функції. Це відповідь на питання, як використовувати useFetch для відправки форми. Для прикладу

<template>
<div>
  <div> {{ data }} </div>
  <button @click="init">Init</button>
  <button @click="changePage(2)">Change page to 2</button>
</div>
</template>

<script setup>
const { data, execute } = useFetch('/api/pager', {
  immediate: false,
  method: 'POST',
  body: {
   page: 1
  }
})

const init = () => { 
  execute()
}

const changePage = (page) => {
  execute({
    body: {
      page: page,
    }
  })
}

</script>

Dedupe

dedupe: Використовується для управління поведінкою дедуплікації запитів, тобто запобігання виконання однакових запитів одночасно. Це корисно, коли ви хочете уникнути непотрібних запитів до сервера, які можуть виникати внаслідок швидких послідовних дій користувача або програмної логіки. Має свої опції:

  • cancel: Ця опція скасовує існуючі запити, коли робиться новий запит із тим же ключем (створюється автоматично на основі body, headers, параметрів запиту та url). Це забезпечує, що завжди буде виконуватися лише останній запит, скасовуючи будь-які попередні незавершені запити з тим же ключем. Це допомагає уникнути непотрібної взаємодії з сервером та зменшити кількість одночасних запитів.

  • defer: При виборі цієї опції нові запити не будуть здійснюватися, якщо існує очікуючий запит з тим же ключем. Це означає, що замість створення нового запиту, ваш додаток буде чекати завершення існуючого запиту та використовувати його результати. Це допомагає забезпечити, що одночасно виконується лише один запит для кожного унікального ключа, уникаючи повторних запитів до тих самих ресурсів.

const { data, execute } = useFetch('/api/data', {
  dedupe: 'cancel' // або 'defer', залежно від потреб
});

Watch

watch: Використовується для створення реактивного запиту, який автоматично перевиконується кожного разу, коли змінюється одна або декілька зазначених реактивних залежностей. Це дозволяє динамічно реагувати на зміни у вашому додатку та автоматично оновлювати дані без необхідності вручну повторювати запити.

Припустимо, у вас є компонент, де ви хочете відобразити дані, які залежать від користувацького вводу або вибору. Ви можете використовувати watch, щоб спостерігати за змінами цих залежностей і автоматично перезавантажувати ваш запит.

<template>
  <div>
    <input v-model="userId" placeholder="Enter User ID">
    <button @click="fetchData">Fetch User Data</button>
    <div v-if="data">{{ data.name }}</div>
  </div>
</template>

<script setup>
const userId = ref('');

const { data, execute } = useFetch('/api/user/' + userId.value, {
  watch: [userId]
});

const fetchData = () => {
  userId.value = 5
};
</script>

Pick

pick: дозволяє вам вибірково витягнути певні поля з відповіді запиту, замість того, щоб працювати з повною відповіддю. Це може бути корисним, коли від сервера приходить велика кількість даних, але вам потрібні лише деякі конкретні поля для вашого компонента або логіки.

Використання pick полягає в передачі масиву з іменами полів, які ви хочете отримати з відповіді. useFetch автоматично витягне ці поля і поверне їх як результат, ігноруючи решту даних відповіді.

<template>
  <div>
    <p>User Name: {{ data.name }}</p>
    <p>User Email: {{ data.email }}</p>
  </div>
</template>

<script setup>
const { data } = useFetch('/api/user', {
  pick: ['name', 'email'] // Вказуємо, що нам потрібні лише ім'я та електронна пошта користувача
});
</script>

Transform

transform: використовується для того, щоб модифікувати або обробити дані, отримані з запиту, перш ніж вони будуть повернуті або збережені у відповідній реактивній змінній. Це може бути корисно для форматування, фільтрації даних або внесення будь-яких необхідних змін до даних, отриманих від API, перш ніж використовувати їх у вашому додатку.

Ви задаєте функцію transform як частину опцій useFetch, і ця функція буде автоматично застосована до даних, отриманих з вашого запиту. Функція повинна приймати дані як аргумент і повертати модифіковані дані.

<script setup>
const { data } = useFetch('/api/posts', {
  transform: (responseData) => {
    // Припустимо, responseData - це масив об'єктів постів
    // Ми хочемо додати нове поле `shortTitle` до кожного поста
    return responseData.map(post => ({
      ...post,
      shortTitle: post.title.length > 50 ? post.title.substring(0, 47) + '...' : post.title
    }));
  }
});
</script>

На цьому думаю все, я сподіваюсь цей пост пролив світло на ваше розуміння запитів в NuxtJs 3, тому що я намагався як можна детальніше описати про все що відбувається в useFetch та useAsyncData. 

Ставте лайк, та пишіть комментарі