Настройка TypeScript для современных проектов React с использованием Webpack
Опубликовано: 2022-03-10В нашу эпоху разработки программного обеспечения 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).
В языке с динамической типизацией, таком как 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 в свой проект.
Чтобы начать новый проект 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
:
- Добавьте
webpack.config.js
, чтобы добавить конфигурации, связанные с webpack. - Добавьте
tsconfig.json
для всех наших конфигураций TypeScript. - Добавьте новый каталог,
src
. - Создайте новый каталог,
components
, в папкеsrc
. - Наконец, добавьте
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. Если все прошло хорошо, вы должны увидеть это:
Если у вас есть навыки работы с веб-пакетами, клонируйте репозиторий для этой установки и используйте его в своих проектах.
Создание файлов
Создайте папку 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
Чтобы сделать этот процесс более управляемым, мы разобьем его на этапы, что позволит нам выполнять миграцию отдельными фрагментами. Вот шаги, которые мы предпримем для переноса нашего проекта:
- Добавьте TypeScript и типы.
- Добавьте
tsconfig.json
. - Начните с малого.
- Переименуйте файлы с расширением
.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. Идите по одному файлу за раз в своем собственном темпе. Делайте то, что имеет смысл для вас и вашей команды. Не пытайтесь справиться со всем сразу.
Чтобы правильно преобразовать это, нам нужно сделать две вещи:
- Измените расширение файла на
.tsx
. - Добавьте аннотацию типа (для чего потребуются некоторые знания TypeScript).
4. Переименуйте расширения файлов в .tsx
В большой кодовой базе может показаться утомительным переименовывать файлы по отдельности.
Переименовать несколько файлов в macOS
Переименование нескольких файлов может быть пустой тратой времени. Вот как вы можете сделать это на Mac. Щелкните правой кнопкой мыши (или Ctrl
+ щелчок или щелкните двумя пальцами одновременно на трекпаде, если вы используете MacBook) папку, содержащую файлы, которые вы хотите переименовать. Затем нажмите «Показать в Finder». В Finder выберите все файлы, которые вы хотите переименовать. Щелкните правой кнопкой мыши выбранные файлы и выберите «Переименовать X элементов…». Затем вы увидите что-то вроде этого:
Вставьте строку, которую вы хотите найти, и строку, которой вы хотите заменить найденную строку, и нажмите «Переименовать». Сделанный.
Переименовать несколько файлов в 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
.
Понимание интерфейсов и типов в 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>
.
действие
Свойства интерфейса IAction
— payload
и 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
и APIcreateContext
из 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[]
.
Хотя 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.
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
в качестве реквизита, а также деструктурируем state
— dispatch
из 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.
использованная литература
- «Как перенести приложение React на TypeScript», Джо Превайт
- «Почему и как использовать TypeScript в вашем приложении React?», Махеш Халдар.