Получение данных и создание пользовательского крючка. 🪝

Цель этой заметки – показать вам способ выполнения HTTP GET-запросов с помощью React и пользовательского хука.

🚨 Примечание: Этот пост требует от вас знания основ React (основные хуки и fetch-запросы).

Любой вид обратной связи приветствуется, спасибо и я надеюсь, что вам понравится статья.🤗

 

Оглавление

📌 Технологии для использования

📌 Создание проекта

📌 Первые шаги

📌 Делаем наш первый забор

📌 Отображение данных API на экране

📌 Создание пользовательского крючка

📌 Усовершенствование крючка useFetch.

📌 Добавление новых компонентов и рефакторинг

📍 Header.tsx

📍 Loading.tsx

📍 ErrorMessage.tsx

📍 Card.tsx

📍 LayoutCards.tsx


🚨 Используемые технологии.

▶️ React JS (версия 18)

▶️ Vite JS

▶️ TypeScript

▶️ Rick and Morty API

▶️ Vanilla CSS (стили можно найти в репозитории в конце этого поста)
 

〽️ Создание проекта.

npm init vite@latest
Войдите в полноэкранный режим Выход из полноэкранного режима

В данном случае мы назовем его: fetching-data-custom-hook (необязательно).

Выберите React, а затем TypeScript.

Затем выполните следующую команду для перехода в только что созданный каталог.

cd fetching-data-custom-hook
Войдите в полноэкранный режим Выход из полноэкранного режима

Далее мы устанавливаем зависимости:

npm install
Войдите в полноэкранный режим Выход из полноэкранного режима

Затем открываем проект в редакторе кода (в моем случае VS code)

code .
Войдите в полноэкранный режим Выход из полноэкранного режима

 

〽️ Первые шаги.

Внутри папки src/App.tsx мы удалим все содержимое файла и разместим функциональный компонент, который показывает заголовок и подзаголовок.

const App = () => {
  return (
            <h1 className="title">Fetching data and create custom Hook</h1>
      <span className="subtitle">using Rick and Morty API</span>
  )
}
export default App;
Войдите в полноэкранный режим Выход из полноэкранного режима

Прежде всего, мы создадим пару интерфейсов, которые помогут нам автоматически заполнять свойства, поступающие в JSON-ответе, предоставляемом API.

  • Первый интерфейс Response содержит свойство results, которое представляет собой массив Results.
  • Второй интерфейс Result, содержит только 3 свойства (хотя их больше, вы можете проверить документацию API), выбираем ID, имя и изображение персонажа.
interface Response {
  results: Result[]
}

interface Result {
  id: number;
  name: string;
  image: string;
}
Войдите в полноэкранный режим Выход из полноэкранного режима

 

〽️ Делаем наш первый фетч.

  1. Сначала мы добавляем состояние, которое имеет тип Result[], а значением по умолчанию будет пустой массив, поскольку мы еще не выполнили вызов API. Он будет использоваться для хранения данных API для отображения.
const App = () => {

  const [data, setData] = useState<Result[]>([]);

  return (
        <h1 className="title">Fetching data and create custom Hook</h1>
      <span className="subtitle">using Rick and Morty API</span>
  )
}
export default App;
Войдите в полноэкранный режим Выход из полноэкранного режима
  1. Чтобы выполнить выборку данных, нам нужно сделать это в useEffect, так как нам нужно выполнить выборку при первом отображении нашего компонента.

Поскольку нам нужно, чтобы он выполнялся только один раз, мы помещаем пустой массив (т.е. без каких-либо зависимостей).

const App = () => {
  const [data, setData] = useState<Result[]>([]);

    useEffect(()=> {

    },[]) // arreglo vació

  return (
    <div>
      <h1 className="title">Fetching data and create custom Hook</h1>
      <span className="subtitle">using Rick and Morty API</span>
    </div>
  )
}
export default App;
Войдите в полноэкранный режим Выход из полноэкранного режима
  1. Внутри тела функции useEffect будет сделан вызов API, а поскольку useEffect не позволяет нам использовать асинхронный код напрямую, мы сделаем вызов, используя обещания.
const [data, setData] = useState<Result[]>([]);

useEffect(()=> {
   fetch('https://rickandmortyapi.com/api/character/?page=8')
   .then( res => res.json())
   .then( (res: Response) => {})
   .catch(console.log)   
},[])
Войдите в полноэкранный режим Выход из полноэкранного режима
  1. Как только обещания будут разрешены, мы получим данные, соответствующие API, которые установим в состояние с помощью функции setData.

Теперь мы можем вывести данные на экран. 😌

🚨 Если что-то пойдет не так с API, catch поймает ошибку и покажет ее в консоли, а значение статуса “data” останется пустым массивом (и в конце не будет показано ничего, кроме названия и подзаголовка приложения).

const [data, setData] = useState<Result[]>([]);

useEffect(()=> {
   fetch('https://rickandmortyapi.com/api/character/?page=8')
   .then( res => res.json())
   .then( (res: Response) =>  {
      setData(res.results);
   })
   .catch(console.log)   
},[])
Войдите в полноэкранный режим Выход из полноэкранного режима

 

〽️ Отображение данных API на экране.

Перед отображением данных API нам необходимо выполнить оценку. 🤔

🔵 Только если длина значения статуса “data” больше 0, мы выводим данные API на экран.

🔵 Если длина значения статуса “data” меньше или равна 0, на экране не будет отображаться никаких данных, только название и субтитр.

const App = () => {
    const [data, setData] = useState<Result[]>([]);

    useEffect(()=> {
       fetch('https://rickandmortyapi.com/api/character/?page=8')
       .then( res => res.json())
       .then( (res: Response) =>  {
          setData(res.results);
       })
       .catch(console.log)   
    },[])

  return (
    <div>
      <h1 className="title">Fetching data and create custom Hook</h1>
      <span className="subtitle">using Rick and Morty API</span>
      {
        (data.length > 0) && <p>data</p>
      }
    </div>
  )
}
export default App;
Войдите в полноэкранный режим Выход из полноэкранного режима

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

Использование функции map, применяемой в массивах. Мы пройдем по массиву значения состояния “data” и вернем новый JSX-компонент, который в данном случае будет представлять собой только изображение и текст.

ПРИМЕЧАНИЕ: свойство key внутри div – это идентификатор, который React использует в списках для более эффективного отображения компонентов. Важно установить его.

const App = () => {
    const [data, setData] = useState<Result[]>([]);

    useEffect(()=> {
       fetch('https://rickandmortyapi.com/api/character/?page=8')
       .then( res => res.json())
       .then( (res: Response) =>  {
          setData(res.results);
       })
       .catch(console.log)   
    },[])

  return (
    <div>
      <h1 className="title">Fetching data and create custom Hook</h1>
      <span className="subtitle">using Rick and Morty API</span>
      {
        (data.length > 0) && data.map( ({ id, image, name }) => (
          <div key={id}> 
            <img src={image} alt={image} /> 
            <p>{name}</p> 
          </div>
        ))
      }
    </div>
  )
}
export default App;
Войдите в полноэкранный режим Выход из полноэкранного режима

Таким образом, мы завершили получение данных и корректное отображение их на экране. Но мы все еще можем его улучшить. 😎


 

〽️ Создание пользовательского крючка.

В папке src/hook мы создаем файл под названием useFetch.

Создайте функцию и вырежьте логику из компонента App.tsx.

const App = () => {

  return (
    <div>
      <h1 className="title">Fetching data and create custom Hook</h1>
      <span className="subtitle">using Rick and Morty API</span>
      {
        (data.length > 0) && data.map( ({ id, image, name }) => (
          <div key={id}> 
            <img src={image} alt={image} /> 
            <p>{name}</p> 
          </div>
        ))
      }
    </div>
  )
}
export default App;
Войдите в полноэкранный режим Выход из полноэкранного режима

Вставьте логику в эту функцию, а в конце верните значение состояния “data“.

export const useFetch = () => {
  const [data, setData] = useState<Result[]>([]);

  useEffect(()=> {
     fetch('https://rickandmortyapi.com/api/character/?page=8')
     .then( res => res.json())
     .then( (res: Response) =>  {
        setData(res.results);
     })
     .catch(console.log)   
  },[]);

  return {
    data
  }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Наконец, мы вызываем хук useFetch, извлекающий данные.

Вот и все, наш компонент стал еще чище и легче для чтения. 🤓

const App = () => {

  const { data } = useFetch();

  return (
    <div>
      <h1 className="title">Fetching data and create custom Hook</h1>
      <span className="subtitle">using Rick and Morty API</span>
      {
        (data.length > 0) && data.map( ({ id, image, name }) => (
          <div key={id}> 
            <img src={image} alt={image} /> 
            <p>{name}</p> 
          </div>
        ))
      }
    </div>
  )
}
export default App;
Войдите в полноэкранный режим Выход из полноэкранного режима

Но подождите, мы еще можем улучшить этот крючок. 🤯
 

〽️ Улучшение хука useFetch.

Теперь мы улучшим крючок, добавив больше свойств.

К существующему состоянию мы добавим другие свойства, и это новое состояние будет иметь тип DataState.

interface DataState {
    loading: boolean;
    data: Result[];
    error: string | null;
}
Войдите в полноэкранный режим Выход из полноэкранного режима

loading, булево значение, позволит нам узнать, когда выполняется вызов API. По умолчанию значение будет установлено в true.

error, строка или нулевое значение, то будет показано сообщение об ошибке. По умолчанию значение будет равно null.

🔵 data, значение типа Result[] , покажет данные API. По умолчанию значением будет пустой массив.

🔴 ПРИМЕЧАНИЕ: только что переименовал свойства поместья.

🔵 данные ➡️ dataState

🔵 setData ➡️ setDataState

export const useFetch = () => {
    const [dataState, setDataState] = useState<DataState>({
      data: [],
      loading: true,
      error: null
  });

  useEffect(()=> {
     fetch('https://rickandmortyapi.com/api/character/?page=8')
     .then( res => res.json())
     .then( (res: Response) =>  {
        setData(res.results);
     })
     .catch(console.log)   
  },[]);

  return {
    data
  }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Теперь мы вынесем логику useEffect в отдельную функцию. Эта функция будет называться handleFetch.

Мы будем использовать useCallback, чтобы сохранить эту функцию и предотвратить ее повторное создание при изменении состояния.

useCallback также получает массив зависимостей, в данном случае мы оставим его пустым, так как хотим, чтобы он был сгенерирован только один раз.

const handleFetch = useCallback(
    () => {},
    [],
)
Войдите в полноэкранный режим Выход из полноэкранного режима

Функция, полученная в useCallback, может быть асинхронной, поэтому мы можем использовать async/await.

  1. Сначала мы разместим try/catch для обработки ошибок.
  2. Затем мы создаем константу со значением URL для вызова API.
  3. Мы выполняем вызов API с помощью fetch и отправляем ему URL (функция await позволит нам дождаться ответа, правильного или неправильного, в случае ошибки он сразу перейдет в функцию catch).
const handleFetch = useCallback(
      async () => {
          try {
                            const url = 'https://rickandmortyapi.com/api/character/?page=18';
              const response = await fetch(url);
          } catch (error) {}
        },
        [],
    )
Войдите в полноэкранный режим Выход из полноэкранного режима
  1. Затем мы оцениваем ответ, если есть ошибка, то активируем catch и отправляем ошибку, которую выдает нам API.
  2. В catch мы собираемся установить состояние. Мы вызываем setDataState, передаем ей функцию для получения предыдущих значений (prev). Мы возвращаем следующее.
    1. Мы разбрасываем предыдущие свойства (…prev), которые в данном случае будут только значением свойства data, которое в итоге окажется пустым массивом.
    2. загрузки, мы устанавливаем значение false.
    3. error, мы устанавливаем значение параметра error, который получает catch, чтобы получить сообщение и поместить его в это свойство.
const handleFetch = useCallback(
      async () => {
          try {
                            const url = 'https://rickandmortyapi.com/api/character/?page=18';
              const response = await fetch(url);

              if(!response.ok) throw new Error(response.statusText);

          } catch (error) {

              setDataState( prev => ({
                  ...prev,
                  loading: false,
                  error: (error as Error).message
              }));
          }
        },
        [],
    )
Войдите в полноэкранный режим Выход из полноэкранного режима
  1. Если нет ошибки от API, мы получаем информацию и устанавливаем состояние аналогично тому, как мы это делали в catch.
  2. Мы вызываем setDataState, передаем ей функцию для получения предыдущих значений (prev). Мы возвращаем следующее.
    1. Мы распространяем предыдущие свойства (…prev), которые в данном случае будут только значением свойства error, которое в итоге будет равно null.
    2. загрузки, мы устанавливаем значение false.
    3. данных, будет значение счетчика dataApi путем обращения к его свойству results.
const handleFetch = useCallback(
      async () => {
          try {
                            const url = 'https://rickandmortyapi.com/api/character/?page=18';
              const response = await fetch(url);

              if(!response.ok) throw new Error(response.statusText);

              const dataApi: Response = await response.json();

              setDataState( prev => ({
                  ...prev,
                  loading: false,
                  data: dataApi.results
              }));

          } catch (error) {

              setDataState( prev => ({
                  ...prev,
                  loading: false,
                  error: (error as Error).message
              }));
          }
        },
        [],
    )
Войдите в полноэкранный режим Выход из полноэкранного режима

После создания функции handleFetch мы возвращаемся к функции useEffect, удаляем логику и добавляем следующее.

Мы оцениваем, если значение состояния “dataState”, обращаясь к свойству data, содержит длину, равную 0, то мы хотим, чтобы функция была выполнена. Это делается для того, чтобы избежать вызова функции более одного раза.

useEffect(() => {
    if (dataState.data.length === 0) handleFetch();
}, []);
Войдите в полноэкранный режим Выход из полноэкранного режима

И крючок будет выглядеть следующим образом:

🔴 ПРИМЕЧАНИЕ: в конце хука мы возвращаем, используя оператор spread, значение состояния “dataState”.

ПРИМЕЧАНИЕ: интерфейсы были перемещены в соответствующую папку внутри src/interfaces.

import { useState, useEffect, useCallback } from 'react';
import { DataState, Response } from '../interface';

const url = 'https://rickandmortyapi.com/api/character/?page=18';

export const useFetch = () => {

    const [dataState, setDataState] = useState<DataState>({
        data: [],
        loading: true,
        error: null
    });

    const handleFetch = useCallback(
        async () => {
            try {
                const response = await fetch(url);

                if(!response.ok) throw new Error(response.statusText);

                const dataApi: Response = await response.json();

                setDataState( prev => ({
                    ...prev,
                    loading: false,
                    data: dataApi.results
                }));

            } catch (error) {

                setDataState( prev => ({
                    ...prev,
                    loading: false,
                    error: (error as Error).message
                }));
            }
        },
        [],
    )

    useEffect(() => {
        if (dataState.data.length === 0) handleFetch();
    }, []);

    return {
        ...dataState
    }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

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

〽️ Добавление новых компонентов и рефакторинг.

Первое, что необходимо сделать, это создать папку components внутри src.

Внутри папки components мы создаем следующие файлы.
 

🟡 Header.tsx

Внутри этого компонента будет только название и субтитр, созданные ранее. 😉.

export const Header = () => {
    return (
        <>
            <h1 className="title">Fetching data and create custom Hook</h1>
            <span className="subtitle">using Rick and Morty API</span>
        </>
    )
}
Войдите в полноэкранный режим Выход из полноэкранного режима

 

🟡 Loading.tsx

Этот компонент будет показан только в том случае, если свойство load хука установлено в true. ⏳

export const Loading = () => {
  return (
    <p className='loading'>Loading...</p>
  )
}
Войдите в полноэкранный режим Выход из полноэкранного режима


 

🟡 ErrorMessage.tsx

Этот компонент будет отображаться только в том случае, если свойство error хука содержит строковое значение. 🚨

export const ErrorMessage = ({msg}:{msg:string}) => {
  return (
    <div className="error-msg">{msg.toUpperCase()}</div>
  )
}
Войдите в полноэкранный режим Выход из полноэкранного режима


 

🟡 Card.tsx

Отображает данные API, т.е. изображение и его текст. 🖼️

import { Result } from '../interface';

export const Card = ({ image, name }:Result) => {

    return (
        <div className='card'>
            <img src={image} alt={image} width={100} />
            <p>{name}</p>
        </div>
    )
}
Войдите в полноэкранный режим Выход из полноэкранного режима

 

🟡 LayoutCards.tsx

Этот компонент служит в качестве контейнера для компоновки свойства данных и отображения карточек с информацией о них. 🔳

ПРИМЕЧАНИЕ: мы используем memo, заключая наш компонент, чтобы избежать повторного рендеринга, что, вероятно, не будет замечено в данном приложении, но это просто совет. Эта мемо-функция перерисовывается только в том случае, если свойство “data” меняет свои значения.

import { memo } from "react"
import { Result } from "../interface"
import { Card } from "./"

interface Props { data: Result[] }

export const LayoutCards = memo(({data}:Props) => {
    return (
        <div className="container-cards">
            {
                (data.length > 0) && data.map( character => (
                    <Card {...character} key={character.id}/>
                ))
            }
        </div>
    )
})
Войдите в полноэкранный режим Выход из полноэкранного режима

Вот как будет выглядеть наш компонент App.tsx.

Мы создаем функцию showData и оцениваем:

  • Если свойство loading равно true, мы возвращаем компонент <Loading/>.
  • Если свойство error равно true, мы возвращаем компоненту <ErrorMessage/>, отправляя ошибку в компонент.
  • Если ни одно из условий не выполняется, это означает, что данные API готовы, и мы возвращаем компонент <LayoutCards/> и отправляем данные на отображение.

Наконец, под компонентом мы раскрываем скобки и вызываем функцию showData.

import { ErrorMessage, Header, Loading, LayoutCards } from './components'
import { useFetch } from './hook';

const App = () => {

  const { data, loading, error } = useFetch();

  const showData =  () => {
    if (loading) return <Loading/>
    if (error) return <ErrorMessage msg={error}/>
    return <LayoutCards data={data} />
  }

  return (
    <>
      <Header/>
      { showData() }
    </>
  )
}
export default App;
Войдите в полноэкранный режим Выход из полноэкранного режима

ПРИМЕЧАНИЕ: Вы также можете перенести функцию showData в хук и изменить расширение файла хука на .tsx, это связано с тем, что вы используете JSX при возврате различных компонентов.


Спасибо, что дошли так далеко. 🙌

Я оставлю репозиторий, чтобы вы могли взглянуть на него, если захотите. ⬇️

Franklin361 / fetching-data-custom-hook

Руководство по получению данных и созданию пользовательского хука

Получение данных и создание пользовательских Hook

Руководство по получению данных и созданию пользовательского хука

Просмотр на GitHub

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