Урок 10: Создание простого счетчика в React

Сегодня мы закрепим наши знания о хуке useState, создав свой первый по-настоящему интерактивный компонент, классический счетчик. Это тот самый «Hello, World!» в мире интерактива на React и понимание этой механики откроет вам дверь к созданию любых динамических интерфейсов.

Пишем компонент счетчика с кнопками «+» или «-»

Вы уже познакомились с useState в прошлых уроках. Мы разобрали, что это такое. Это специальная функция, которая позволяет нашим функциональным компонентам «запоминать» данные между разными вызовами. Без состояния наши компоненты были бы просто статичными, как фотография. Они отображали бы то, что им передали при создании и никогда не менялись бы в ответ на действия пользователя.

Сегодня мы возьмем этот теоретический фундамент и построим на нем первое реальное здание. Мы создадим компонент Counter, который будет отображать числовое значение и две кнопки: одна для увеличения этого значения, другая для уменьшения. Каждое нажатие на кнопку будет приводить к перерисовке компонента с новым значением. В этом и заключается суть React. Мы описываем, как интерфейс должен выглядеть в зависимости от состояния, а React берет на себя всю сложную работу по эффективному обновлению DOM.

Почему именно счетчик? Это идеальный учебный пример. Он максимально прост визуально, но при этом содержит в себе всю логику работы с состоянием: его инициализацию, обновление и отображение. Отработав этот паттерн, вы с легкостью сможете применять его для управления любыми другими данными: текстом в форме, флагами открытия/закрытия модальных окон, элементами списка и так далее. Это как выучить базовый аккорд на гитаре. Зная его, вы уже сможете сыграть сотни песен.

Структура нашего компонента

Прежде чем писать код, давайте мысленно представим, что нам нужно. Наш компонент должен:

  1. Хранить текущее значение счетчика. Это и будет наше состояние. Мы назовем его, например, count.

  2. Отображать это значение на странице.

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

Теперь переведем это на язык React. Вспомним структуру useState:

javascript
const [stateVariable, setStateFunction] = useState(initialValue);

В нашем случае:

  • stateVariable это count, текущее значение счетчика.

  • setStateFunctionэто setCount, функция для его обновления.

  • initialValue,  давайте начнем с 0.

Создадим новый файл Counter.js и начнем наполнять его жизнью.

jsx
// Counter.js
import React, { useState } from 'react';

function Counter() {
  // Объявляем переменную состояния count с начальным значением 0
  const [count, setCount] = useState(0);

  // Функции для обработки нажатий на кнопки
  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  return (
    <div style={{ textAlign: 'center', padding: '20px' }}>
      <h2>Счетчик: {count}</h2>
      {/* Подключаем наши функции к кнопкам */}
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
    </div>
  );
}

export default Counter;

Давайте построчно разберем, что здесь происходит.

  1. import React, { useState } from 'react'; импортируем необходимые инструменты. Без этой строки React не узнает, что такое useState.

  2. function Counter() { ... } объявляем наш функциональный компонент.

  3. const [count, setCount] = useState(0); это сердце нашего компонента. В этой строке мы «просим» React выделить нам ячейку памяти для хранения числа. Изначально в этой ячейке будет лежать 0. React возвращает нам массив из двух элементов: текущее значение (count) и функцию для его обновления (setCount). Мы сразу же «распаковываем» этот массив в две отдельные константы с помощью синтаксиса деструктуризации.

  4. const increment = () => { ... } это функция-обработчик. Она будет вызвана, когда пользователь нажмет на кнопку «+». Внутри нее мы вызываем setCount(count + 1). Мы не меняем переменную count напрямую (например, count = count + 1 это грубейшая ошибка!). Вместо этого мы передаем новое значение в функцию setCount. Это как сказать React: «Эй, состояние устарело! Пожалуйста, замени его на это новое значение и перерисуй компонент».

  5. const decrement = () => { ... } абсолютно аналогичная функция для уменьшения значения.

  6. В блоке return мы описываем JSX, который должен быть отрендерен. Обратите внимание:

    • Мы отображаем текущее значение счетчика с помощью <h2>Счетчик: {count}</h2>. Когда count меняется, меняется и эта надпись.

    • Кнопкам мы передаем наши функции-обработчики в пропс onClick. Важно: мы передаем саму функцию (onClick={increment}), а не результат ее вызова (onClick={increment()},  так функция вызовется сразу при рендере, а не по клику!).

Теперь, если мы импортируем и используем этот компонент в нашем App.js, мы увидим работающий счетчик!

jsx
// App.js
import React from 'react';
import Counter from './Counter';

function App() {
  return (
    <div className="App">
      <h1>Мое первое React-приложение</h1>
      <Counter />
    </div>
  );
}

export default App;

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

Функциональные обновления и асинхронность состояния

Наш код работает, но он содержит потенциальную ловушку. Давайте рассмотрим сценарий, который ее демонстрирует. Представьте, что мы хотим добавить кнопку «+3», которая увеличивает счетчик сразу на 3 единицы. Наивная реализация могла бы выглядеть так:

jsx
// Потенциально проблемный код
const incrementByThree = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
};

Если вы добавите такую функцию и попробуете ее, то увидите, что счетчик увеличится всего на 1, а не на 3. Почему?

Дело в том, что обновления состояния в React асинхронны. Когда вы вызываете setCount, React не меняет значение count мгновенно. Вместо этого он планирует обновление. В приведенном выше примере все три вызова setCount(count + 1) используют одно и то же, текущее на момент вызова функции, значение count. Представьте, что count равен 5. Для React это выглядит как:

  • setCount(5 + 1) -> планирует обновление на 6.

  • setCount(5 + 1) -> снова планирует обновление на 6.

  • setCount(5 + 1) -> и снова на 6.

В итоге последнее «планирование» побеждает и счетчик становится равен 6.

Как с этим бороться? Для этого существует концепция функциональных обновлений. Вместо того чтобы передавать в setCount новое значение, мы можем передать функцию. Эта функция получит в качестве аргумента самое актуальное на данный момент значение состояния (назовем его prevCount или latestCount) и должна вернуть новое значение.

Исправим наш пример:

jsx
// Корректный код с функциональным обновлением
const incrementByThree = () => {
  setCount(prevCount => prevCount + 1);
  setCount(prevCount => prevCount + 1);
  setCount(prevCount => prevCount + 1);
};

Теперь логика изменилась:

  • Первый вызов setCount говорит: «Возьми самое актуальное значение (допустим, 5), прибавь к нему 1 и установи результат (6)».

  • Второй вызов, видя, что в очереди уже есть обновление до 6, берет это значение (6), прибавляет 1 и устанавливает 7.

  • Третий вызов делает то же самое и итоговое значение становится 8.

Все работает как часы! Функциональные обновления это лучшая практика, особенно когда новое состояние зависит от предыдущего. Давайте перепишем наши исходные функции increment и decrement в этом стиле. Это сделает наш код более надежным.

jsx
// Counter.js (обновленная версия)
import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(prevCount => prevCount + 1); // Используем предыдущее состояние
  };

  const decrement = () => {
    setCount(prevCount => prevCount - 1);
  };

  const incrementByThree = () => {
    setCount(prevCount => prevCount + 3);
  };

  return (
    <div style={{ textAlign: 'center', padding: '20px' }}>
      <h2>Счетчик: {count}</h2>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={incrementByThree}>+3</button>
    </div>
  );
}

export default Counter;

Запомните это правило: если вы вычисляете новое состояние на основе старого, всегда используйте функциональную форму setState.

Практические задачи для закрепления материала

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

Задача 1: Сброс на ноль

Добавьте в наш счетчик третью кнопку «Сбросить». При нажатии на нее значение счетчика должно возвращаться к своему начальному состоянию, то есть к 0.

Подсказка. Вам нужно создать новую функцию-обработчик (например, `reset`), которая будет вызывать `setCount` и передавать туда начальное значение (0).

Решение

jsx
// ... внутри компонента Counter ...
const reset = () => {
  setCount(0);
};

// ... внутри JSX ...
return (
  <div ...>
    ...
    <button onClick={reset}>Сбросить</button>
  </div>
);

Задача 2: Случайное значение

Добавьте кнопку «Случайное», которая будет устанавливать счетчик в произвольное число от 1 до 100. Для генерации случайного числа используйте Math.floor(Math.random() * 100) + 1.

Решение

jsx
// ... внутри компонента Counter ...
const randomize = () => {
  const randomValue = Math.floor(Math.random() * 100) + 1;
  setCount(randomValue);
};

// ... внутри JSX ...
<button onClick={randomize}>Случайное</button>

Задача 3: Счетчик с шагом

Усложним задачу. Давайте добавим возможность пользователю самому задавать шаг, на который изменяется счетчик. Добавьте поле ввода (input), где можно ввести число (например, 5) и тогда кнопки «+» и «-» будут изменять значение счетчика на это число.

Подсказка: Вам понадобится еще один useState для хранения значения из поля ввода.

Решение

jsx
import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  // Добавляем состояние для шага
  const [step, setStep] = useState(1); // Начальный шаг = 1

  const increment = () => {
    setCount(prevCount => prevCount + step);
  };

  const decrement = () => {
    setCount(prevCount => prevCount - step);
  };

  // Обработчик изменения поля ввода
  const handleStepChange = (event) => {
    // event.target.value - это всегда строка, преобразуем в число
    const newStep = parseInt(event.target.value, 10) || 1; // Если не число, то 1
    setStep(newStep);
  };

  return (
    <div style={{ textAlign: 'center', padding: '20px' }}>
      <h2>Счетчик: {count}</h2>
      {/* Поле для ввода шага */}
      <div>
        <label>
          Шаг:
          <input
            type="number"
            value={step}
            onChange={handleStepChange}
            style={{ margin: '0 10px' }}
          />
        </label>
      </div>
      <button onClick={increment}>+{step}</button>
      <button onClick={decrement}>-{step}</button>
    </div>
  );
}

export default Counter;

Задача 4 (повышенной сложности): Верхняя и нижняя граница

Сделайте так, чтобы счетчик не мог уходить в отрицательные значения и не мог превышать 100. При достижении границы соответствующая кнопка (например, «-» при значении 0) должна становиться неактивной (disabled).

Подсказка. Вы можете проверять текущее count внутри функций increment и decrement и решать, вызывать ли setCount. Либо вы можете вычислять атрибут disabled для кнопок прямо в JSX на основе текущего count.

Решение (через disabled в JSX)

jsx
import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  const decrement = () => {
    setCount(prevCount => prevCount - 1);
  };

  // Вычисляем, должны ли кнопки быть disabled
  const isDecrementDisabled = count <= 0;
  const isIncrementDisabled = count >= 100;

  return (
    <div style={{ textAlign: 'center', padding: '20px' }}>
      <h2>Счетчик: {count}</h2>
      <button onClick={decrement} disabled={isDecrementDisabled}>-1</button>
      <button onClick={increment} disabled={isIncrementDisabled}>+1</button>
    </div>
  );
}

export default Counter;

В этом решении кнопка «-1» станет серой и неактивной, когда count станет меньше или равен 0, а кнопка «+1» когда count достигнет 100.

Итоги десятого урока

Давайте подведем черту. Сегодня вы, коллеги, прошли ключевой рубеж. Вы не просто узнали, а почувствовали на практике, как работает состояние в React.

  • Мы еще раз вспомнили хук useState и применили его для создания интерактивного компонента-счетчика.

  • Мы разобрали полную структуру такого компонента: объявление состояния, создание функций-обработчиков и их привязка к элементам интерфейса.

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

  • Вы решили несколько практических задач, которые показали, как можно расширять и модифицировать базовую логику.

Самое главное, вы теперь понимаете основной поток данных в React. Пользовательское действие (клик) -> Вызов функции-обработчика -> Обновление состояния через setState -> Ре-рендер компонента с новыми данными. Этот цикл лежит в основе 99% всей интерактивности в React-приложениях.

Как всегда, если есть вопросы, не стесняйтесь задавать их в комментариях к уроку.

Удачи в практике и до скорой встречи в одиннадцатом уроке!

← Все уроки курса по React для начинающих

Поделиться статьей:
Поддержать автора блога

Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.

Персональные рекомендации
Оставить комментарий