Перейти к содержанию

Знакомство с популярными JS фреймворками - React.js

Добро пожаловать в обзорную статью о библиотеке React. Посмотрим, как она поможет справиться с нашим проектом.

В этой серии⚓︎

Вступление⚓︎

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

Оформлять код мы будем в файлах с расширением JSX — это такое специальное расширение языка JavaScript, позволяющее писать HTML и JavaScript код вместе, без использования тега <script>.

Примечание

Из особенностей перехода с обычной разметки на JSX: в исходной вёрстке проекта (если вы захотите реализовать всё с нуля) нужно заменить class на className, а все атрибуты в kebab-case — на camelCase.

Подготовка⚓︎

Итак, перейдите в папку projects, откройте консоль и запустите следующую команду:

npm create vite@latest react-todo -- --template react

Теперь перейдите в созданную папку react-todo и установите UnoCSS:

npm i -D unocss @iconify-json/heroicons
pnpm create vite react-todo --template react

Теперь перейдите в созданную папку react-todo и установите UnoCSS:

pnpm add -D unocss @iconify-json/heroicons
yarn create vite react-todo --template react

Теперь перейдите в созданную папку react-todo и установите UnoCSS:

yarn add -D unocss @iconify-json/heroicons
bun create vite react --template react

Теперь перейдите в созданную папку react-todo и установите UnoCSS:

bun add -D unocss @iconify-json/heroicons

и обновите vite.config.js:

import { defineConfig } from 'vite'
import UnoCSS from 'unocss/vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [UnoCSS(), react()],
});

Добавьте uno.config.js:

import { defineConfig, presetIcons, presetWind4 } from 'unocss'

export default defineConfig({
  presets: [
    presetWind4(),
    presetIcons(),
  ],
})

Обновите src/main.jsx:

import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import 'virtual:uno.css'

createRoot(document.getElementById('root')).render(
  <App />
)

Как и ранее, мы создадим 3 компонента, не считая корневого (App.jsx). Но прежде добавьте класс bg-gray-200 элементу body в файле index.html, чтобы у страницы был серый фон.

Запускаем проект:

npm run dev
pnpm run dev
yarn dev
bun run dev

App.jsx⚓︎

Перепишите код файла src/App.jsx:

import TodoList from './components/TodoList';

function App() {
  return <TodoList title='Список дел' />;
}

export default App;

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

export default App;

Каждая такая функция возвращает строку в JSX формате, без всяких экранирований и кавычек. Например: <div></div>. Когда возвращаемый блок большой, его требуется окружить круглыми скобками:

return (
  <div>
    <h1>Заголовок</h1>
  </div>
);

TodoList.jsx⚓︎

Итак, после переделки нашей вёрстки (см. Вступление) мы можем использовать её в первом компоненте:

function TodoList(props) {
  return (
    <div className="max-w-sm md:max-w-lg mx-auto my-10 bg-white rounded-md shadow-md overflow-hidden">
      <h1 className="text-2xl font-bold text-center py-4 bg-gray-100">Список дел</h1>
      <ul className="list-none p-4">
        <li className="flex items-center mb-2 hover:cursor-pointer">
          <input type="checkbox" className="mr-2" checked>
          <span className="line-through">Вымыть пол</span>
          <div className="ml-auto text-gray-400 hover:text-gray-600">
            <button className="i-heroicons-trash w-6 h-6" />
          </div>
        </li>
      </ul>
      <div className="p-4 bg-gray-100">
        <div className="flex items-center">
          <input type="text" className="flex-1 mr-2 py-2 px-4 rounded-md border border-gray-300" placeholder="Новая задача" autoFocus>
          <button type="submit" className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded-md">
            Добавить
          </button>
        </div>
      </div>
    </div>
  );
}

export default TodoList;

Далее вынесем элемент списка и форму добавления новой задачи в отдельные компоненты, а также добавим логику React:

import { useState, useEffect } from 'react';
import TodoItem from './TodoItem';
import TodoForm from './TodoForm';

function TodoList(props) {
  // Создаем переменные для хранения списка задач и функции для для обновления текущего списка задач
  const [todos, setTodos] = useState([]);

  // Запрашиваем список задач при обновлении страницы
  useEffect(() => {
    fetch('https://dummyjson.com/todos')
      .then((response) => response.json())
      .then((data) => {
        setTodos(data.todos.slice(0, 10));
      });
  }, []);

  // Обработчик события добавления новой задачи
  const addTask = (title) => {
    if (!title) return;

    // Обязательно используем функцию setTodos, вместо изменения массива напрямую
    setTodos((prevTodos) => [
      ...prevTodos,
      {
        id: crypto.randomUUID(),
        todo: title,
        completed: false,
      },
    ]);
  };

  // Обработчик события переключения статуса задачи
  const toggleTask = (id) => {
    setTodos((prevTodos) => prevTodos.map((t) =>
      t.id === id ? { ...t, completed: !t.completed } : t
    ));
  };

  // Обработчик события удаления задачи
  const deleteTask = (id) => {
    setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
  };

  return (
    <div className='max-w-sm md:max-w-lg mx-auto my-10 bg-white rounded-md shadow-md overflow-hidden'>
      <h1 className='text-2xl font-bold text-center py-4 bg-gray-100'>{props.title}</h1>
      {todos.length > 0 && (
        <ul className='list-none p-4'>
          {todos.map(t => <TodoItem key={t.id} task={t} onToggle={toggleTask} onRemove={deleteTask} />)}
        </ul>
      )}
      <TodoForm onSubmit={addTask} />
    </div>
  );
}

export default TodoList;

useReducer⚓︎

В качестве альтернативы рассмотрим ещё использование хука useReducer вместо useState:

import { useReducer, useEffect } from 'react';
import TodoItem from './TodoItem';
import TodoForm from './TodoForm';

const taskReducer = (todos, action) => {
  switch (action.type) {
    case 'add':
      return [
        ...todos,
        {
          id: crypto.randomUUID(),
          todo: action.title,
          completed: false,
        },
      ];

    case 'toggle':
      return todos.map((t) =>
        t.id === action.id ? { ...t, completed: !t.completed } : t
      );

    case 'delete':
      return todos.filter((t) => t.id !== action.id);

    case 'set':
      return action.todos;

    default:
      throw new Error('Неизвестное действие: ' + action.type);
  }
}

function TodoList(props) {
  const [todos, dispatch] = useReducer(taskReducer, []);

  useEffect(() => {
    fetch('https://dummyjson.com/todos')
      .then((response) => response.json())
      .then((data) => {
        dispatch({ type: 'set', todos: data.todos.slice(0, 10) });
      });
  }, []);

  const addTask = (title) => {
    if (!title) return;
    dispatch({ type: 'add', title });
  };

  const toggleTask = (id) => {
    dispatch({ type: 'toggle', id });
  };

  const deleteTask = (id) => {
    dispatch({ type: 'delete', id });
  };

  return (
    // вывод такой же, как и выше
  );
}

export default TodoItem;

Какой из вариантов вам больше нравится, тот и используйте.

TodoItem.jsx⚓︎

function TodoItem({ task, onToggle, onRemove }) {
  // Обработчик события изменения статуса задачи
  const toggleTask = () => onToggle(task.id);

  // Обработчик события удаления задачи
  const deleteTask = (e) => {
    // Добавляем, чтобы обрабатывался именно клик на кнопке удаления, а не на всем элементе списка
    e.stopPropagation();

    // Отправляем id удаляемого элемента в метод родительского компонента
    onRemove(task.id);
  };

  return (
    <li className='flex items-center mb-2 hover:cursor-pointer' onClick={toggleTask}>
      <input type='checkbox' className='mr-2' checked={task.completed} readOnly />
      <span className={todo.completed ? 'line-through' : ''}>{task.title}</span>
      <div className="ml-auto text-gray-400 hover:text-gray-600">
        <button className="i-heroicons-trash w-6 h-6" onClick={deleteTask} />
      </div>
    </li>
  );
}

export default TodoItem;

Примечание

Запомните, useState позволяет настроить СОСТОЯНИЕ заданной переменной, а не её СТОЯНИЕ, как иногда можно услышать на собеседованиях. Например, следующий код объявляет переменную counter с начальным значением 1, а также функцию setCounter, предназначенную для изменения этой переменной. То есть в React нельзя обновлять состояние простым перезаписыванием переменной, если вы хотите, чтобы она была по-настоящему реактивной:

import { useState } from 'react'

const [counter, setCounter] = useState(1)

Обратите внимание, что мы разложили входные параметры (props), для удобства использования в коде (task.title вместо props.task.title и т. д.).

TodoForm.jsx⚓︎

import { useRef } from 'react';

function TodoForm(props) {
  // Создаем переменную-ссылку для связывания с элементом <input> через атрибут ref
  const inputRef = useRef(null);

  // Обработчик события добавления новой задачи
  const addTask = () => {
    // Передаем введённый в поле текст далее, родительскому компоненту
    props.onSubmit(inputRef.current.value);

    inputRef.current.value = '';
    inputRef.current.focus();
  };

  return (
    <div className='p-4 bg-gray-100'>
      <div className='flex items-center'>
        <input
          ref={inputRef}
          type='text'
          className='flex-1 mr-2 py-2 px-4 rounded-md border border-gray-300'
          placeholder='Новая задача'
          autoFocus
        />
        <button
          className='bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded-md'
          onClick={addTask}
        >
          Добавить
        </button>
      </div>
    </div>
  );
}

export default TodoForm;

Обратите внимание, что в React нет нужды в чём-то типа r-model и прочего, всё удобно реализуется через один атрибут ref и хук useRef.

Документация⚓︎

Если вы заинтересовались React, загляните в локализованный справочник. Новичкам также пригодится короткий вступительный ролик.

Заключение⚓︎

Итак, мы закончили наше простое приложение TODO на React.js:

  • успешно изменили исходную вёрстку в соответствии с требованиями библиотеки
  • познакомились с основными директивами React и форматом JSX
  • настроили валидацию входных параметров в компонентах

В следующей части этой серии мы познакомимся с Preact.


Скачать готовый проект

Комментарии