Front-end course

React. Redux-thunk. Routing

Поток данных.

В целом в React-Redux поток данных несложен:

  • Компонент через connect присоединяется через props к redux, получая actionCreator-ы для отправки в redux, и данные из хранилища.
  • При выполнении диспетчеризации action меняется состояние хранилища.
  • Новое состояние автоматически обновляет все подписанные через connect компоненты. В этом цикле не совсем ясно, куда воткнуть обращения к бэку, при условии, что:

    • Компонент должен остаться независимым и связанным с внешним миром только через connect,
    • Redux - хранилище синхронное, а получение информации с бэка - процесс многоэтапный и асинхронный.
    • Компоненты должны знать о состоянии процесса общения с бэком (состоянии промиса, как минимум).

В таком случае слой отправки-получения данных можно встроить в два места:

  • В actionCreator, перед диспетчеризацией.
  • В том или ином слушателе состояния redux (используя subscribe). Внутри хранилища создается специальная ветвь для хранения информации о желании компонентов получить что-то с back-end.

Первый подход более прост и распространен.

actionCreator

В коде actionCreator должны быть реализованы асинхронные обращения. Если разобрать жизненный путь Promise, то обычный actionCreator будет представлять что-то вроде:

//три состояния
const actionPending = () => ({ type: 'SET_STATUS', status: 'PENDING', payload: null, error: null })
const actionResolved = payload => ({ type: 'SET_STATUS', status: 'RESOLVED', payload, error: null })
const actionRejected = error => ({ type: 'SET_STATUS', status: 'REJECTED', payload: null, error })

//асинхронный многоэтапный экшен
const actionFetch   = () => {
    let promise = fetch("...")
    promise.then(
        data => store.dispatch(actionResolved(data)),
        error => store.dispatch(actionRejected(error))
    )

    return actionPending()
}

Таким образом синхронная функция actionFetch обращается куда надо на сервер, навешивает обработчики результатов промиса в then, и возвращает изначальное состояние actionPending. Когда промис поменяет свое состояние, это будет обработано redux через действия actionResolved и actionRejected.

Редьюсер

Где-то в недрах редьюсеров:

...   
    if (action.type === 'SET_STATUS'){
        return {status: action.status, payload: action.payload, error: action.error}
    }
...

Что в этом подходе неудобно, так это привязка к store.

Встречайте, redux-thunk

Redux-thunk

Redux-thunk предоставляет возможность передавать действие-функцию вместо действия-объекта. Теперь actionCreator может возвращать функции, и dispatch их запустит. Сама же функция может реализовывать любой длительный асинхронный процесс, запуская dispatch для изменения состояния Redux.

import thunk from 'redux-thunk';
import {createStore, combineReducers, applyMiddleware} from 'redux';

const store = createStore(msgStatusReducer, applyMiddleware(thunk)) //вторым параметром идет миддлвар

Теперь функция actionFetch выглядит немного по-другому:

const actionFetch   = () => {
    return dispatch => { //возвращаем функцию. 
        let promise = fetch("...")
        dispatch(actionPending())
        promise.then(
            data => dispatch(actionResolved(data)),
            error => dispatch(actionRejected(error))
        )
    }
}
  • actionCreator возвращает не объект, а функцию;
  • Redux её запускает, передавая свой dispatch;
  • функция инициирует асинхронный процесс и использует простые объектные actionCreator для оповещения хранилища;

async function тоже можно:

function actionFetch(){
    return async function (dispatch){
        dispatch(actionPending())
        try {
            dispatch(actionResolved(await fetch(...)))
        }
        catch (e) {
            dispatch(actionRejected(e))
        }
    }
}

Подход с использованием async function предпочтителен, так как он позволяет линейно описать асинхронный процесс любой сложности. Подобным образом работает Redux-Saga. И если на простых примерах синхронный подход с then мало отличается от асинхронной функции; то при нескольких обращениях зависящих друг от друга, код асинхронной функции будет намного прозрачнее.

SPA && ROUTING

Предыстория

Со времен зарождения современного фронтенда были попытки сделать продвинутый фронт с многостраничным интерфейсом. Технически это не представляет особой сложности, однако нет особого смысла в многостраничном интерфейсе без ссылок на эти страницы. С этим же были проблемы - при любой попытке смены адресной строки браузер упорно переходил на другую страницу, т.е. обращался к серверу и сбрасывал JS, подгружая новые скрипты. Это если страница реально существовала на сервере. Обычно 404.

# - ссылки

При переходе по ссылкам внутри страницы браузер, очевидно, страницу не перезагружает. Этим длительное время пользовались первые SPA типа gmail. На таких сервисах роутинг, аналогичный роутингу в express реализуется после символа # в адресе, таким образом с точки зрения браузера все эти “переходы” по ссылкам происходят внутри одной страницы, чем и пользуется JS, прослушивая события изменения адреса и перерисовывая содержимое страницы.

HTML5 позволяет отлавливать любое изменение адреса, а также управлять историей браузера. Таким образом можно полностью просимулировать работу страниц внутри SPA, как будто бы они загружаются с сервера.

Почему это называется SPA ?

Потому что физически это одна страница, которая просто показывает пользователю фейковые адреса и разный DOM.

Как это устроено

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

React

С точки зрения React каждая страница - это компонент. Роутинг прописывается в декларативной форме в JSX.

Пример:

import { Router, Route, Link } from 'react-router-dom';
import createHistory from "history/createBrowserHistory";

<Router history = {createHistory()}>
    <div>
        <Route path="/chat/:param1/:param2" component = {ChatPage} />
        <Route path="/" component = { MainPage } exact />
    </div>
</Router>
  • Router обрамляет все маршруты и обеспечивает интеграцию с Browser History API
  • Route принимает props:

    • path - шаблон адреса, как в express (обратите внимание на param1 и param2)
    • component - класс компонента.
    • exact - нужен чтобы маршрут срабатывал только при полном совпадении.

Компоненты

Принимают параметры шаблона адреса в props

class ChatPage extends Component {
  render() {
    console.log(this.props)
    return (
        <div className="App">
            {this.props.match.params.param1} <br/>
            {this.props.match.params.param2}
        </div>
    )
  }
}

Link

Компонент Link предназначен для создания ссылок.

<Link to='/'>...go to main</Link>

Switch

Компонент Switch выбирает первый подходящий маршрут, остальные игнорирует:

<Router history = {createHistory()}>
    <div>
        <Switch>
            <Route path="/chat/" component = { ChatPage } />
            <Route path="/" component = { MainPage } exact />
            <Route component = { NotFound } />
        </Switch>
    </div>
</Router>

Redirect

Иногда нужно назначить перенаправление с одного адреса на другой. В таком случае поможет компонент Redirect:

<Router history = {createHistory()}>
    <div>
        <Switch>
            <Route path="/chat/" component = {ChatPage} />
            <Route path="/" component = { MainPage } exact />
            <Redirect from="/main" to="/" />
            <Route component = { NotFound } />
        </Switch>
    </div>
</Router>

При посещении адреса /main будет автоматически происходить переход на / и работать компонент MainPage

Переход на другую страницу из кода

Зачастую после сохранения тех или иных форм нужно перейти на другую страницу, т. е. сменить адрес в адресной строке таким образом, чтобы React Router это обработал согласно конфигурации маршрутов. Это можно сделать с помощью рендеринга компонента Redirect без from. Для этого отправьте в state тот или иной маркер перехода чтобы спровоцировать перерисовку.

history.push

Этот способ проще - используйте history, который передан в Router (создается функцией createHistory)

let history = createHistory()

///...

<Router history={history}>
...
</Router>

...

history.push('/') //переход на главную.

Вложенные маршруты

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

Например:

  • / Главная страница
  • /dashboard Личный кабинет
  • /admin Админка

    • /admin/
    • /admin/dashboard Админский кабинет

В такой структуре логично отделить маршруты общей и административной части сайта.

//App
<Router history = {createHistory()}>
    <div>
        <Switch>
            <Route path="/" component = { MainPage } exact />
            <Route path="/dashboard" component = { DashboardPage } />
            <Route path="/admin" component = { AdminPage } />
            <Route component = { NotFound } />
        </Switch>
    </div>
</Router>
class AdminPage extends Component {
    render(){
        let root = this.props.match.url
        return(
            <div>
                <Route path={`${root}/`} 
                        component={ AdminMainPage }
                    />
                <Route path={`${root}/dashboard`} 
                        component={ AdminDashboardPage }
                    />
            </div>
        )
    }
}

Written by Vadim Goloviychuk