➡️ Весь сопутствующий материал доступен на GitHub.

Замысел проекта

Задачей этого руководства было не идти проторенным путем “волшебных” готовых шаблонов (например create-react-app и react-boilerplate), а самостоятельно разобраться во множестве динамических компонентов, формирующих приложение React .

До этого подобную тему освещал Jedai Saboteur в своей статье Creating a React App… From Scratch (англ.), на которую ввиду ее грамотности даже сделали ссылку в официальной документации React.

Как бы то ни было, но время идет, и я решил создать современное приложение React с нуля в 2021 году. В итоге было решено добавить в цепочку инструментов ряд “важных элементов” и задействовать последние версии основных библиотек. В некотором смысле у меня получилась последняя версия вышеприведенного руководства. 

Задача

Наша цель проста: создать приложение React с нуля. “С нуля” подразумевает не то, что я буду разрабатывать вспомогательные инструменты, а то, что вместо использования, например, create-react-app, я сконфигурирую их самостоятельно.

Тем не менее помимо настройки простого рабочего приложения я также определил пару дополнительных, “важных” по меркам современного стека, требований:

  1. Поддержка TypeScript.
  2. Возможность внешнего управления состоянием.

Инструменты

Озадачившись списком необходимых инструментов, я решил обратиться к документации React.

После прочтения раздела Creating Toolchain from Scratch я прикинул следующий список:

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

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

Компилятор, например Babel. Дает возможность писать современный JS-код, который будет успешно работать в старых браузерах.

Этот краткий отрывок достаточно подробно раскрывает суть необходимых компонентов. В итоге на их роли я выбрал:

  • Пакетный менеджер: Yarn
  • Бандлер: webpack
  • Компилятор: Babel

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

Однако, согласно моим требованиям, все еще недостает одного элемента: библиотеки управления состоянием. 

Redux могла стать очевидным выбором, но я предпочел Kea. Дело в том, что разработана Kea на базе Redux, но при этом существенно упрощает управление состоянием. 

По правде говоря, здесь я определенно предвзят, так как выбрал Kea больше из-за того, что использую эту библиотеку на работе, а разработал ее мой коллега

Начало

Сперва создайте каталог и выполните в нем yarn init.

Когда система попросит задать entry point, укажите src/index.tsx. Далее станет понятно, почему именно так.

Далее внутри этого каталога создайте еще два: src и public.

В src будет храниться весь исходный код проекта, а в public мы разместим статические ресурсы. 

Настройка

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

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

В качестве примера я решил взять для этого руководства webpack v5, который вызвал ряд проблем совместимости с конфигурациями, используемыми мной ранее в проектах с webpack v4. Как обычно разобраться с этой проблемой и попутно запастись дополнительными знаниями мне помогла документация, различные статьи и посты на Stack Overflow. 

Babel

Для работы Babel требуется еще несколько пакетов, которые устанавливаются так:

yarn add --dev \
  @babel/core \
  @babel/cli \
  @babel/preset-env \
  @babel/preset-typescript \
  @babel/preset-react

babel-core  —  это компилятор, то есть сам основной компонент.

babel-cli позволит использовать компилятор из командной строки.

Последние три пакета  —  это “шаблоны” (настройки) Babel для работы с различными сценариями использования. present-env избавляет нас лишних проблем, позволяя писать современный JS-код и обеспечивая его работоспособность на различных клиентах. preset-typescript и preset-react говорят сами за себя: так как используем мы и TypeScript, и React, то нам понадобятся они оба. 

В завершении нужно сконфигурировать файл babel/config.js, указав компилятору используемые настройки:

// babel.config.js

module.exports = {
    presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
}

TypeScript

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

Сначала установим пакет typescript:

yarn add --dev typescript

Забегая чуть вперед, я предлагаю вам также установить следующие пакеты, если вы собираетесь следовать руководству до конца:

yarn add --dev @types/react @types/react-dom @types/react-redux

В них содержатся объявления типов для модулей, которые мы задействуем в проекте. 

Помимо этого, нам также нужен файл tsconfig.json. Я использую конфигурацию с PostHog, с которой мы работаем в продакшене:

{
    "compilerOptions": {
        "baseUrl": "./src",
        "paths": {
            "types/*": ["./types/*"]
        },
        // https://www.sitepoint.com/react-with-typescript-best-practices/
        "allowJs": true, // Разрешить компиляцию JS-файлов.
        "skipLibCheck": true, // Пропустить проверку во типов всех файлах объявлений.
        "esModuleInterop": true, // Отключить импорт пространств имен.
(import * as fs from "fs") and enables CJS/AMD/UMD style imports (import fs from "fs")
        "allowSyntheticDefaultImports": true, // Разрешить предустановленные импорты из модулей без предустановленного экспорта.
        "strict": true, // Включить все опции строгой проверки типов.
        "forceConsistentCasingInFileNames": true, // Запретить ссылки на один и тот же файл, написанный в другом регистре.
        "module": "esnext", // Указываем модуль генерации кода.
        "moduleResolution": "node", // Разрешение модулей в стиле Node.js
        "resolveJsonModule": true, // Включить модули, импортированные с расширением .json
        "noEmit": true, // Не отправлять вывод (то есть не компилировать код, а только проверить типы).
        "jsx": "react", // Поддержка JSX в файлах .tsx
        "sourceMap": true, // Сгенерировать соответствующий файл .map
        "declaration": true, // Сгенерировать соответствующий файл .d.ts
        "noUnusedLocals": true, // Сообщать об ошибке при неиспользованных локальных элементах (переменных, функциях, импортах).
        "noUnusedParameters": true, // Сообщать об ошибке при неиспользуемых параметрах.
        "experimentalDecorators": true, // Включить экспериментальную поддержку для декораторов ES.
        "noFallthroughCasesInSwitch": true, // Сообщать об ошибке при встрече кейсов fallthrough в инструкциях switch.
        "lib": ["dom", "es2019.array"]
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules/**/*", "staticfiles/**/*", "frontend/dist/**/*"]
}

Можете смело менять приведенную конфигурацию под свои нужды. Тем не менее важно сохранить эти опции:

"noEmit": true, 
"jsx": "react",

"jsx": "react" говорит сама за себя. Что же касается noEmit, то для нее мы устанавливаем true, так как Babel компилирует TS автоматически, и typescript нам нужен только для проверки ошибок (например, при написании кода).

Примечание: комментарии в файлах tsconfig.json разрешены.

Webpack

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

Итого:

yarn add --dev \
    webpack \
    webpack-cli \
    webpack-dev-server \
    style-loader \
    css-loader \
    babel-loader

webpack и webpack-cli следуют тому же принципу, что и Babel. Первый является основным пакетом, а второй позволяет обращаться к нему через командную строку.

webpack-dev-server требуется нам для локальной разработки. Вы заметите, что package.json никогда не ссылается на него из скрипта, но он необходим для запуска webpack serve:

[webpack-cli] For using 'serve' command you need to install: 'webpack-dev-server' package

Последними идут загрузчики, нужные для разных файлов, которые мы будем обрабатывать. ts-loader также присутствует, но поскольку мы используем для компиляции JS-файлов Babel, фактичекски, он нам не понадобится.

В завершении, как и для Babel, нужно настроить файл конфигурации webpack.config.js:

// webpack.config.js
const path = require('path')
const webpack = require('webpack')

module.exports = {
    entry: './src/index.tsx', // точка входа, о которой говорилось ранее.
    mode: 'development',
    module: {
        rules: [
            {
                test: /\.[jt]sx?$/, // сопоставляет файлы .js, .ts, и .tsx 
                loader: 'babel-loader', // использует для указанных типов файлов загрузчик babel-loader (ts-loader не требуется).
                exclude: /node_modules/, 
            },
            {
                test: /\.css$/, // сопоставляет только файлы .css (т.е. не .scss и др.)
                use: ['style-loader', 'css-loader'], 
            },
        ],
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.js'],
    },
    output: {
        filename: 'bundle.js', // выходной бандл
    },
    devServer: {
        contentBase: path.join(__dirname, 'public/'),
        port: 3000,
        publicPath: 'http://localhost:3000/dist/',
        hotOnly: true,
    },
    plugins: [new webpack.HotModuleReplacementPlugin()], // used for hot reloading when developing
    devtool: 'eval-source-map', // создает высококачественные карты кода
}

React

Учитывая, что создаем мы приложение React, нам также нужны и соответствующие пакеты.

Этого будет достаточно:

yarn add react react-dom react-hot-loader

react говорит за себя. react-dom будет использоваться для рендеринга приложения в index.tsx, а react-hot-loader для разработки. Он будет автоматически обновлять файл при внесении изменений.

Kea

В завершении настроим библиотеку управления состоянием. 

Согласно документации нам нужно следующее:

yarn add kea redux react-redux reselect

Здесь мы подумаем наперед и добавим отдельный пакет, который используется при написании логики Kea в TypeScript:

yarn add --dev kea-typegen

package.json

После всей этой настройки нужно добавить в package.json пару скриптов:

...
"scripts": {
   "start": "webpack serve --mode development",
   "typegen": "kea-typegen write ./src"
},
...

start будет запускать сервер, а typegen генерировать типы для файлов логики Kea. 

Наконец, сам код React

Нехило так настроечек? Думаю, что шаблоны определенно заслуживают благодарности, особенно, когда они берут на себя все управление зависимостями и версионирование (react-scripts).

Как бы то ни было, но с настройкой мы закончили, и пора заняться кодом.

Но сначала немного чистого HTML

Первым делом нам нужен файл index.html, который React будет использовать для отрисовки приложения. Это будет наш единственный файл .html. Он также будет единственным в каталоге public/.

Вот мой index.html:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
        <title>React from Scratch</title>
    </head>
    <body>
        <div id="root"></div>
        <noscript> You need to enable JavaScript to access this website. </noscript>
        <script src="../dist/bundle.js"></script>
    </body>
</html>

Здесь происходит несколько действий:

  • Мы настраиваем несколько тегов метаданных, а также заголовок для сайта.
  • Мы указали div root, который будем использовать для отрисовки приложения (по сути, это стартовая точка, с которой React будет динамически генерировать внутренний HTML-файл).
  • Мы добавили сообщение для тех, у кого отключен JavaScript, так как у них наше приложение не заработает.
  • Мы импортировали готовый бандл webpack, который пока еще не сгенерировали.
  • Здесь в одном файле будет содержаться весь создаваемый нами код.

Точка входа

Помните упоминание о точке входа? Что ж, вот мы до нее и добрались. Перейдите в подкаталог src/ и создайте файл index.tsx.

Вот содержимое моего:

import React from 'react'
import ReactDOM from 'react-dom' 
import { Provider } from 'react-redux'
import { getContext, resetContext } from 'kea'
import { App } from './App'

resetContext({
    createStore: {},
    plugins: [],
})

ReactDOM.render(
    <Provider store={getContext().store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

Здесь происходит три основных действия:

  1. Мы настраиваем Kea, которая, подобно Redux, использует Provider , делая хранилище доступным для всех вложенных компонентов (в нашем случае для всего приложения).
  • Вызов resetContext здесь, по сути, не нужен, так как в него ничего не передается. Тем не менее я его оставил, чтобы вы знали, куда добавлять, к примеру, плагины Kea, так как они вам наверняка понадобятся.

2. Мы импортируем и отрисовываем компонент App (который еще не собран).

3. Мы сообщаем React отрисовать приложение, используя div root из index.html в качестве «точки привязки».

Приложение!

Далее также внутри src/ создайте файл App.tsx со следующим содержимым:

import React from 'react'
import { hot } from 'react-hot-loader/root'
import { MyJSComponent } from './components/MyJSComponent'
import { Counter } from './components/Counter'

export const App = hot(_App)
export function _App(): JSX.Element | null {
    return (
        <div>
            <h1>Hello world!</h1>
            <MyJSComponent />
            <Counter />
        </div>
    )
}

Если в этот момент вы просто хотите увидеть приложение в действии, то можете удалить импорты и ссылки на MyJsComponent и Counter, после чего выполнить yarn start. Это запустит сервер, и вы сможете перейти к приложению по адресу localhost:3000, где оно поприветствует вас фразой “Hello world!”.

Эти два дополнительных компонента я включил, чтобы убедиться в следующем:

  1. Возможности написания JavaScript параллельно с TypeScript.
  2. Возможности управления состоянием.
  3. В том, что бандлер без проблем обрабатывает файлы .css (Counter содержит минимальную стилизацию).

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

Написание JS и TS 

Как вы видели в App.tsx, у нас есть TS-файл, спокойно импортирующий JS-файл.

Проблем не возникает, так как в webpack.config.js установлено это правило:

{
    test: /\.[jt]sx?$/,
    loader: 'babel-loader',
    exclude: /node_modules/,
},

Если удалить j из test, то использовать JS-файлы с помощью TS-файлов мы не сможем.

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

Создал я его в новом каталоге components/, заполнив следующим содержимым:

import React from 'react'

export const MyJSComponent = () => (
<h2>Try out the counter below!</h2>
)

Counter

Последним я добавил в этот мини-проект традиционный React-компонент Counter.

Задачей было убедиться в работоспособности настроек Kea и в том, что при этом также работает импорт CSS-файлов.

Сначала я создал в components/ подкаталог Counter. В него я добавил три файла:

1. index.tsx  —  содержит сам компонент:

import React, { useState } from 'react'
import { useValues, useActions } from 'kea'
import { counterLogic } from './counterLogic'
import './style.css'

export const Counter = () => {
    const { count } = useValues(counterLogic)
    const { incrementCounter, decrementCounter, updateCounter } = useActions(counterLogic)
    const [inputValue, setInputValue] = useState(0)
    return (
        <div>
            <h3>{count}</h3>
            <div>
                <button onClick={incrementCounter}>+</button>
                <button onClick={decrementCounter}>-</button>
            </div>
            <br />
            <div>
                <input type="number" value={inputValue} onChange={(e) => setInputValue(Number(e.target.value))} />
                <button onClick={() => updateCounter(inputValue)}>Update Value</button>
            </div>
        </div>
    )
}

Все довольно просто. Кликаем +, и отсчет идет вверх, кликаем -, и он устремляется вниз. Установите любое число, используя ввод, и отсчет также обновится.

Обратите внимание на импорт stype.css.

2. counterLogic.ts  —  размещает в себе логику управления состоянием, используемым компонентом Counter. Я не стану объяснять, как работает Kea, но тут все должно быть понятно само собой:

import { kea } from 'kea'
import { counterLogicType } from './counterLogicType'

export const counterLogic = kea<counterLogicType>({
    actions: {
        incrementCounter: true, // https://kea.js.org/docs/guide/concepts#actions
        decrementCounter: true, // true является сокращением для функции, которая не получает аргументов
        updateCounter: (newValue: number) => ({ newValue }),
    },
    reducers: {
        count: [
            0, // значение по умолчанию
            {
                incrementCounter: (state) => state + 1,
                decrementCounter: (state) => state - 1,
                updateCounter: (_, { newValue }) => newValue, // игнорировать состояние, установить новое значение
            },
        ],
    },
})

3. style.css —  здесь я использовал минимальную стилизацию просто, чтобы убедиться в правильной работоспособности CSS:

h3 {
    color: blue;
}

Вот и все!

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

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Yakko Majuri: Building a Modern React App From Scratch in 2021

Предыдущая статья9 советов, как выделиться среди Java-разработчиков
Следующая статьяОднонаправленный поток данных в пользовательском интерфейсе Android