В этом посте мы рассмотрим использование библиотеки Redux с ReactJS. Redux, в сущности, централизует государственное управление. Он предоставляет API и шаблоны для управления состоянием предсказуемым образом. Это приводит к согласованному поведению приложения. Если для одного и того же набора изменений мы каждый раз получаем одно и то же результирующее состояние, изменение предсказуемо.
Работая над этой статьей, мы создаем простое приложение ReactJS, чтобы прокатиться по миру Redux. Сначала это кажется пугающим, но к тому времени, когда мы закончим, вы лучше поймете его преимущества и оцените мотивы, стоящие за ним. Давайте начнем с некоторых основных концепций в первую очередь.
1. Основные понятия
Прежде чем мы начнем создавать наш пример приложения, давайте сначала рассмотрим некоторые термины и их определения. Вот некоторые из ключевых игроков в Redux Universe:
действия
Вы можете думать о действиях как об объекте JavaScript, представляющем действия, которые вызывают изменение в состоянии приложения. Мы называем или скорее отправляем действия в ответ на событие, которое может быть или не быть результатом действий пользователя. Единственное правило с объектами действия заключается в том, что они имеют свойство «тип». Кроме того, структура объекта действия зависит от нас.
Переходники
Редукторы — это функции JavaScript, которые фактически изменяют состояние в ответ на действия. Это в основном чистые функции, поскольку они не изменяют передаваемые в них данные, а используют их для создания нового состояния.
хранить
Это простой объект JavaScript, который содержит состояние всего приложения. Изменения данных, хранящихся в этом объекте, приводят к повторной визуализации представления. Помните, что его данные изменяются редукторами, которые в свою очередь действуют, когда действия запускаются нашим кодом.
2. Базовая структура приложения
Давайте создадим базовую структуру приложения и развернем все необходимые нам пакеты npm. Для начала мы используем пакет create-Reaction-app для генерации базового приложения ReactJS, как показано ниже:
1
|
>npx create-react-app my-app . |
Это создает скелетное приложение ReactJS с именем my-app в текущей папке.
Далее, так как мы будем работать с Redux, мы устанавливаем несколько зависимостей, связанных с ним, среди других.
1
|
> npm install redux react-redux prop-types redux-immutable-state-invariant |
Теперь мы готовы построить наше приложение ReactJS Redux.
3. ReactJS и Redux — пример приложения
Мы создаем простое приложение, которое показывает совпадающие названия стран в качестве пользовательских типов в текстовом поле. Названия стран извлекаются из серверного API и заполняются в неупорядоченном списке под текстовым полем. В нашем штате будет храниться текст, введенный пользователем, и список соответствующих названий стран, извлеченных из API. Форма нашего состояния выглядит следующим образом:
initialState.js
01
02
03
04
05
06
07
08
09
10
|
export default { user: { text: "" }, country: { countries: [], isLoading: false , error: null } }; |
3.1 Настройка API сервера
Мы используем Nodejs и Express для быстрой настройки API на стороне сервера. API возвращает список стран, соответствующих предоставленному тексту. Код нашего серверного API выглядит следующим образом:
index.js
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
const express = require( 'express' ); const app = express(); const bodyParser = require( "body-parser" ); const api = require( './countryController' ); const cors = require( 'cors' ); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(cors()); app.use( '/api' , api()); const Port = process.env.PORT || 2019; app.listen(Port, () => { console.log(`Listening on ${Port}.`); }); |
countryController.js
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
const router = require( 'express' ).Router(); module.exports = () => { router.get( '/country/:code?' , (req, res) => { let code = req.params.code || "" ; if (code) { return res.json(countries.filter(c => c.code.toLowerCase().indexOf(code.toLowerCase()) > -1 || c.name.toLowerCase().indexOf(code.toLowerCase()) > -1)); } return res.json(countries); }); return router; }; const countries = [ { name: 'Afghanistan' , code: 'AF' }, { name: 'Åland Islands' , code: 'AX' }, { name: 'Albania' , code: 'AL' }, { name: 'Algeria' , code: 'DZ' }, { name: 'American Samoa' , code: 'AS' }, { name: 'AndorrA' , code: 'AD' }, { name: 'Angola' , code: 'AO' }, ... |
3.2 Настройка Redux
Redux требует немного подробного стандартного кода, чтобы начать работу. Но не позволяйте этому обескураживать вас. Как только вы поймете, что делает каждая пьеса, это не покажется вам слишком сложным.
Сначала давайте настроим наши действия, помните, что это объекты JavaScript с обязательным свойством типа. Для этого мы создадим три файла, а именно: actionTypes.js, countryActions.js и userActions.js. actionTypes.js хранит строковые константы с именами наших действий. Два других файла содержат так называемые создатели действий. Создатели действий — это в основном функции JavaScript, которые возвращают объекты действий.
actionTypes.js
1
2
3
4
|
export const FETCH_COUNTRIES_BEGIN = "FETCH_COUNTRIES_BEGIN" ; export const FETCH_COUNTRIES_SUCCESS = "FETCH_COUNTRIES_SUCCESS" ; export const FETCH_COUNTRIES_FAILED = "FETCH_COUNTRIES_FAILED" ; export const USER_TEXT = "USER_TEXT" ; |
countryActions.js
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
import { FETCH_COUNTRIES_BEGIN, FETCH_COUNTRIES_FAILED, FETCH_COUNTRIES_SUCCESS } from './actionTypes' ; export function fetch_countries(result) { return function (dispatch) { dispatch(fetch_countries_begin(result)) return window.fetch(`http: //localhost:2019/api/country/${result}`) .then(response => response.json(), (error) => { dispatch(fetch_countries_error(error)); } ).then((res) => dispatch(fetch_countries_success(res))); } } export function fetch_countries_success(result) { return { type: FETCH_COUNTRIES_SUCCESS, result }; } export function fetch_countries_error(result) { return { type: FETCH_COUNTRIES_FAILED, result }; } export function fetch_countries_begin(result) { return { type: FETCH_COUNTRIES_BEGIN, result }; } |
userActions.js
1
2
3
4
5
|
import { USER_TEXT } from './actionTypes' ; export function user_text(result) { return { type: USER_TEXT, result }; } |
Теперь, когда наши действия настроены, давайте настроим наши редукторы дальше. Редукторы — это чистые функции, которые принимают в качестве параметров предыдущее состояние и объект действия и возвращают новый объект состояния. Достаточно просто, мы создаем два отдельных редуктора, по одному для обработки пользовательского ввода и списка стран из API. Но здесь есть ловушка, поскольку редуктор синхронно возвращает объект состояния, как мы обрабатываем вызовы API или, скорее, асинхронные операции.
Стандартный способ сделать это — использовать Redux-thunk. Это промежуточное ПО, улучшающее хранилище, позволяет редукторам возвращать функции или обещания. Позже мы улучшим наш магазин с помощью промежуточного программного обеспечения Thunk, но сейчас мы предполагаем, что это так. Наши редукторы выглядят так:
countryReducer.js
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
import * as type from "../actions/actionTypes" ; import initialState from "./initialState" ; export default function country(state = initialState.country, action) { switch (action.type) { case type.FETCH_COUNTRIES_SUCCESS: return { ...state, countries: action.result, isLoading: false , error: null }; case type.FETCH_COUNTRIES_FAILED: return { ...state, countries: [], isLoading: false , error: action.result }; case type.FETCH_COUNTRIES_BEGIN: return { ...state, countries: [], isLoading: true , error: null }; default : return state; } } |
userTextReducer.js
01
02
03
04
05
06
07
08
09
10
11
|
import * as type from "../actions/actionTypes" ; import initialState from "./initialState" ; export default function user(state = initialState.user, action) { switch (action.type) { case type.USER_TEXT: return { ...state, text: action.result }; default : return state; } } |
index.js
1
2
3
4
5
6
7
8
9
|
import { combineReducers } from "redux" ; import country from './countryReducer' ; import user from './userReducer' ; const rootReducer = combineReducers({ country, user }); export default rootReducer; |
Наконец, пришло время настроить наш магазин. В Redux есть только один центральный магазин, в котором хранится все состояние нашего приложения. Код выглядит следующим образом:
configureStore.js
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
import { createStore, applyMiddleware, compose } from "redux" ; import rootReducer from "./reducers" ; import reduxImmutableStateInvariant from "redux-immutable-state-invariant" ; import thunkMiddleware from "redux-thunk" ; export default function configureStore(initialState) { const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; // add support for Redux dev tools return createStore( rootReducer, initialState, composeEnhancers(applyMiddleware(reduxImmutableStateInvariant(), thunkMiddleware)) ); } |
Теперь, когда мы завершили настройку Redux, давайте двигаться дальше и работать над нашим пользовательским интерфейсом.
4. Создание компонентов пользовательского интерфейса
Сначала мы создадим наши презентационные компоненты, а именно: Input & CountryList. Компонент Input отображает простой тег ввода, чтобы пользователь мог вводить названия стран. Код для этого компонента выглядит следующим образом:
Input.js
01
02
03
04
05
06
07
08
09
10
11
12
13
|
import React from 'react' ; export default function Input(props) { const handleChange = (event) => { props.user_text(event.target.value); props.fetch_countries(event.target.value); } return <input type= "text" style={{ margin: "15px" , width: "225px" }} placeholder= "Type Country Name or Code" onChange={handleChange} value={props.text} />; } |
Далее мы создаем компонент для отображения списка стран из API. Здесь нет ничего необычного, просто отображается неупорядоченный список названий стран. Код для этого компонента выглядит следующим образом:
CountryList.js
01
02
03
04
05
06
07
08
09
10
11
12
|
import React from 'react' ; function CountryList(props) { return <> <ul>{ props.countries && props.countries.map(c => <li key={c.code}>{c.name}</li> )} </ul> </>; } export default CountryList; |
Теперь давайте создадим наш Контейнерный Компонент, чтобы представить пользовательский интерфейс пользователю. Мы называем это HomePage.js, важной частью здесь является подключение этого компонента к Redux с помощью метода connect. Он принимает два аргумента matchStateToProps и matchDispatchToProps. То, что они делают, это связывают состояние приложения с реквизитом и действия с реквизитом. Затем мы передаем их презентационным компонентам для завершения примера. Код для этого компонента HomePage выглядит следующим образом:
HomePage.js
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
import React from 'react' ; import Input from '../Common/Input' ; import CountryList from '../Common/CountryList' ; import * as countryActions from '../../Redux/actions/countryActions' ; import * as userActions from '../../Redux/actions/userActions' ; import { connect } from 'react-redux' ; import { bindActionCreators } from "redux" ; function HomePage(props) { return <><Input user_text={props.actions.user_text} fetch_countries={props.actions.fetch_countries}> </Input> <CountryList countries={props.country.countries}> </CountryList> </>; } const matchStateToProps = (state) => { return { country: state.country, text: state.text } } const matchDispatchToProps = (dispatch) => { return { actions: { fetch_countries: bindActionCreators(countryActions.fetch_countries, dispatch), fetch_countries_begin: bindActionCreators(countryActions.fetch_countries_begin, dispatch), fetch_countries_success: bindActionCreators(countryActions.fetch_countries_success, dispatch), fetch_countries_error: bindActionCreators(countryActions.fetch_countries_error, dispatch), user_text: bindActionCreators(userActions.user_text, dispatch) } } } export default connect(matchStateToProps, matchDispatchToProps)(HomePage); |
Теперь мы закончили с частью кода нашего приложения. В следующем разделе мы рассмотрим, как запустить приложение, и увидим приложение в действии.
5. Запуск приложения
Нам нужно внести некоторые изменения в нашу команду запуска в package.json. Изменения обеспечивают параллельный запуск как серверного API, так и клиентского кода. Для этого есть утилиты, но для этого примера я просто использовал команду start. Давайте изменим команду запуска, чтобы она выглядела так:
package.json
1
2
3
|
... "start" : "start react-scripts start && start node api/index.js" , ... |
Теперь при запуске команды npm start запускается наше приложение, и результат выглядит следующим образом:
6. Загрузите исходный код
Это был учебник по ReacJS и Redux