React - обработка исключений

Внимание

Данный материал является частью цикла статей «Создаем приложение на React с использованием Redux». Не забудьте посмотреть другие статьи по этой теме :-)

  1. React - Настройка проекта и подготовка компонентов
  2. React - навигация и маршрутизация
  3. React - использование Redux в приложении React
  4. React – Lazy Loading и компоненты высшего порядка (HOC)
  5. React - обработка исключений
  6. React - создание динамических форм и модальных компонентов
  7. React - валидация формы и обработка POST запроса
  8. React - обработка запроса PUT для редактирования данных
  9. React - обработка запроса на удаление данных

Создание компонента для обработки 500-x ошибок

Давайте создадим новую папку InternalServer внутри папки ErrorPages, а в ней создадим два новых файла и назовем их InternalServer.js и InternalServer.css:

Мы собираемся изменить файл InternalServer.js:

import React from 'react';
import './InternalServer.css';

const internalServer = (props) => {
    return (
        <p className={'internalServer'}>{"500 SERVER ERROR, CONTACT ADMINISTRATOR!"}</p>
    )
}

export default internalServer;

Затем изменим файл InternalServer.css:

.internalServer{
    font-weight: bold;
    font-size: 50px;
    text-align: center;
    color: #c72d2d;
}

Далее, давайте изменим файл App.js:

import InternalServer from '../components/ErrorPages/InternalServer/InternalServer';
<Route path="/" exact component={Home} />
<Route path="/owner-list" component={AsyncOwnerList} />
<Route path="/500" component={InternalServer} />
<Route path="*" component={NotFound} />

Превосходно.

Компонент для информации о 500-x ошибок готов, и мы можем продолжить.

Реализация Redux для обработки ошибок

Как и в случае с репозиторием в этом приложении, мы собираемся создать еще один экземпляр redux для обработки ошибок в одном месте нашего приложения. Мы уже немного знакомы с redux, поэтому эту часть будет довольно легко реализовать. Еще одна вещь заключается в том, что после создания нового файла-редьюсера у нас будет два файла-редьюсера, и, следовательно, нам нужно выполнить другой процесс регистрации внутри файла Index.js.

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

Давайте продолжим, изменив файл ActionTypes.js, добавив три дополнительных типа действий:

export const HTTP_404_ERROR = 'HTTP_404_ERROR';
export const HTTP_500_ERROR = 'HTTP_500_ERROR';
export const HTTP_OTHER_ERROR = 'HTTP_OTHER_ERROR';

Внутри папки actions находится файл repositoryActions.js. Теперь внутри той же папки нам нужно добавить еще один файл действия errorHandlerActions.js. Итак, давайте изменим этот файл:

import * as actionTypes from './actionTypes';

const execute404Handler = (props) => {
    return {
        type: actionTypes.HTTP_404_ERROR,
        props: props
    }
}

const execute500Handler = (props) => {
    return {
        type: actionTypes.HTTP_500_ERROR,
        props: props
    }
}

const executeOtherErrorHandler = (error) => {
    return {
        type: actionTypes.HTTP_OTHER_ERROR,
        error: error
    }
}

export const handleHTTPError = (error, props) => {
    if (error.response.status === 404) {
        return execute404Handler(props);
    }
    else if (error.response.status === 500) {
        return execute500Handler(props);
    }
    else {
        return executeOtherErrorHandler(error);
    }
}

В приведенном выше коде мы экспортируем действие handleHTTPError, в котором мы проверяем код состояния ошибки и выполняем соответствующую функцию. То же самое мы сделали с файлом repositoryActions.js.

Внутри файла repositoryActions.js нам нужно импортировать этот файл errorHandlerActions.js:

import * as errorHandlerActions from './errorHandlerActions';

И чтобы заменить все комментарии в блоке catch каждой функции:

dispatch(errorHandlerActions.handleHTTPError(error, props));

Давайте продолжим, создав новый файл reducer errorHandlerReducer.js внутри папки reducer.

Сейчас у нас есть такая структура папок:

Файл errorHandlerReducer.js должен выглядеть так:

import * as actionTypes from '../actions/actionTypes';

const initialState = {
    showErrorModal: false,
    errorMessage: ''
}

const execute404 = (state, action) => {
    action.props.history.push('/404');
    return { ...state };
}

const execute500 = (state, action) => {
    action.props.history.push('/500');
    return { ...state };
}

const executeOtherError = (state, action) => {
    return {
        ...state,
        showErrorModal: true,
        errorMessage: action.error.response.data
    };
}

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case actionTypes.HTTP_404_ERROR:
            return execute404(state, action);
        case actionTypes.HTTP_500_ERROR:
            return execute500(state, action);
        case actionTypes.HTTP_OTHER_ERROR:
            return executeOtherError(state, action);
        default:
            return state;
    }
}

export default reducer;

Эта логика тоже знакома. Мы создаем объект состояния (initialState), а затем функцию reducer, которая принимает параметры state и action. Функция reducer обновит state на основе свойства type, отправленного из файла errorHandlerActions.

Регистрация нескольких редьюсеров

Чтобы завершить настройку Redux, давайте изменим файл Index.js, чтобы также зарегистрировать этот редьюсер:

import repositoryReducer from './store/reducers/repositoryReducer';
import errorHandlerReducer from './store/reducers/errorHandlerReducer';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';

const rootReducers = combineReducers({
    repository: repositoryReducer,
    errorHandler: errorHandlerReducer
})

const store = createStore(rootReducers, applyMiddleware(thunk));

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

Мы импортируем файл errorHandlerReducer и функцию combineReducers. Затем с помощью функции combineReducers мы создаем объект rootReducers, который содержит все наши редьюсеры. Наконец, мы просто передаем этот объект в создание магазина.

Модификация компонента OwnerList

Нам нужно сделать еще одну вещь. В компоненте OwnerList нам нужно изменить способ настройки свойства data из файла repositoryReducer. Давайте изменим функцию mapStateToProps:

const mapStateToProps = (state) => {
    return {
        data: state.repository.data
    }
}

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

Мы можем попробовать нашу обработку ошибок, изменив код в методе сервера GetAllOwners. В качестве первой строки кода мы можем добавить return NotFound() или return StatusCode(500, “Some message”), и мы обязательно будем перенаправлены на правильную страницу ошибки.

Реализация компонента OwnerDetails

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

Мы и сделаем именно это.

Родительский компонент будет компонентом OwnerDetails, а дочерний компонент будет компонентом OwnersAccounts. Итак, давайте начнем с создания дочернего компонента.

Для компонента OwnersAccounts мы собираемся создать такую ​​структуру:

Давайте изменим файл OwnersAccounts.js:

import React from 'react';
import { Row, Col, Table } from 'react-bootstrap';
import Moment from 'react-moment';

const ownersAccounts = (props) => {
    let accounts = null;
    if (props.accounts) {
        accounts = props.accounts.map(account => {
            return (
                <tr key={account.id}>
                    <td>{account.accountType}</td>
                    <td><Moment format="DD/MM/YYYY">{account.dateCreated}</Moment></td>
                </tr>
            );
        })
    }
    return (
        <Row>
            <Col md={12}>
                <Table responsive striped>
                    <thead>
                        <tr>
                            <th>Account type</th>
                            <th>Date created</th>
                        </tr>
                    </thead>
                    <tbody>
                        {accounts}
                    </tbody>
                </Table>
            </Col>
        </Row>
    )
}

export default ownersAccounts;

Давайте продолжим, создав структуру папок для компонента OwnerDetails:

Затем мы собираемся импортировать все необходимые файлы в компонент OwnerDetails:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Well, Row, Col } from 'react-bootstrap';
import * as repositoryActions from '../../../store/actions/repositoryActions';
import Moment from 'react-moment';
import OwnersAccounts from '../../../components/OwnerComponents/OwnersAccounts/OwnersAccounts';
import Aux from '../../../hoc/Auxiliary/Auxiliary';

После этого добавим реализацию компонента:

class OwnerDetails extends Component {
    render() {
       const owner = this.props.data;

        return (
            <Aux>
                <Well>
                    <Row>
                        <Col md={3}>
                            <strong>Owner name:</strong>
                        </Col>
                        <Col md={3}>
                            {owner.name}
                        </Col>
                    </Row>
                    <Row>
                        <Col md={3}>
                            <strong>Date of birth:</strong>
                        </Col>
                        <Col md={3}>
                            <Moment format="DD/MM/YYYY">{owner.dateOfBirth}</Moment>
                        </Col>
                    </Row>
                    {this.renderTypeOfUserConditionally(owner)}
                </Well>
                <OwnersAccounts accounts={owner.accounts} />
            </Aux>
        )
    }
}

export default OwnerDetails;

В приведенном выше коде мы используем оператор this.props.data, но еще не реализовали редьюсер. Мы сделаем это через минуту. Обратите внимание на вызов функции renderTypeOfUserConditionally. В этой функции мы условно обрабатываем данные владельца и возвращаем код JSX для отображения. Мы собираемся реализовать эту функцию через минуту. Ниже данных владельца мы отображаем все аккаунты, связанные с владельцем.

Условный рендеринг

Чтобы реализовать renderTypeOfUserConditionally, нам нужно добавить следующий код над функцией render, но все еще внутри класса:

renderTypeOfUserConditionally = (owner) => {
    let typeOfUser = null;

    if (owner.accounts && owner.accounts.length <= 2) {
        typeOfUser = (
            <Row>
                <Col md={3}>
                    <strong>Type of user:</strong>
                </Col>
                <Col md={3}>
                    <span className={'text-success'}>Beginner user.</span>
                </Col>
            </Row>
        );
    }
    else {
        typeOfUser = (
            <Row>
                <Col md={3}>
                    <strong>Type of user:</strong>
                </Col>
                <Col md={3}>
                    <span className={'text-info'}>Advanced user.</span>
                </Col>
            </Row>
        );
    }

    return typeOfUser;
}

Подключение к Redux

Наконец, давайте подключим этот компонент к файлу редьюсера, чтобы получить данные владельца под закрывающим тегом функции рендеринга:

const mapStateToProps = (state) => {
    return {
        data: state.repository.data
    }
}

const mapDispatchToProps = (dispatch) => {
    return {
        onGetData: (url, props) => dispatch(repositoryActions.getData(url, props))
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(OwnerDetails);

И над функцией renderTypeOfUserConditionally добавьте этот хук жизненного цикла:

componentDidMount = () => {
    let id = this.props.match.params.id;
    let url = '/api/owner/' + id + '/account';
    this.props.onGetData(url, { ...this.props })
}

Мы извлекаем id из URL-адреса, вызывая оператор match.params.id из объекта props. Затем мы просто создаем наш URL-адрес и вызываем свойство onGetData для получения данных с сервера.

Прежде чем проверять наш результат, мы должны добавить маршрут для этого нового компонента в файл App.js, прямо под маршрутом owner-list:

<Route path="/ownerDetails/:id" component={OwnerDetails} />

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

Кроме того, давайте проверим результат для опытного пользователя:

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

Заключение

На данный момент мы почти уверены, что вы поняли как работает Redux. Как мы уже говорили в предыдущих статьях, после некоторой практики вы сможете легко применять Redux, используя его собственный шаблон реализации. Мы использовали его для отправки HTTP-запросов и обработки HTTP-ошибок, а теперь вы можете использовать его для других задач, которые вы хотели бы решить.

Прочитав этот пост, вы узнали:

  • Как реализовать логику обработки ошибок с помощью рабочего процесса Redux.
  • Как объединить редьюсеры в один объект
  • Как получить данные для подробного представления.

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

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