Настройка TypeScript для современных проектов React с использованием Webpack

Опубликовано: 2022-03-10
Краткое резюме ↬ В этой статье представлен Typescript, надстрочный индекс JavaScript, который представляет функцию статического типа для обнаружения распространенных ошибок в кодах разработчиков, что повышает производительность и, следовательно, приводит к надежным корпоративным приложениям. Вы также узнаете, как эффективно настроить TypeScript в проекте React, когда мы создадим приложение для выбора эпизода Money Heist, изучая TypeScript, хуки React, такие как useReducer, useContext и Reach Router.

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

К счастью, нам не нужно ждать, пока технический комитет Ecma 39 введет в JavaScript систему статических типов. Вместо этого мы можем использовать TypeScript.

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

В этом руководстве мы узнаем, что такое TypeScript и как с ним работать в проекте React. К концу мы создадим проект, состоящий из приложения для выбора эпизодов для телешоу Money Heist , используя TypeScript и текущие React-подобные хуки ( useState , useEffect , useReducer , useContext ). Обладая этими знаниями, вы можете экспериментировать с TypeScript в своих проектах.

Эта статья не является введением в TypeScript. Следовательно, мы не будем рассматривать базовый синтаксис TypeScript и JavaScript. Однако вам не нужно быть экспертом ни в одном из этих языков, чтобы следовать этому, потому что мы постараемся следовать принципу KISS (будь проще, глупец).

Еще после прыжка! Продолжить чтение ниже ↓

Что такое TypeScript?

В 2019 году TypeScript занял седьмое место среди самых используемых языков и пятое место среди самых быстрорастущих языков на GitHub. Но что такое TypeScript?

Согласно официальной документации, TypeScript — это надмножество типов JavaScript, которое компилируется в обычный JavaScript. Он разработан и поддерживается Microsoft и сообществом открытого исходного кода.

«Супермножество» в этом контексте означает, что язык содержит все возможности и функциональные возможности JavaScript, а затем и некоторые из них. TypeScript — это типизированный язык сценариев.

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

TypeScript не был создан для изменения JavaScript. Вместо этого он расширяет возможности JavaScript новыми ценными функциями. Любая программа, написанная на простом JavaScript, также будет работать, как и ожидалось, на TypeScript, включая кроссплатформенные мобильные приложения и серверные части на Node.js.

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

Почему TypeScript?

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

Меньше ошибок

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

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

Рефакторинг проще

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

В TypeScript такие вещи часто можно реорганизовать, просто щелкнув команду «Переименовать символ» в вашей интегрированной среде разработки (IDE).

Переименование приложения в expApp (большой предварительный просмотр)

В языке с динамической типизацией, таком как JavaScript, единственный способ одновременного рефакторинга нескольких файлов — это традиционная функция «поиск и замена» с использованием регулярных выражений (RegExp).

В статически типизированном языке, таком как TypeScript, «поиск и замена» больше не нужны. С помощью команд IDE, таких как «Найти все вхождения» и «Переименовать символ», вы можете увидеть все вхождения в приложении данной функции, класса или свойства объектного интерфейса.

TypeScript поможет вам найти все экземпляры бита рефакторинга, переименовать его и предупредит вас об ошибке компиляции, если ваш код имеет какие-либо несоответствия типов после рефакторинга.

TypeScript имеет еще больше преимуществ, чем мы рассмотрели здесь.

Недостатки TypeScript

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

Ложное чувство безопасности

Функция проверки типов в TypeScript часто создает у разработчиков ложное чувство безопасности. Проверка типов действительно предупреждает нас, когда с нашим кодом что-то не так. Однако статические типы не уменьшают общую плотность ошибок.

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

Если вы хотите уменьшить количество ошибок с помощью TypeScript, рассмотрите вместо этого разработку через тестирование.

Сложная система набора текста

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

Однако TypeScript по-прежнему остается JavaScript, поэтому понимание JavaScript важно.

Когда использовать TypeScript?

Я бы посоветовал вам использовать TypeScript в следующих случаях:

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

В последних нескольких разделах мы сбалансировали плюсы и минусы TypeScript. Перейдем к делу дня: настройке TypeScript в современном React-проекте .

Начиная

Есть несколько способов настроить TypeScript в проекте React. В этом уроке мы рассмотрим только два.

Способ 1: создать приложение React + TypeScript

Около двух лет назад команда React выпустила приложение Create React 2.1 с поддержкой TypeScript. Таким образом, вам, возможно, никогда не придется делать тяжелую работу, чтобы внедрить TypeScript в свой проект.

Анонс TypeScript в приложении Create React (большой предварительный просмотр)

Чтобы начать новый проект Create React App, вы можете запустить этот…

 npx create-react-app my-app --folder-name

… или это:

 yarn create react-app my-app --folder-name

Чтобы добавить TypeScript в проект Create React App, сначала установите его и соответствующие @types :

 npm install --save typescript @types/node @types/react @types/react-dom @types/jest

… или:

 yarn add typescript @types/node @types/react @types/react-dom @types/jest

Затем переименуйте файлы (например, index.js в index.tsx ) и перезапустите сервер разработки !

Это было быстро, не так ли?

Способ 2: настроить TypeScript с помощью Webpack

Webpack — это сборщик статических модулей для приложений JavaScript. Он берет весь код из вашего приложения и делает его пригодным для использования в веб-браузере. Модули — это фрагменты кода многократного использования, созданные из JavaScript, node_modules , изображений и стилей CSS вашего приложения, которые упакованы так, чтобы их было легко использовать на вашем веб-сайте.

Создать новый проект

Начнем с создания нового каталога для нашего проекта:

 mkdir react-webpack cd react-webpack

Мы будем использовать npm для инициализации нашего проекта:

 npm init -y

Приведенная выше команда создаст файл package.json с некоторыми значениями по умолчанию. Давайте также добавим некоторые зависимости для webpack, TypeScript и некоторых модулей, специфичных для React.

Установка пакетов

Наконец, нам нужно установить необходимые пакеты. Откройте интерфейс командной строки (CLI) и запустите это:

 #Installing devDependencies npm install --save-dev @types/react @types/react-dom awesome-typescript-loader css-loader html-webpack-plugin mini-css-extract-plugin source-map-loader typescript webpack webpack-cli webpack-dev-server #installing Dependencies npm install react react-dom

Давайте также вручную добавим несколько разных файлов и папок в нашу папку react-webpack :

  1. Добавьте webpack.config.js , чтобы добавить конфигурации, связанные с webpack.
  2. Добавьте tsconfig.json для всех наших конфигураций TypeScript.
  3. Добавьте новый каталог, src .
  4. Создайте новый каталог, components , в папке src .
  5. Наконец, добавьте index.html , App.tsx и index.tsx в папку components .

Структура проекта

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

 ├── package.json ├── package-lock.json ├── tsconfig.json ├── webpack.config.js ├── .gitignore └── src └──components ├── App.tsx ├── index.tsx ├── index.html

Начните добавлять код

Начнем с index.html :

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>React-Webpack Setup</title> </head> <body> <div></div> </body> </html>

Это создаст HTML с пустым div с идентификатором output .

Давайте добавим код в наш компонент React App.tsx :

 import * as React from "react"; export interface HelloWorldProps { userName: string; lang: string; } export const App = (props: HelloWorldProps) => ( <h1> Hi {props.userName} from React! Welcome to {props.lang}! </h1> );

Мы создали объект интерфейса и назвали его HelloWorldProps , где userName и lang имеют string тип.

Мы передали props нашему компоненту App и экспортировали его.

Теперь давайте обновим код в index.tsx :

 import * as React from "react"; import * as ReactDOM from "react-dom"; import { App } from "./App"; ReactDOM.render( <App userName="Beveloper" lang="TypeScript" />, document.getElementById("output") );

Мы только что импортировали компонент App в index.tsx . Когда webpack увидит какой-либо файл с расширением .ts или .tsx , он транспилирует этот файл с помощью библиотеки awesome-typescript-loader.

Конфигурация TypeScript

Затем мы добавим некоторую конфигурацию в tsconfig.json :

 { "compilerOptions": { "jsx": "react", "module": "commonjs", "noImplicitAny": true, "outDir": "./build/", "preserveConstEnums": true, "removeComments": true, "sourceMap": true, "target": "es5" }, "include": [ "src/components/index.tsx" ] }

Давайте также посмотрим на различные параметры, которые мы добавили в tsconfig.json :

  • compilerOptions Представляет различные параметры компилятора.
  • jsx:react Добавляет поддержку JSX в файлы .tsx .
  • lib Добавляет в компиляцию список файлов библиотек (например, использование es2015 позволяет нам использовать синтаксис ECMAScript 6).
  • module Генерирует код модуля.
  • noImplicitAny ошибки для объявлений с подразумеваемым типом any .
  • outDir Представляет выходной каталог.
  • sourceMap Создает файл .map , который может быть очень полезен для отладки приложения.
  • target Представляет целевую версию ECMAScript для переноса нашего кода (мы можем добавить версию в зависимости от конкретных требований нашего браузера).
  • include Используется для указания списка файлов, которые необходимо включить.

Конфигурация веб-пакета

Давайте добавим некоторую конфигурацию веб-пакета в webpack.config.js .

 const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = { entry: "./src/components/index.tsx", target: "web", mode: "development", output: { path: path.resolve(\__dirname, "build"), filename: "bundle.js", }, resolve: { extensions: [".js", ".jsx", ".json", ".ts", ".tsx"], }, module: { rules: [ { test: /\.(ts|tsx)$/, loader: "awesome-typescript-loader", }, { enforce: "pre", test: /\.js$/, loader: "source-map-loader", }, { test: /\.css$/, loader: "css-loader", }, ], }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(\__dirname, "src", "components", "index.html"), }), new MiniCssExtractPlugin({ filename: "./src/yourfile.css", }), ], };

Давайте посмотрим на различные параметры, которые мы добавили в webpack.config.js :

  • entry Указывает точку входа для нашего приложения. Это может быть один файл или массив файлов, которые мы хотим включить в нашу сборку.
  • output Содержит конфигурацию вывода. Приложение смотрит на это, когда пытается вывести связанный код из нашего проекта на диск. Путь представляет выходной каталог для вывода кода, а имя файла представляет собой имя файла для него. Обычно он называется bundle.js .
  • resolve Webpack смотрит на этот атрибут, чтобы решить, объединять или пропускать файл. Таким образом, в нашем проекте вебпак будет рассматривать для сборки файлы с расширениями .js , .jsx , .json , .ts и .tsx .
  • module Мы можем разрешить веб-пакету загружать определенный файл по запросу приложения, используя загрузчики. Он принимает объект правил, который указывает, что:
    • любой файл, заканчивающийся расширением .tsx или .ts , должен загружаться с помощью awesome-typescript-loader ;
    • файлы с расширением .js следует загружать с помощью source-map-loader ;
    • файлы с расширением .css следует загружать с помощью css-loader .
  • plugins У Webpack есть свои ограничения, и он предоставляет плагины для их преодоления и расширения своих возможностей. Например, html-webpack-plugin создает файл шаблона, который отображается в браузере из файла index.html в каталоге ./src/component/index.html .

MiniCssExtractPlugin отображает родительский файл CSS приложения.

Добавление скриптов в package.json

Мы можем добавить различные сценарии для создания приложений React в наш файл package.json :

 "scripts": { "start": "webpack-dev-server --open", "build": "webpack" },

Теперь запустите npm start в CLI. Если все прошло хорошо, вы должны увидеть это:

Выходные данные настройки React-Webpack (большой предварительный просмотр)

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

Создание файлов

Создайте папку src и файл index.tsx . Это будет базовый файл, который отображает React.

Теперь, если мы запустим npm start , он запустит наш сервер и откроет новую вкладку. Запуск npm run build соберет веб-пакет для производства и создаст для нас папку сборки.

Мы увидели, как настроить TypeScript с нуля, используя метод настройки Create React App и webpack.

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

Далее мы рассмотрим, как легко перенести проект React на TypeScript.

Перенос существующего приложения Create React на TypeScript

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

  1. Добавьте TypeScript и типы.
  2. Добавьте tsconfig.json .
  3. Начните с малого.
  4. Переименуйте файлы с расширением .tsx .

1. Добавьте TypeScript в проект

Во-первых, нам нужно добавить TypeScript в наш проект. Предполагая, что ваш проект React был загружен с помощью Create React App, мы можем запустить следующее:

 # Using npm npm install --save typescript @types/node @types/react @types/react-dom @types/jest # Using Yarn yarn add typescript @types/node @types/react @types/react-dom @types/jest

Обратите внимание, что мы еще ничего не изменили в TypeScript. Если мы запустим команду для локального запуска проекта ( npm start или yarn start ), ничего не изменится. Если это так, то отлично! Мы готовы к следующему шагу.

2. Добавьте файл tsconfig.json

Прежде чем воспользоваться преимуществами TypeScript, нам нужно настроить его с помощью файла tsconfig.json . Самый простой способ начать работу — создать каркас с помощью этой команды:

 npx tsc --init

Это дает нам некоторые основы с большим количеством комментариев кода. Теперь замените весь код в tsconfig.json следующим:

 { "compilerOptions": { "jsx": "react", "module": "commonjs", "noImplicitAny": true, "outDir": "./build/", "preserveConstEnums": true, "removeComments": true, "sourceMap": true, "target": "es5" }, "include": [ "./src/**/**/\*" ] }

Конфигурация TypeScript

Давайте также посмотрим на различные параметры, которые мы добавили в tsconfig.json :

  • compilerOptions Представляет различные параметры компилятора.
    • target Преобразует новые конструкции JavaScript в более старую версию, например ECMAScript 5.
    • lib Добавляет в компиляцию список файлов библиотек (например, использование es2015 позволяет нам использовать синтаксис ECMAScript 6).
    • jsx:react Добавляет поддержку JSX в файлы .tsx .
    • lib Добавляет в компиляцию список файлов библиотек (например, использование es2015 позволяет нам использовать синтаксис ECMAScript 6).
    • module Генерирует код модуля.
    • noImplicitAny Используется для создания ошибок для объявлений с подразумеваемым типом any .
    • outDir Представляет выходной каталог.
    • sourceMap Генерирует файл .map , который может быть очень полезен для отладки нашего приложения.
    • include Используется для указания списка файлов, которые необходимо включить.

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

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

3. Начните с простого компонента

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

Чтобы правильно преобразовать это, нам нужно сделать две вещи:

  1. Измените расширение файла на .tsx .
  2. Добавьте аннотацию типа (для чего потребуются некоторые знания TypeScript).

4. Переименуйте расширения файлов в .tsx

В большой кодовой базе может показаться утомительным переименовывать файлы по отдельности.

Переименовать несколько файлов в macOS

Переименование нескольких файлов может быть пустой тратой времени. Вот как вы можете сделать это на Mac. Щелкните правой кнопкой мыши (или Ctrl + щелчок или щелкните двумя пальцами одновременно на трекпаде, если вы используете MacBook) папку, содержащую файлы, которые вы хотите переименовать. Затем нажмите «Показать в Finder». В Finder выберите все файлы, которые вы хотите переименовать. Щелкните правой кнопкой мыши выбранные файлы и выберите «Переименовать X элементов…». Затем вы увидите что-то вроде этого:

Переименование файлов на Mac (большой предварительный просмотр)

Вставьте строку, которую вы хотите найти, и строку, которой вы хотите заменить найденную строку, и нажмите «Переименовать». Сделанный.

Переименовать несколько файлов в Windows

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

Мы рассмотрели, как настроить TypeScript в приложении React. Теперь давайте создадим приложение для выбора эпизодов для Money Heist , используя TypeScript.

Мы не будем рассматривать основные типы TypeScript. Прежде чем продолжить работу с этим учебным пособием, необходимо просмотреть документацию.

Время строить

Чтобы сделать этот процесс менее сложным, мы разобьем его на этапы, что позволит нам создавать приложение отдельными фрагментами. Вот все шаги, которые мы предпримем, чтобы создать средство выбора эпизодов Money Heist :

  • Создание шаблона приложения React.
  • Получить эпизоды.
    • Создайте соответствующие типы и интерфейсы для наших эпизодов в interface.ts .
    • Настройте хранилище для получения эпизодов в store.tsx .
    • Создайте действие для загрузки эпизодов в action.ts .
    • Создайте компонент EpisodeList.tsx , содержащий выбранные эпизоды.
    • Импортируйте компонент EpisodesList на нашу домашнюю страницу, используя React Lazy and Suspense .
  • Добавьте эпизоды.
    • Настройте магазин, чтобы добавить выпуски в store.tsx .
    • Создайте действие для добавления эпизодов в action.ts .
  • Удалить эпизоды.
    • Настройте хранилище для удаления эпизодов в store.tsx .
    • Создайте действие для удаления эпизодов в action.ts .
  • Любимый эпизод.
    • Импортируйте компонент EpisodesList в любимый эпизод.
    • Render EpisodesList внутри любимого эпизода.
  • Использование Reach Router для навигации.

Настроить реакцию

Самый простой способ настроить React — использовать приложение Create React. Create React App — это официально поддерживаемый способ создания одностраничных приложений React. Он предлагает современную настройку сборки без настройки.

Мы будем использовать его для начальной загрузки приложения, которое будем создавать. В CLI выполните следующую команду:

 npx create-react-app react-ts-app && cd react-ts-app

После успешной установки запустите сервер React, запустив npm start .

Стартовая страница React (большой предварительный просмотр)

Понимание интерфейсов и типов в Typescript

Интерфейсы в TypeScript используются, когда нам нужно присвоить типы свойствам объектов. Следовательно, мы будем использовать интерфейсы для определения наших типов.

 interface Employee { name: string, role: string salary: number } const bestEmployee: Employee= { name: 'John Doe', role: 'IOS Developer', salary: '$8500' //notice we are using a string }

При компиляции вышеприведенного кода мы бы увидели такую ​​ошибку: «Типы имущественной salary несовместимы. string типа не может быть назначена number типа ».

Такие ошибки возникают в TypeScript, когда свойству или переменной присваивается тип, отличный от определенного типа. В частности, приведенный выше фрагмент означает, что свойству salary был присвоен string тип, а не number .

Давайте создадим файл interface.ts в нашей папке src . Скопируйте и вставьте в него этот код:

 /** |-------------------------------------------------- | All the interfaces! |-------------------------------------------------- */ export interface IEpisode { airdate: string airstamp: string airtime: string id: number image: { medium: string; original: string } name: string number: number runtime: number season: number summary: string url: string } export interface IState { episodes: Array<IEpisode> favourites: Array<IEpisode> } export interface IAction { type: string payload: Array<IEpisode> | any } export type Dispatch = React.Dispatch<IAction> export type FavAction = ( state: IState, dispatch: Dispatch, episode: IEpisode ) => IAction export interface IEpisodeProps { episodes: Array<IEpisode> store: { state: IState; dispatch: Dispatch } toggleFavAction: FavAction favourites: Array<IEpisode> } export interface IProps { episodes: Array<IEpisode> store: { state: IState; dispatch: Dispatch } toggleFavAction: FavAction favourites: Array<IEpisode> }

Хорошей практикой является добавление «I» к имени интерфейса. Это делает код читабельным. Однако вы можете решить исключить его.

Интерфейс IEpisode

Наш API возвращает набор свойств, таких как airdate выхода в эфир, airstamp эфире, время в airtime , id , image , name , number , runtime , season , summary и url -адрес. Следовательно, мы определили интерфейс IEpisode и установили соответствующие типы данных для свойств объекта.

Интерфейс IState

Наш интерфейс IState имеет свойства episodes и favorites соответственно, а также интерфейс Array<IEpisode> .

действие

Свойства интерфейса IActionpayload и type . Свойство type имеет строковый тип, а полезная нагрузка имеет тип Array | any Array | any .

Обратите внимание, что Array | any Array | any означает массив интерфейса эпизода или любого типа.

Для типа Dispatch задано значение React.Dispatch и интерфейс <IAction> . Обратите внимание, что React.Dispatch — это стандартный тип для функции dispatch в соответствии с кодовой базой @types/react react, а <IAction> — это массив действия интерфейса.

Кроме того, в Visual Studio Code есть средство проверки TypeScript. Таким образом, просто выделяя или наводя курсор на код, он достаточно умен, чтобы предложить соответствующий тип.

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

Получить эпизоды

Создание магазина

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

Для этого мы воспользуемся хуком useReducer . Создайте файл store.tsx в папке src . Скопируйте и вставьте в него следующий код.

 import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} } import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} } import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} } import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }

Ниже приведены шаги, которые мы предприняли для создания магазина:

  • При определении нашего хранилища нам нужен хук useReducer и API createContext из React, поэтому мы его импортировали.
  • Мы импортировали IState и IAction из ./types/interfaces .
  • Мы объявили объект initialState с типом IState и свойствами эпизодов и избранного, которые оба установлены в пустой массив соответственно.
  • Затем мы создали переменную Store , содержащую метод createContext , и ей передается initialState .

Тип метода createContext<IState | any> <IState | any> , что означает, что это может быть тип <IState> или any . Мы увидим, что тип any часто используется в этой статье.

  • Затем мы объявили функцию- reducer и передали state и action в качестве параметров. Функция reducer имеет оператор switch, который проверяет значение action.type . Если значение равно FETCH_DATA , то он возвращает объект, который имеет копию нашего состояния (...state) и состояния эпизода, содержащего полезную нагрузку нашего действия.
  • В операторе switch мы возвращаем состояние по default .

Обратите внимание, что параметры state и action в функции редуктора имеют IState и IAction соответственно. Кроме того, функция reducer имеет тип IState .

  • Наконец, мы объявили функцию StoreProvider . Это даст всем компонентам нашего приложения доступ к хранилищу.
  • Эта функция принимает children элементы в качестве реквизита, а внутри функции StorePrivder мы объявили хук useReducer .
  • Мы деструктурировали state и dispatch .
  • Чтобы сделать наш магазин доступным для всех компонентов, мы передали значение объекта, содержащее state и dispatch .

state , содержащее наши эпизоды и состояние избранного, будет доступно другим компонентам, а dispatch — это функция, которая изменяет состояние.

  • Мы экспортируем Store и StoreProvider , чтобы их можно было использовать в нашем приложении.

Создать Action.ts

Нам нужно будет делать запросы к API, чтобы получить эпизоды, которые будут показаны пользователю. Это будет сделано в файле действий. Создайте файл Action.ts и вставьте следующий код:

 import { Dispatch } from './interface/interfaces' export const fetchDataAction = async (dispatch: Dispatch) => { const URL = 'https://api.tvmaze.com/singlesearch/shows?q=la-casa-de-papel&embed=episodes' const data = await fetch(URL) const dataJSON = await data.json() return dispatch({ type: 'FETCH_DATA', payload: dataJSON.\_embedded.episodes }) }

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

  • Функция fetchDataAction принимает параметры dispatch в качестве параметра.
  • Поскольку наша функция асинхронная, мы будем использовать async и await .
  • Мы создаем переменную ( URL ), которая содержит конечную точку нашего API.
  • У нас есть еще одна переменная с именем data , которая содержит ответ от API.
  • Затем мы сохраняем ответ JSON в dataJSON после того, как получили ответ в формате JSON, вызвав data.json() .
  • Наконец, мы возвращаем функцию отправки, которая имеет свойство type и строку FETCH_DATA . У него также есть payload() . _embedded.episodes — это массив объектов эпизодов из нашей endpoint .

Обратите внимание, что функция fetchDataAction извлекает нашу конечную точку, преобразует ее в объекты JSON и возвращает функцию отправки, которая обновляет состояние, объявленное ранее в Store.

Для экспортируемого типа отправки установлено значение React.Dispatch . Обратите внимание, что React.Dispatch — это стандартный тип для функции отправки в соответствии с кодовой базой @types/react react, а <IAction> — это массив действий интерфейса.

Компонент списка эпизодов

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

В папке components создайте файл EpisodesList.tsx , скопируйте и вставьте в него следующий код:

 import React from 'react' import { IEpisode, IProps } from '../types/interfaces' const EpisodesList = (props: IProps): Array<JSX.Element> => { const { episodes } = props return episodes.map((episode: IEpisode) => { return ( <section key={episode.id} className='episode-box'> <img src={!!episode.image ? episode.image.medium : ''} alt={`Money Heist ${episode.name}`} /> <div>{episode.name}</div> <section style={{ display: 'flex', justifyContent: 'space-between' }}> <div> Season: {episode.season} Number: {episode.number} </div> <button type='button' > Fav </button> </section> </section> ) }) } export default EpisodesList
  • Мы импортируем IEpisode и IProps из interfaces.tsx .
  • Затем мы создаем функцию EpisodesList , которая принимает реквизиты. Реквизиты будут иметь тип IProps , а функция — тип Array<JSX.Element> .

Код Visual Studio предлагает записать тип нашей функции как JSX.Element[] .

Код Visual Studio предлагает тип (большой предварительный просмотр)

Хотя Array<JSX.Element> равен JSX.Element[] , Array<JSX.Element> называется общим идентификатором. Следовательно, общий шаблон будет часто использоваться в этой статье.

  • Внутри функции мы деструктурируем episodes из props , который имеет IEpisode .

Прочтите об общей идентичности. Эти знания понадобятся нам по мере продвижения.

  • Мы вернули реквизиты episodes и сопоставили их, чтобы вернуть несколько HTML-тегов.
  • Первый раздел содержит key , который является episode.id , и className episode-box , который будет создан позже. Мы знаем, что в наших эпизодах есть изображения; следовательно, тег изображения.
  • У изображения есть тернарный оператор, который проверяет наличие episode.image или episode.image.medium . В противном случае мы отображаем пустую строку, если изображение не найдено. Кроме того, мы включили episode.name в div.

В section мы показываем сезон, к которому относится серия, и ее номер. У нас есть кнопка с текстом Fav . Мы экспортировали компонент EpisodesList , чтобы использовать его в нашем приложении.

Компонент домашней страницы

Мы хотим, чтобы домашняя страница инициировала вызов API и отображала эпизоды с помощью созданного нами компонента EpisodesList . Внутри папки components создайте компонент HomePage и скопируйте и вставьте в него следующий код:

 import React, { useContext, useEffect, lazy, Suspense } from 'react' import App from '../App' import { Store } from '../Store' import { IEpisodeProps } from '../types/interfaces' import { fetchDataAction } from '../Actions' const EpisodesList = lazy<any>(() => import('./EpisodesList')) const HomePage = (): JSX.Element => { const { state, dispatch } = useContext(Store) useEffect(() => { state.episodes.length === 0 && fetchDataAction(dispatch) }) const props: IEpisodeProps = { episodes: state.episodes, store: { state, dispatch } } return ( <App> <Suspense fallback={<div>loading...</div>}> <section className='episode-layout'> <EpisodesList {...props} /> </section> </Suspense> </App> ) } export default HomePage
  • Мы импортируем useContext , useEffect , lazy и Suspense из React. Импортированный компонент приложения — это основа, на которой все остальные компоненты должны получить ценность магазина.
  • Мы также импортируем Store , IEpisodeProps и FetchDataAction из соответствующих файлов.
  • Мы импортируем компонент EpisodesList , используя функцию React.lazy , доступную в React 16.6.

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

  • Деструктурируем state и dispatch в качестве реквизита из Store .
  • Амперсанд (&&) в useEffect проверяет, является ли состояние нашего эпизода empty (или равным 0). В противном случае мы возвращаем функцию fetchDataAction .
  • Наконец, мы возвращаем компонент App . Внутри него мы используем оболочку Suspense и устанавливаем fallback элемент div с loading текстом. Это будет отображаться пользователю, пока мы ждем ответа от API.
  • Компонент EpisodesList будет монтироваться, когда данные будут доступны, и данные, которые будут содержать episodes , — это то, что мы распространяем в него.

Настроить файл index.txs

Компонент Homepage должен быть дочерним по отношению к StoreProvider . Нам придется сделать это в файле index . Переименуйте index.js в index.tsx и вставьте следующий код:

 import React from 'react' import ReactDOM from 'react-dom' import './index.css' import { StoreProvider } from './Store' import HomePage from './components/HomePage' ReactDOM.render( <StoreProvider> <HomePage /> </StoreProvider>, document.getElementById('root') )

Мы импортируем StoreProvider , HomePage и index.css из соответствующих файлов. We wrap the HomePage component in our StoreProvider . This makes it possible for the Homepage component to access the store, as we saw in the previous section.

Мы прошли долгий путь. Let's check what the app looks like, without any CSS.

App without CSS (Large preview)

Create Index.css

Delete the code in the index.css file and replace it with this:

 html { font-size: 14px; } body { margin: 0; padding: 0; font-size: 10px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .episode-layout { display: flex; flex-wrap: wrap; min-width: 100vh; } .episode-box { padding: .5rem; } .header { display: flex; justify-content: space-between; background: white; border-bottom: 1px solid black; padding: .5rem; position: sticky; top: 0; }

Our app now has a look and feel. Here's how it looks with CSS.

(Большой превью)

Now we see that our episodes can finally be fetched and displayed, because we've adopted TypeScript all the way. Great, isn't it?

Add Favorite Episodes Feature

Let's add functionality that adds favorite episodes and that links it to a separate page. Let's go back to our Store component and add a few lines of code:

Note that the highlighted code is newly added:

 import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext<IState | any>(initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload }
 case 'ADD_FAV': return { ...state, favourites: [...state.favourites, action.payload] }
 default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return <Store.Provider value={{ state, dispatch }}>{children}</Store.Provider> }

To implement the “Add favorite” feature to our app, the ADD_FAV case is added. It returns an object that holds a copy of our previous state, as well as an array with a copy of the favorite state , with the payload .

We need an action that will be called each time a user clicks on the FAV button. Let's add the highlighted code to index.tx :

 import { IAction, IEpisode, Dispatch } from './types/interfaces'
export const fetchDataAction = async (dispatch: Dispatch) => { const URL = 'https://api.tvmaze.com/singlesearch/shows?q=la-casa-de-papel&embed=episodes' const data = await fetch(URL) const dataJSON = await data.json() return dispatch({ type: 'FETCH_DATA', payload: dataJSON._embedded.episodes }) }
export const toggleFavAction = (dispatch: any, episode: IEpisode | any): IAction => { let dispatchObj = { type: 'ADD_FAV', payload: episode } return dispatch(dispatchObj) }
export const toggleFavAction = (dispatch: any, episode: IEpisode | any): IAction => { let dispatchObj = { type: 'ADD_FAV', payload: episode } return dispatch(dispatchObj) }

We create a toggleFavAction function that takes dispatch and episodes as parameters, and any and IEpisode|any as their respective types, with IAction as our function type. We have an object whose type is ADD_FAV and that has episode as its payload. Lastly, we just return and dispatch the object.

Мы добавим еще несколько фрагментов в EpisodeList.tsx . Скопируйте и вставьте выделенный код:

 import React from 'react' import { IEpisode, IProps } from '../types/interfaces' const EpisodesList = (props: IProps): Array<JSX.Element> => {
 const { episodes, toggleFavAction, favourites, store } = props const { state, dispatch } = store

 return episodes.map((episode: IEpisode) => { return ( <section key={episode.id} className='episode-box'> <img src={!!episode.image ? episode.image.medium : ''} alt={`Money Heist - ${episode.name}`} /> <div>{episode.name}</div> <section style={{ display: 'flex', justifyContent: 'space-between' }}> <div> Seasion: {episode.season} Number: {episode.number} </div> <button type='button'
 onClick={() => toggleFavAction(state, dispatch, episode)} > {favourites.find((fav: IEpisode) => fav.id === episode.id) ? 'Unfav' : 'Fav'}
 </button> </section> </section> ) }) } export default EpisodesList

Мы включаем togglefavaction , favorites и store в качестве реквизита, а также деструктурируем statedispatch из Store. Чтобы выбрать наш любимый эпизод, мы включаем метод toggleFavAction в событие onClick и передаем реквизиты state , dispatch и episode в качестве аргументов функции.

Наконец, мы просматриваем состояние favorite , чтобы проверить, совпадает ли fav.id (идентификатор избранного) с episode.id . Если это так, мы переключаемся между Unfav и Fav . Это помогает пользователю узнать, добавили ли они этот эпизод в избранное или нет.

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

Если вы зашли так далеко, похлопайте себя по спине.

Компонент любимой страницы

В папке components создайте файл FavPage.tsx . Скопируйте и вставьте в него следующий код:

 import React, { lazy, Suspense } from 'react' import App from '../App' import { Store } from '../Store' import { IEpisodeProps } from '../types/interfaces' import { toggleFavAction } from '../Actions' const EpisodesList = lazy<any>(() => import('./EpisodesList')) export default function FavPage(): JSX.Element { const { state, dispatch } = React.useContext(Store) const props: IEpisodeProps = { episodes: state.favourites, store: { state, dispatch }, toggleFavAction, favourites: state.favourites } return ( <App> <Suspense fallback={<div>loading...</div>}> <div className='episode-layout'> <EpisodesList {...props} /> </div> </Suspense> </App> ) }

Чтобы создать логику выбора любимых эпизодов, мы написали небольшой код. Мы импортируем lazy и Suspense из React. Мы также импортируем Store , IEpisodeProps и toggleFavAction из соответствующих файлов.

Мы импортируем наш компонент EpisodesList , используя функцию React.lazy . Наконец, мы возвращаем компонент App . Внутри него мы используем оболочку Suspense и устанавливаем запасной элемент div с загружаемым текстом.

Это работает аналогично компоненту Homepage . Этот компонент получит доступ к магазину, чтобы получить эпизоды, которые пользователь добавил в избранное. Затем список эпизодов передается компоненту EpisodesList .

Давайте добавим еще несколько фрагментов в файл HomePage.tsx .

Включите toggleFavAction из ../Actions . Также включите метод toggleFavAction в качестве реквизита.

 import React, { useContext, useEffect, lazy, Suspense } from 'react' import App from '../App' import { Store } from '../Store' import { IEpisodeProps } from '../types/interfaces'
import { fetchDataAction, toggleFavAction } from '../Actions'
const EpisodesList = lazy<any>(() => import('./EpisodesList')) const HomePage = (): JSX.Element => { const { state, dispatch } = useContext(Store) useEffect(() => { state.episodes.length === 0 && fetchDataAction(dispatch) }) const props: IEpisodeProps = { episodes: state.episodes, store: { state, dispatch },
 toggleFavAction, favourites: state.favourites
 } return ( <App> <Suspense fallback={<div>loading...</div>}> <section className='episode-layout'> <EpisodesList {...props} /> </section> </Suspense> </App> ) } export default HomePage

Наша FavPage должна быть связана, поэтому нам нужна ссылка в нашем заголовке в App.tsx . Для этого мы используем Reach Router, библиотеку, похожую на React Router. Уильям Ле объясняет различия между Reach Router и React Router.

В интерфейсе командной строки запустите npm install @reach/router @types/reach__router . Мы устанавливаем как библиотеку Reach Router, так и типы Reach reach-router .

После успешной установки импортируйте Link из @reach/router .

 import React, { useContext, Fragment } from 'react' import { Store } from './tsx'
import { Link } from '@reach/router'
 const App = ({ children }: { children: JSX.Element }): JSX.Element => {
 const { state } = useContext(Store)
return ( <Fragment> <header className='header'> <div> <h1>Money Heist</h1> <p>Pick your favourite episode</p> </div>
 <div> <Link to='/'>Home</Link> <Link to='/faves'>Favourite(s): {state.favourites.length}</Link> </div>
 </header> {children} </Fragment> ) } export default App

Мы деструктурируем хранилище из useContext . Наконец, у нашего дома будет Link и путь к / , а у нашего любимого есть путь к /faves .

{state.favourites.length} проверяет количество эпизодов в избранном и отображает его.

Наконец, в наш файл index.tsx мы импортируем компоненты FavPage и HomePage соответственно и оборачиваем их в Router .

Скопируйте выделенный код в существующий код:

 import React from 'react' import ReactDOM from 'react-dom' import './index.css' import { StoreProvider } from './Store'
import { Router, RouteComponentProps } from '@reach/router' import HomePage from './components/HomePage' import FavPage from './components/FavPage' const RouterPage = ( props: { pageComponent: JSX.Element } & RouteComponentProps ) => props.pageComponent
ReactDOM.render( <StoreProvider>
 <Router> <RouterPage pageComponent={<HomePage />} path='/' /> <RouterPage pageComponent={<FavPage />} path='/faves' /> </Router>
 </StoreProvider>, document.getElementById('root') )

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

Код «Добавить в избранное» работает (большой предварительный просмотр)

Удалить любимую функциональность

Наконец, мы добавим функцию «Удалить эпизод», чтобы при нажатии кнопки мы переключались между добавлением или удалением любимого эпизода. Мы будем отображать количество добавленных или удаленных серий в шапке.

ХРАНИТЬ

Чтобы создать функционал «Удалить любимый выпуск», мы добавим еще один кейс в наш магазин. Итак, переходим в Store.tsx и добавляем выделенный код:

 import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext<IState | any>(initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } case 'ADD_FAV': return { ...state, favourites: [...state.favourites, action.payload] }
 case 'REMOVE_FAV': return { ...state, favourites: action.payload }
 default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }

Мы добавляем еще один case с именем REMOVE_FAV и возвращаем объект, содержащий копию нашего initialState . Кроме того, состояние favorites содержит полезную нагрузку действия.

ДЕЙСТВИЕ

Скопируйте следующий выделенный код и вставьте его в action.ts :

 import { IAction, IEpisode, IState, Dispatch } from './types/interfaces'
export const fetchDataAction = async (dispatch: Dispatch) => { const URL = 'https://api.tvmaze.com/singlesearch/shows?q=la-casa-de-papel&embed=episodes' const data = await fetch(URL) const dataJSON = await data.json() return dispatch({ type: 'FETCH_DATA', payload: dataJSON.\_embedded.episodes }) } //Add IState withits type
export const toggleFavAction = (state: IState, dispatch: any, episode: IEpisode | any): IAction => { const episodeInFav = state.favourites.includes(episode)
 let dispatchObj = { type: 'ADD_FAV', payload: episode }
 if (episodeInFav) { const favWithoutEpisode = state.favourites.filter( (fav: IEpisode) => fav.id !== episode.id ) dispatchObj = { type: 'REMOVE_FAV', payload: favWithoutEpisode }
 } return dispatch(dispatchObj) }

Мы импортируем интерфейс IState из ./types/interfaces , потому что нам нужно будет передать его в качестве типа свойствам state в функции toggleFavAction .

Переменная episodeInFav создается для проверки наличия эпизода в favorites .

Мы фильтруем состояние избранного, чтобы проверить, не совпадает ли идентификатор избранного с идентификатором эпизода. Таким образом, для dispatchObj переназначается тип REMOVE_FAV и полезная нагрузка favWithoutEpisode .

Давайте предварительно просмотрим результат нашего приложения.

Заключение

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

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

Пожалуйста, поделитесь своими отзывами и опытом работы с TypeScript в разделе комментариев ниже. Я хотел бы увидеть, что вы придумали!

Вспомогательный репозиторий для этой статьи доступен на GitHub.

использованная литература

  1. «Как перенести приложение React на TypeScript», Джо Превайт
  2. «Почему и как использовать TypeScript в вашем приложении React?», Махеш Халдар.