Pull to refresh
1012.67
OTUS
Цифровые навыки от ведущих экспертов

React.js: Знакомимся с useReducer, Axios и JSON Server на примере создания инвентарного списка

Reading time13 min
Views3K
Original author: TAPAS ADHIKARY

Когда речь заходит о веб-разработке, трудно обойти вниманием React.js. Она уже десять лет является одной из главных библиотек пользовательского интерфейса и лежит в основе множества популярных фреймворков, таких как, например, Next.js.

Если вы являетесь React-разработчиком, вы, скорее всего, уже оценили его компонентную архитектуру, однонаправленную привязку данных, огромную поддержку со стороны сообщества и страсть команды React к созданию новых возможностей для разработчиков.

Для тех, кто только начинает работать с React или является новичком в этой области, здесь, на freeCodeCamp, есть полный роадмап по React.js. И я думаю, что изучать библиотеку будет намного легче, если вы уже знаете основы JavaScript.

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

А также между делом мы создадим мок API сервера с помощью JSON Server, будем использовать axios для вызова API и, наконец, воспользуемся хуком useReducer для управления состоянием.

Звучит интересно? Тогда давайте приступим. Ну а если вам больше приходятся по душе уроки в формате видео, то вот и видео-версия этого проекта: ?.

Настройка проекта с помощью React и TailwindCSS

Прежде чем начинать делать что-либо, давайте настроим проект. Кстати, вы можете поглядывать на исходный код по мере прочтения статьи.

Для создания этого приложения мы будем использовать React с Vite и TailwindCSS. Вы можете настроить эти инструменты самостоятельно, выполнив несколько простых шагов из документации по Vite и TailwindCSS.

Но почему бы не использовать что-то, что предоставляет все это вместе? Это сэкономит ваше время для будущих React-проектов, поскольку вы сможете каждый раз использовать для их создания одну и ту же инфраструктуру.

Перейдите в этот репозиторий и нажмите на кнопку Use this template (использовать этот шаблон), как показано на изображении ниже. Это поможет вам создать совершенно новый репозиторий на основе шаблона с настроенными Vite, React и TailwindCSS.

Screenshot-2024-02-12-at-5.25.29-PM
Создание репозитория React-проекта с готовыми TailwindCSS и Vite на основе шаблона

Теперь задайте подходящее вашему репозиторию имя (в этой статье мы назовем его inventory-list) и описание. Если вы хотите, вы можете оставить репозиторий приватным. В любом случае перейдите к созданию репозитория, нажав на кнопку в самом низу.

Screenshot-2024-02-12-at-5.30.34-PM
Предоставьте информацию о новом репозитории

Вот и все. У вас есть репозиторий со всеми основными компонентами готовыми для начала работы. Теперь перейдите в командную строку/терминал и клонируйте только что созданный репозиторий:

git clone <YOUR NEWLY CREATED REPOSITORY URL>

Перейдите в каталог проекта и установите зависимости проекта с помощью следующих команд:

## Перейдите в каталог проекта.
cd inventory-list

## Установите зависимости

## С помощью NPM
npm install

## С помощью Yarn
yarn install

## С помощью PNPM
pnpm install

После успешной установки зависимостей выполните следующую команду, чтобы запустить проект на локальном сервере:

## Запустите проект локально

## С помощью NPM
npm run dev

## С помощью Yarn
yarn dev

## С помощью PNPM
pnpm dev

Теперь проект должен быть запущен локально и доступен по дефолтному URL, http://localhost:5173. Вы можете перейти по этому URL в браузере и импортировать исходный код проекта в ваш любимый редактор кода (лично я использую VS Code). Теперь мы готовы писать код.

Как настроить сервер с помощью JSON Server

JSON Server — это оптимальный вариант, когда вы хотите работать с фиктивным/моковым API для предоставления нужным вам данных. Его легко настроить и приспособить к вашему конкретному случаю.

Давайте настроим JSON Server для нашего проекта. Первым делом его нужно установить.

Откройте терминал в корневом каталоге проекта и введите следующую команду для установки JSON Server:

## С помощью NPM
npm install json-server

## С помощью Yarn
yarn add json-server

## С помощью PNPM
pnpm install json-server

В качестве источников данных для выполнения таких HTTP-операций, как GET/POST/PUT/PATCH/DELETE, JSON Server использует JSON-файлы. Создайте каталог server/database в каталоге src/. Теперь создайте в каталоге src/server/database/ файл data.json со следующим содержимым:

{
  "edibles": [
    {
      "id": 1,
      "picture": "?",
      "name": "Banana",
      "price": 32,
      "quantity": 200,
      "type": "fruits"
    },
    {
      "id": 2,
      "picture": "?",
      "name": "Strawberry",
      "price": 52,
      "quantity": 100,
      "type": "fruits"
    },
    {
      "id": 3,
      "picture": "?",
      "name": "Checken",
      "price": 110,
      "quantity": 190,
      "type": "foods",
      "sub-type": "Non-Veg"
    },
    {
      "id": 4,
      "picture": "?",
      "name": "Lettuce",
      "price": 12,
      "quantity": 50,
      "type": "Vegetables"
    },
    {
      "id": 5,
      "picture": "?",
      "name": "Tomato",
      "price": 31,
      "quantity": 157,
      "type": "Vegetables"
    },
    {
      "id": 6,
      "picture": "?",
      "name": "Mutton",
      "price": 325,
      "quantity": 90,
      "type": "Non-Veg"
    },
    {
      "id": 7,
      "picture": "?",
      "name": "Carrot",
      "price": 42,
      "quantity": 190,
      "type": "Vegetables"
    }
  ]
}

Файл data.json содержит массив съедобных товаров. Каждый товар в массиве имеет такие свойства, как картинка, название, цена, количество и тип, которые будут отображаться в инвентарном списке.

Нам осталось добавить скрипт в файл package.json, который позволит нам легко запускать JSON Server. Откройте файл package.json и добавьте эту строку в объект scripts:

"start-server": "json-server --watch ./src/server/database/data.json"

Затем перейдите в терминал и выполните следующую команду, чтобы запустить JSON Server:

## С помощью NPM
npm run start-server

## С помощью Yarn
yarn start-server

## С помощью PNPM
pnpm run start-server

В терминале должно появиться следующее сообщение:

Screenshot-2024-03-07-at-8.46.32-AM-1
Вывод

Оно указывает на то, что JSON Server запущен локально на localhost:3000 и существует конечная точка API под названием edibles, которая выдает нам данные. Теперь вы можете обратиться к URL http://localhost:3000/edibles из браузера, чтобы посмотреть эти данные (полученные вызовом метода GET):

image-32
Вывод API

Отлично! Теперь у нас есть конечная точка API /edibles, которую можно использовать в React-компоненте.

Как настроить и работать с Axios

Axios — это HTTP-клиент, который помогает нам выполнять асинхронные вызовы на основе промисов из браузера и среды Node.js. Он обладает рядом полезных функций, которые делают его одной из самых используемых библиотек для асинхронных запросов/ответов.

Следует отметить, что в этом проекте вместо Axios мы могли бы использовать fetch Web API из JavaScript. Единственная причина использования Axios здесь — это мое желание плавно познакомить вас с ним. В следующих статьях вы узнаете, как он используется для работы с JWT-токенами в React-приложении. Следите за новостями!

Откройте терминал в корневом каталоге проекта и выполните следующую команду для установки Axios:

## С помощью NPM
npm install axios

## С помощью Yarn
yarn add axios

## С помощью PNPM
pnpm install axios

Вот и все. Мы вернемся к Axios немного позже, после того как подготовим основные компоненты, необходимые для нашего инвентарного списка.

Как использовать хук useReducer из React

React — это библиотека пользовательского интерфейса, которая делает упор на компонентную архитектуру. Компонент — это обособленная сущность, которая должна выполнять одну задачу. Несколько компонентов объединяются вместе для создания конечного пользовательского интерфейса.

Часто компонент имеет свои собственные приватные данные. Мы называем их состояниями (states) компонента. Значение состояния определяет поведение и внешний вид компонента. При изменении состояния компонент перестраивается, чтобы соответствовать актуальному значению состояния.

Традиционный способ работы с состоянием в React — это хук useState. Он отлично работает до тех пор, пока изменения состояния тривиальны. Когда логика изменения состояния становится более сложной и вам нужно следить за несколькими сценариями вокруг нее, useState может сделать эту работу очень неудобной. В этом случае вам следует подумать об использовании хука useReducer.

useReducer — это стандартный хук из библиотеки React. Он принимает два основных параметра:

  • initState: начальное значение состояния.

  • reducer (редуктор): функция JavaScript, которая содержит логику изменения состояния на основе экшена (или триггера).

Хук возвращает следующее:

  • state: текущее значение состояния.

  • Функцию dispatch: функция, которая сообщает соответствующему редуктору, что делать дальше и с какими данными работать.

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

image-27
Анатомия хука useReducer

Как создавать экшены

Функция reducer — это сердце хука useReducer. Она выполняет всю необходимую логику, чтобы поддерживать ваше приложение в актуальном и валидном состоянии.

Но как функция reducer узнает о своей задаче? Кто говорит функции reducer, что делать и с какими данными работать? Здесь на помощь приходят экшены (actions) — объекты, содержащие все детали для редуктора.

Мы определяем экшены с типами, которые указывают на этапы изменения состояния в функции-редукторе. Тот же объект экшена может нести в себе данные приложения (иногда мы называем их полезной нагрузкой – payload) для передачи в функцию reducer, когда компонент выполняет диспетчеризацию.

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

Создайте каталог actions/ в каталоге src/. Теперь создайте в каталоге src/actions/ файл index.js со следующим содержимым:

const FETCH_ACTIONS = {
  PROGRESS: 'progress',
  SUCCESS: 'success',
  ERROR: 'error',
}

export { FETCH_ACTIONS };

В нем мы определили три экшена: PROGRESS, SUCCESS и ERROR. Далее давайте создадим редуктор.

Создаем редуктор для нашего инвентаря

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

Создайте каталог reducers/ в каталоге src/. Теперь в каталоге src/reducers/ создайте файл inventoryReducers.js.

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

import { FETCH_ACTIONS } from "../actions"

Вы можете определить начальное состояние в файле редуктора. Хуку useReducer нужен редуктор и начальное состояние, чтобы дать нам текущее значение состояния, помните? Давайте определим начальное состояние.

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

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

Создайте переменную initialState, присвоив ей следующее значение:

const initialState = {
  items: [],
  loading: false,
  error: null,
}

Далее давайте создадим функцию редуктора. Создайте функцию inventoryReducer со следующим кодом:

const inventoryReducer = (state, action) => {

  switch (action.type) {
    case FETCH_ACTIONS.PROGRESS: {
      return {
        ...state,
        loading: true,
      }
    }

    case FETCH_ACTIONS.SUCCESS: {
      return {
        ...state,
        loading: false,
        items: action.data,
      }
    }

    case FETCH_ACTIONS.ERROR: {
      return {
        ...state,
        loading: false,
        error: action.error,
      }
    }
    
    default: {
      return state;
    }      
  }

}

Давайте разберемся в приведенном выше фрагменте кода. Функция inventoryReducer принимает два аргумента: state и action. Функция-редуктор работает с состоянием на основе типа экшена. Например,

  • Если это экшен PROGRESS, мы хотим, чтобы значение loading было true.

  • Для экшена SUCCESS мы хотим заполнить items данными, полученными из ответа API, а также сделать значение loading равным false.

  • Для экшена ERROR мы предоставим значение свойству error.

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

Наконец, экспортируйте функцию inventoryReducer и объект initialState:

export {inventoryReducer, initialState};

А вот полный код из файла inventoryReducers.js:

import { FETCH_ACTIONS } from "../actions"

const initialState = {
  items: [],
  loading: false,
  error: null,
}

const inventoryReducer = (state, action) => {

  switch (action.type) {
    case FETCH_ACTIONS.PROGRESS: {
      return {
        ...state,
        loading: true,
      }
    }

    case FETCH_ACTIONS.SUCCESS: {
      return {
        ...state,
        loading: false,
        items: action.data,
      }
    }

    case FETCH_ACTIONS.ERROR: {
      return {
        ...state,
        loading: false,
        error: action.error,
      }
    }
    
    default: {
      return state;
    } 
  }

}

export {inventoryReducer, initialState};

Как создать компонент инвентарного списка 

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

Создайте в каталоге src/ каталог с именем components/. Теперь в каталоге stc/components/ создайте файл InventoryList.jsx.

Сначала импортируйте необходимые вещи, например:

  • Хук useReducer, в котором мы будем использовать редуктор инвентаря.

  • Хук useEffect для обработки асинхронного вызова с помощью Axios.

  • Редуктор инвентаря и начальное состояние, которые мы создали выше.

  • Все необходимые нам экшены.

  • axios для выполнения асинхронных вызовов.

import { useReducer, useEffect } from "react";

import { inventoryReducer, initialState } from "../reducers/inventoryReducer";

import { FETCH_ACTIONS } from "../actions";

import axios from "axios";

Теперь создайте функцию, чтобы определить компонент:

const InventoryList = () => {

  const [state, dispatch] = useReducer(inventoryReducer, initialState);

  const { items, loading, error} = state;

  return(
    <div className="flex flex-col m-8 w-2/5">
      
    </div>
  );
};

Здесь:

  • Мы использовали хук useReducer. Мы передали ему в качестве аргументов inventoryReducer и initialState для получения значения текущего состояния (state) и функции dispatch.

  • Поскольку мы знаем, что объект состояния имеет свойства items, loading и error, мы деструктурируем их в нашем компоненте. Мы будем использовать их в ближайшее время.

  • Компонент возвращает пустой div, который мы будем изменять по ходу работы.

Наконец, нам осталось сделать экспорт по умолчанию компонента следующим образом:

export default InventoryList;

Как использовать Axios для получения данных и отправки их в редуктор

Настало время получения данных! Получение данных путем асинхронного вызова — это побочный эффект, который необходимо обработать в вашем компоненте. Скопируйте и вставьте код useEffect внутрь функции InventoryList.

// -- Код выше, как он есть

const InventoryList = () => {

  // --- Код выше, как он есть
    
    
  useEffect(() => {
    dispatch({type: FETCH_ACTIONS.PROGRESS});

    const getItems = async () => {
      try{
        let response = await axios.get("http://localhost:3000/edibles");
        if (response.status === 200) {
          dispatch({type: FETCH_ACTIONS.SUCCESS, data: response.data});
        }
      } catch(err){
        console.error(err);
        dispatch({type: FETCH_ACTIONS.ERROR, error: err.message})
      }
    }

    getItems();

  }, []);
    
  // --- Оператор возврата JSX ниже, как он есть 

Давайте разберемся в ходе выполнения этого кода:

  • В начале колбека useEffect мы отправили экшен PROGRESS. Он вызывает функцию-редуктор с типом экшена progress, чтобы установить значение свойства loading в true. Позже мы сможем использовать значение свойства loading в JSX для отображения индикатора загрузки.

  • Затем мы используем Axios для асинхронного вызова API по URL. Получив ответ, мы проверяем, является ли он успешным, и в этом случае мы отправляем экшн SUCCESS вместе с данными items (полезная нагрузка, помните?) из ответа. В этот раз диспетчер вызовет редуктор с экшеном success, чтобы изменить свойства items и loading соответствующим образом.

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

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

Завершаем работу над частью JSX

Часть JSX довольно проста:

// -- Код выше, как он есть

const InventoryList = () => {

  // -- Код выше, как он есть

  return (
    <div className="flex flex-col m-8 w-2/5">
      {
        loading ? (
          <p>Loading...</p>
        ) : error ? (
          <p>{error}</p>
        ) : (
          <ul className="flex flex-col">
            <h2 className="text-3xl my-4">Item List</h2>
            {
              items.map((item) => (
                <li
                  className="flex flex-col p-2 my-2 bg-gray-200 border rounded-md" 
                  key={item.id}>
                  <p className='my-2 text-xl'>
                    <strong>{item.name}</strong> {' '} {item.picture} of type <strong>{item.type}</strong>
                    {' '} costs <strong>{item.price}</strong> INR/KG.
                  </p>
                  <p className='mb-2 text-lg'>
                    Available in Stock: <strong>{item.quantity}</strong>
                  </p>

                </li>
              ))
            }
            
          </ul>
        )
      }

    </div>
  )
}

export default InventoryList;

Вот что происходит в этом коде:

  • Мы показываем сообщение “loading...”, если значение свойства загрузки состояния равно true.

  • Мы показываем сообщение об ошибке, если она имела место быть.

  • Ни в том, ни в другом случае мы не перебираем элементы инвентаря с помощью функции map. Каждый из элементов в массиве items содержит такую информацию, как изображение, название, цена и многое другое. Мы отображаем эту информацию в удобочитаемом виде.

Вот полный код компонента InventoryList:

import { useReducer, useEffect } from "react";
import { inventoryReducer, initialState } from "../reducers/inventoryReducer";
import { FETCH_ACTIONS } from "../actions";

import axios from "axios";

const InventoryList = () => {

  const [state, dispatch] = useReducer(inventoryReducer, initialState);

  const { items, loading, error} = state;

  console.log(items, loading, error);

  useEffect(() => {
    dispatch({type: FETCH_ACTIONS.PROGRESS});

    const getItems = async () => {
      try{
        let response = await axios.get("http://localhost:3000/edibles");
        if (response.status === 200) {
          dispatch({type: FETCH_ACTIONS.SUCCESS, data: response.data});
        }
      } catch(err){
        console.error(err);
        dispatch({type: FETCH_ACTIONS.ERROR, error: err.message})
      }
    }

    getItems();

  }, []);


  return (
    <div className="flex flex-col m-8 w-2/5">
      {
        loading ? (
          <p>Loading...</p>
        ) : error ? (
          <p>{error}</p>
        ) : (
          <ul className="flex flex-col">
            <h2 className="text-3xl my-4">Item List</h2>
            {
              items.map((item) => (
                <li
                  className="flex flex-col p-2 my-2 bg-gray-200 border rounded-md" 
                  key={item.id}>
                  <p className='my-2 text-xl'>
                    <strong>{item.name}</strong> {' '} {item.picture} of type <strong>{item.type}</strong>
                    {' '} costs <strong>{item.price}</strong> INR/KG.
                  </p>
                  <p className='mb-2 text-lg'>
                    Available in Stock: <strong>{item.quantity}</strong>
                  </p>

                </li>
              ))
            }
            
          </ul>
        )
      }

    </div>
  )
}

export default InventoryList

Как использовать инвентарный список в компоненте приложения

Теперь нам нужно сообщить компоненту App о компоненте InventoryList, чтобы мы могли его отобразить. Откройте файл App.jsx и замените его содержимое следующим фрагментом кода:

import InventoryList from "./components/InventoryList"

function App() {

  return (
    <>
      <InventoryList />
    </
  )
}

export default App

Вот и всё. Убедитесь, что сервер вашего приложения запущен. Теперь зайдите в приложение через браузер, используя следующий URL http://localhost:5173/.

image-28
Конечный результат — инвентарный список

Заключение

Надеюсь, вам понравилось разбираться в этом проекте и вы узнали что-то новое о React. Вот исходный код на моем GitHub. Пожалуйста, не стесняйтесь расширять проект, добавляя такие функции, как:

  • Добавление предмета в инвентарь

  • Редактирование элемента в инвентаре

  • Удаление предмета из инвентаря

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

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

Давайте коннектиться!

  • Я преподаю на своем YouTube-канале tapaScript. Подписывайтесь на канал, если хотите изучать JavaScript, ReactJS, Next.js, Node.js, Git и все, что касается веб-разработки в принципе.

  • Подписывайтесь на меня в X (Twitter) или LinkedIn, если не хотите пропустить ежедневную порцию советов по веб-разработке и программированию.

  • Найти все мои публичные выступления можно здесь.

  • Следите за моей работой в опенсорсе на GitHub.

Увидимся в следующей статье. А пока, пожалуйста, берегите себя и будьте счастливы.


В заключение приглашаем всех желающих на бесплатный открытый урок «Хуки и оптимизация в React», который пройдет 27 мая. На нем коснемся работы с состоянием в компонентах react. Напишем большой список инпутов, проведем анализ производительности и оптимизируем код. Записаться можно по ссылке.

Tags:
Hubs:
Total votes 6: ↑6 and ↓0+9
Comments2

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS