🔢 Делаем калькулятор на React c хуками и Context API
Идея для проекта выходного дня – собираем калькулятор на React Hooks и Context API. Осваиваем эти интересные технологии в деле. Приводим пример возможной реализации проекта.
Не бывает лучше проекта в портфолио, чем тот, что можно оценить в действии. Через тернии – к звёздам! Показываем пример простого калькулятора на React с использованием современных технологий. Проект можно и нужно улучшать. Текущий результат – кликабельная задеплоенная демка (код на GitHub). Ниже некоторые пояснения о выбранных решениях.
Приступим!
Для начала запустим стандартный create-react-app:
npx create-react-app calculator cd calculator npm start
Файловая структура и CSS
Файловая структура рассматриваемого приложения выглядит следующим образом:
src
├── App.js
├── index.js
└── components
├── BackButton.js
├── Calculator.js
├── ClearButton.js
├── Display.js
├── EqualButton.js
├── FunctionButton.js
├── NegativeButton.js
├── NumberButton.js
├── NumberProvider.js
└── styles
└── Styles.js
Если вы хотите точно
следовать примеру, нужно установить Styled Components для CSS:
npm -i styled-components
Затем необходимо добавить Styled CSS в Styles.js.
Основная структура
Файл Calculator.js задает настройки экрана и клавиатуры.
Он содержит следующий код:
import React from 'react';
import NumberButton from './NumberButton';
import FunctionButton from './FunctionButton';
import ClearButton from './ClearButton';
import Display from './Display';
import EqualButton from './EqualButton';
import BackButton from './BackButton';
import NegativeButton from './NegativeButton';
import { CalculatorStyles } from './styles/Styles';
const Calculator = () => (
<CalculatorStyles>
<div className='display'>
<h1>CALC-U-LATER</h1>
<Display />
</div>
<div className='number-pad'>
<ClearButton />
<BackButton />
<NegativeButton />
<FunctionButton buttonValue='/' />
<NumberButton buttonValue={7} />
<NumberButton buttonValue={8} />
<NumberButton buttonValue={9} />
<FunctionButton buttonValue='*' />
<NumberButton buttonValue={4} />
<NumberButton buttonValue={5} />
<NumberButton buttonValue={6} />
<FunctionButton buttonValue='-' />
<NumberButton buttonValue={1} />
<NumberButton buttonValue={2} />
<NumberButton buttonValue={3} />
<FunctionButton buttonValue='+' />
<div className='zero-button'>
<NumberButton buttonValue={0} />
</div>
<NumberButton buttonValue='.' />
<EqualButton />
</div>
</CalculatorStyles>
);
export default Calculator;
Каждый компонент кнопок следует одной базовой структуре. Нулевая кнопка лежит в отдельном div-блоке, так как мы используем для макета грид.
Обратите внимание, что свойство
buttonValue необходимо только для NumberButton и FunctionButton.
Создание Context API Provider
NumberProvider.js –
это сердце приложения и место, где наши функции обретают жизнь. Здесь же используется React Context API – отличный инструмент передачи данных между компонентами.
Чтобы передать данные или функции через вложенную структуру компонентов, пришлось бы попотеть. Обертка из компонента провайдера позволяет передавать данные в любой вложенный компонент независимо от глубины вложенности. Всякий раз, когда нужно получить данные или использовать функцию, находящуюся внутри провайдера, она будет доступна глобально.
import React from 'react';
export const NumberContext = React.createContext();
const NumberProvider = (props) => {
const number = '0';
return (
<NumberContext.Provider
value={{
number,
}}>
{props.children}
</NumberContext.Provider>
);
};
export default NumberProvider;
Любое переданное значение теперь доступно всем вложенным компонентам. Так что есть всё необходимое для заполнения App.js:
import React from 'react';
import Calculator from './components/Calculator';
import NumberProvider from './components/NumberProvider';
const App = () => (
<NumberProvider>
<Calculator />
</NumberProvider>
);
export default App;
Используем Context Provider
Теперь добавим код для дисплея. Можно выводить значение, передав его в функцию useContext из
нового React Hooks API. Больше не нужно передавать проп через вложенные компоненты.
import React, { useContext } from 'react';
import { NumberContext } from './NumberProvider';
import { DisplayStyles } from './styles/Styles';
const Display = () => {
const { number } = useContext(NumberContext);
return (
<DisplayStyles>
<h2>{number}</h2>
<p>Enter Some Numbers</p>
</DisplayStyles>
);
};
export default Display;
Число, которое передано
на три уровня выше в NumberProvider, становится сразу же доступным компоненту Display при вызове useContext и передаче созданного нами NumberContext. Цифровой
дисплей теперь включен, работает и отображает number, исходно приравненное нулю.
Начинаем работать с Hooks
Хуки позволяют «облегчить» синтаксис класса и получить
состояние внутри функциональных компонентов. Добавим следующий код в NumberProvider.js, чтобы создать хук:
import React, { useState } from 'react';
export const NumberContext = React.createContext();
const NumberProvider = (props) => {
const [number, setNumber] = useState('');
const handleSetDisplayValue = (num) => {
if (!number.includes('.') || num !== '.') {
setNumber(`${(number + num).replace(/^0+/, '')}`);
}
};
return (
<NumberContext.Provider
value={{
handleSetDisplayValue,
number,
}}>
{props.children}
</NumberContext.Provider>
);
};
export default NumberProvider;
Вместо написания класса
с состоянием мы разбиваем состояние и переносим каждую его часть в переменную number. Здесь же используется setNumber, действующая, как функция setState. Для инициализации используется useState.
Создание компонента кнопки
Теперь мы можем вызвать функцию с помощью Context API в любом из вложенных компонентов.
import React, { useContext } from 'react';
import { NumberContext } from './NumberProvider';
const NumberButton = ({ buttonValue }) => {
const { handleSetDisplayValue } = useContext(NumberContext);
return (
<button type='button' onClick={() => handleSetDisplayValue(buttonValue)}>
{buttonValue}
</button>
);
};
export default NumberButton;
Обратите внимание, как
можно начать вводить значения, заданные в NumberProvider, в другие компоненты
приложения с помощью функции useContext. Состояние и функции, влияющие на них,
хранятся в NumberProvider . Просто вызываем определенный контекст – и готово.
Функции провайдера
Завершенный NumberProvider.js находится ниже и содержит следующие функции, которые используются вместе с хуками:
handleSetDisplayValue: задает значение, выводимое на дисплей. Мы проверяем, что в числовой строке есть только один десятичный знак, и ограничиваем длину числа 8 символами. Он передает свойствоbuttonValueвNumberButton.js.handleSetStoredValue: принимает строку и сохраняет ее, позволяя ввести другое число.handleClearValue: сбрасывает всё в 0. Это «функция очистки», которая передается вClearButton.js.handleBackButton: позволяет удалять ранее введенные символы по одному, пока вы не вернетесь в 0. Код привязан кBackButton.js.handleSetCalcFunction: срабатывает при выборе математической функции, передается вFunctionButton.jsи в свойстваbuttonValue.handleToggleNegative: оперирует отображаемыми или сохраненными значениями, передаваемыми вNegativeButton.js.doMath: запускает выбранную математическую операцию.
import React, { useState } from 'react';
export const NumberContext = React.createContext();
const NumberProvider = (props) => {
const [number, setNumber] = useState('');
const [storedNumber, setStoredNumber] = useState('');
const [functionType, setFunctionType] = useState('');
const handleSetDisplayValue = (num) => {
if ((!number.includes('.') || num !== '.') && number.length < 8) {
setNumber(`${(number + num).replace(/^0+/, '')}`);
}
};
const handleSetStoredValue = () => {
setStoredNumber(number);
setNumber('');
};
const handleClearValue = () => {
setNumber('');
setStoredNumber('');
setFunctionType('');
};
const handleBackButton = () => {
if (number !== '') {
const deletedNumber = number.slice(0, number.length - 1);
setNumber(deletedNumber);
}
};
const handleSetCalcFunction = (type) => {
if (number) {
setFunctionType(type);
handleSetStoredValue();
}
if (storedNumber) {
setFunctionType(type);
}
};
const handleToggleNegative = () => {
if (number) {
if (number > 0) {
setNumber(`-${number}`);
} else {
const positiveNumber = number.slice(1);
setNumber(positiveNumber);
}
} else if (storedNumber > 0) {
setStoredNumber(`-${storedNumber}`);
} else {
const positiveNumber = storedNumber.slice(1);
setStoredNumber(positiveNumber);
}
};
const doMath = () => {
if (number && storedNumber) {
switch (functionType) {
case '+':
setStoredNumber(
`${Math.round(`${(parseFloat(storedNumber) + parseFloat(number)) * 100}`) / 100}`
);
break;
case '-':
setStoredNumber(
`${Math.round(`${(parseFloat(storedNumber) - parseFloat(number)) * 1000}`) / 1000}`
);
break;
case '/':
setStoredNumber(
`${Math.round(`${(parseFloat(storedNumber) / parseFloat(number)) * 1000}`) / 1000}`
);
break;
case '*':
setStoredNumber(
`${Math.round(`${parseFloat(storedNumber) * parseFloat(number) * 1000}`) / 1000}`
);
break;
default:
break;
}
setNumber('');
}
};
return (
<NumberContext.Provider
value={{
doMath,
functionType,
handleBackButton,
handleClearValue,
handleSetCalcFunction,
handleSetDisplayValue,
handleSetStoredValue,
handleToggleNegative,
number,
storedNumber,
setNumber,
}}>
{props.children}
</NumberContext.Provider>
);
};
export default NumberProvider;
Итоговое представление экрана
Обновим файл для отображения экрана. Он должен показывать number и storedNumber в соответствии с functionType. Еще есть несколько проверок, таких как отображение 0 при вставке пустой строки вместо числа.
import React, { useContext } from 'react';
import { NumberContext } from './NumberProvider';
import { DisplayStyles } from './styles/Styles';
const Display = () => {
const { number, storedNumber, functionType } = useContext(NumberContext);
return (
<DisplayStyles>
<h2>{!number.length && !storedNumber ? '0' : number || storedNumber}</h2>
<p>{!storedNumber ? 'ENTER SOME NUMBERS' : `${storedNumber} ${functionType} ${number}`}</p>
</DisplayStyles>
);
};
export default Display;
Заключение
Библиотека программиста надеется, что данный материал немного прояснит вопрос о том, как можно использовать хуки React вместе с Context API. Использование встроенных функций React дает ряд преимуществ:
- простой для понимания синтаксис и отсутствие беспорядка в компонентах класса. Больше никаких super и constructor – просто чистые переменные;
- проще устанавливать и использовать состояние внутри компонентов и между ними;
- нет необходимости в Redux для небольших проектов.
React Hooks и Context API – отличные способы упростить приложения React и писать более чистый код. О других технологиях React читайте в нашем руководстве для React-разработчика.