Если, как и я, вы читали документы Redux, смотрели видео Дэна, прошли курс Уэса и все еще не совсем понимали, как использовать Redux, надеюсь, это поможет.

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

В этом руководстве предполагается, что вы имеете базовое представление о React и ES6 / 2015, но, надеюсь, вам будет достаточно легко следовать ему в любом случае.

Способ без Redux

Начнем с создания компонента React в components/ItemList.js для получения и отображения списка элементов.

Закладываем фундамент

Сначала мы настроим статический компонент с state, который содержит различные items для вывода, и 2 логических состояния, чтобы отображать что-то другое при загрузке или с ошибками соответственно.

import React, { Component } from 'react';
class ItemList extends Component {
    constructor() {
        super();
        this.state = {
            items: [
                {
                    id: 1,
                    label: 'List item 1'
                },
                {
                    id: 2,
                    label: 'List item 2'
                },
                {
                    id: 3,
                    label: 'List item 3'
                },
                {
                    id: 4,
                    label: 'List item 4'
                }
            ],
            hasErrored: false,
            isLoading: false
        };
    }
    render() {
        if (this.state.hasErrored) {
            return <p>Sorry! There was an error loading the items</p>;
        }
        if (this.state.isLoading) {
            return <p>Loading…</p>;
        }
        return (
            <ul>
                {this.state.items.map((item) => (
                    <li key={item.id}>
                        {item.label}
                    </li>
                ))}
            </ul>
        );
    }
}
export default ItemList;

Может показаться, что это не так уж много, но это хорошее начало.

При рендеринге компонент должен выводить 4 элемента списка, но если вы установите isLoading или hasErrored на true, вместо этого будет выведен соответствующий <p></p>.

Делаем это динамичным

Жесткое кодирование элементов не делает компонент очень полезным, поэтому давайте возьмем items из JSON API, который также позволит нам установить isLoading и hasErrored по мере необходимости.

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

Чтобы получить элементы, мы будем использовать метко названный Fetch API. Fetch значительно упрощает выполнение запросов, чем классический XMLHttpRequest, и возвращает обещание разрешенного ответа (что важно для Thunk). Fetch доступен не во всех браузерах, поэтому вам нужно добавить его в качестве зависимости в свой проект с помощью:

npm install whatwg-fetch --save

Преобразование на самом деле довольно простое.

  • Сначала мы установим наш начальный items на пустой массив []
  • Теперь мы добавим метод для получения данных и установим состояния загрузки и ошибки:
fetchData(url) {
    this.setState({ isLoading: true });
    fetch(url)
        .then((response) => {
            if (!response.ok) {
                throw Error(response.statusText);
            }
            this.setState({ isLoading: false });
            return response;
        })
        .then((response) => response.json())
        .then((items) => this.setState({ items })) // ES6 property value shorthand for { items: items }
        .catch(() => this.setState({ hasErrored: true }));
}
  • Затем мы назовем его, когда компонент монтируется:
componentDidMount() {
  this.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
}

Остается (без изменений строки опущены):

class ItemList extends Component {
    constructor() {
        this.state = {
            items: [],
        };
    }
    fetchData(url) {
        this.setState({ isLoading: true });
        fetch(url)
            .then((response) => {
                if (!response.ok) {
                    throw Error(response.statusText);
                }
                this.setState({ isLoading: false });
                return response;
            })
            .then((response) => response.json())
            .then((items) => this.setState({ items }))
            .catch(() => this.setState({ hasErrored: true }));
    }
    componentDidMount() {
        this.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
    }
    render() {
    }
}

Вот и все. Теперь ваш компонент получает items из конечной точки REST! Вы должны надеяться, что перед 4 элементами списка ненадолго появится "Loading…". Если вы передадите неработающий URL на fetchData, вы должны увидеть наше сообщение об ошибке.

Однако на самом деле компонент не должен включать логику для получения данных, и данные не должны храниться в состоянии компонента, поэтому здесь на помощь приходит Redux.

Переход на Redux

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

npm install redux react-redux redux-thunk --save

Понимание Redux

Есть несколько основных принципов Redux, которые нам необходимо понять:

  1. Существует 1 глобальный объект состояния, который управляет состоянием всего вашего приложения. В этом примере он будет вести себя идентично нашему начальному состоянию компонента. Это единственный источник истины.
  2. Единственный способ изменить состояние - создать действие, которое представляет собой объект, описывающий, что следует изменить. Создатели действий - это функции, которым dispatch предписано производить изменение - все, что они делают, это return действие.
  3. Когда действие dispatched, редуктор - это функция, которая фактически изменяет состояние, соответствующее этому действию, или возвращает существующее состояние, если действие не применимо к этому редуктору.
  4. Редукторы - это «чистые функции». Они не должны иметь побочных эффектов или изменять состояние - они должны возвращать измененную копию.
  5. Отдельные редукторы объединяются в один rootReducer для создания дискретных свойств состояния.
  6. Магазин - это то, что объединяет все вместе: оно представляет состояние с помощью rootReducer, любого промежуточного программного обеспечения (в нашем случае Thunk) и позволяет вам фактически dispatch действия.
  7. Для использования Redux в React компонент <Provider /> обертывает все приложение и передает storedown всем дочерним элементам.

Все это должно стать яснее, когда мы начнем преобразовывать наше приложение для использования Redux.

Проектирование нашего государства

Из уже проделанной работы мы знаем, что у нашего состояния должно быть 3 свойства: items, hasErrored и isLoading, чтобы это приложение работало должным образом при любых обстоятельствах, что соответствует необходимости 3 уникальных действий.

Теперь вот почему Action Creators отличаются от Actions и не обязательно имеют отношение 1: 1: нам нужен четвертый создатель действия, который вызывает наши 3 других действия (создателей) в зависимости от статуса получения данных. Этот четвертый создатель действия почти идентичен нашему первоначальному fetchData() методу, но вместо того, чтобы напрямую устанавливать состояние с помощью this.setState({ isLoading: true }), мы dispatch действие, которое будет делать то же самое: dispatch(isLoading(true)).

Создание наших действий

Давайте создадим actions/items.js файл, в котором будут храниться наши создатели действий. Начнем с трех простых действий.

export function itemsHasErrored(bool) {
    return {
        type: 'ITEMS_HAS_ERRORED',
        hasErrored: bool
    };
}
export function itemsIsLoading(bool) {
    return {
        type: 'ITEMS_IS_LOADING',
        isLoading: bool
    };
}
export function itemsFetchDataSuccess(items) {
    return {
        type: 'ITEMS_FETCH_DATA_SUCCESS',
        items
    };
}

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

Первые 2 создателя действий принимают bool (_34 _ / _ 35_) в качестве аргумента и возвращают объект со значимым type и bool, присвоенным соответствующему свойству.

Третий, itemsFetchDataSuccess(), будет вызываться, когда данные будут успешно извлечены, и данные будут переданы ему как items. С помощью магии сокращений значений свойств ES6 мы вернем объект со свойством с именем items, значение которого будет массивом items;

Примечание: значение, которое вы используете для type, и имя другого возвращаемого свойства важны, потому что вы будете повторно использовать их в своих редукторах

Теперь, когда у нас есть 3 действия, которые будут представлять наше состояние, мы преобразуем fetchDataметод нашего исходного компонента в itemsFetchData() средство создания действия.

По умолчанию создатели действий Redux не поддерживают асинхронные действия, такие как выборка данных, поэтому здесь мы используем Redux Thunk. Thunk позволяет вам писать создателей действий, которые возвращают функцию вместо действия. Внутренняя функция может получать методы хранения dispatch и getState в качестве параметров, но мы просто будем использовать dispatch.

Самый простой пример - это запуск itemsHasErrored() вручную через 5 секунд.

export function errorAfterFiveSeconds() {
    // We return a function instead of an action object
    return (dispatch) => {
        setTimeout(() => {
            // This function is able to dispatch other action creators
            dispatch(itemsHasErrored(true));
        }, 5000);
    };
}

Теперь мы знаем, что такое преобразователь, и можем написать itemsFetchData().

export function itemsFetchData(url) {
    return (dispatch) => {
        dispatch(itemsIsLoading(true));
        fetch(url)
            .then((response) => {
                if (!response.ok) {
                    throw Error(response.statusText);
                }
                dispatch(itemsIsLoading(false));
                return response;
            })
            .then((response) => response.json())
            .then((items) => dispatch(itemsFetchDataSuccess(items)))
            .catch(() => dispatch(itemsHasErrored(true)));
    };
}

Создание наших редукторов

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

Примечание. В Redux все редукторы вызываются независимо от действия, поэтому внутри каждого из них вы должны вернуть исходный state, если действие неприменимо.

Каждый редуктор принимает 2 параметра: предыдущее состояние (state) и объект action. Мы также можем использовать функцию ES6, называемую параметрами по умолчанию, чтобы установить начальный state по умолчанию.

Внутри каждого редуктора мы используем оператор switch, чтобы определить совпадение action.type. Хотя это может показаться ненужным в этих простых редукторах, ваши редукторы теоретически могут иметь много условий, поэтому _56 _ / _ 57 _ / _ 58_ быстро испортятся.

Если action.type совпадает, мы возвращаем соответствующее свойство action. Как упоминалось ранее, type и action[propertyName] - это то, что было определено в ваших создателях действий.

Хорошо, зная это, давайте создадим наши редукторы элементов в reducers/items.js.

export function itemsHasErrored(state = false, action) {
    switch (action.type) {
        case 'ITEMS_HAS_ERRORED':
            return action.hasErrored;
        default:
            return state;
    }
}
export function itemsIsLoading(state = false, action) {
    switch (action.type) {
        case 'ITEMS_IS_LOADING':
            return action.isLoading;
        default:
            return state;
    }
}
export function items(state = [], action) {
    switch (action.type) {
        case 'ITEMS_FETCH_DATA_SUCCESS':
            return action.items;
        default:
            return state;
    }
}

Обратите внимание, как каждый редуктор назван в честь свойства состояния результирующего хранилища, при этом action.type не обязательно должно соответствовать. Надеемся, что первые два редуктора имеют полный смысл, но последний, items(), немного отличается.

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

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

Создав отдельные редукторы, нам нужно объединить их в rootReducer, чтобы создать единый объект.

Создайте новый файл на reducers/index.js.

import { combineReducers } from 'redux';
import { items, itemsHasErrored, itemsIsLoading } from './items';
export default combineReducers({
    items,
    itemsHasErrored,
    itemsIsLoading
});

Мы импортируем все редукторы из items.js и экспортируем их с combineReducers() Redux. Поскольку имена наших редукторов идентичны тем, что мы хотим использовать для имен свойств магазина, мы можем использовать сокращение ES6.

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

import { combineReducers } from 'redux';
import { items, itemsHasErrored, itemsIsLoading } from './items';
import { posts, postsHasErrored, postsIsLoading } from './posts';
export default combineReducers({
    items,
    itemsHasErrored,
    itemsIsLoading,
    posts,
    postsHasErrored,
    postsIsLoading
});

В качестве альтернативы вы можете использовать псевдонимы для методов на import, но я предпочитаю единообразие по всем направлениям.

Настройте магазин и предоставьте его своему приложению

Это довольно просто. Давайте создадим store/configureStore.js с помощью:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
    return createStore(
        rootReducer,
        initialState,
        applyMiddleware(thunk)
    );
}

Теперь измените index.js нашего приложения, чтобы включить <Provider />, configureStore, настройте store и оберните наше приложение (<ItemList />), чтобы передать store вниз как props:

import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './store/configureStore';
import ItemList from './components/ItemList';
const store = configureStore(); // You can also pass in an initialState here
render(
    <Provider store={store}>
        <ItemList />
    </Provider>,
    document.getElementById('app')
);

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

Преобразование нашего компонента для использования хранилища и методов Redux

Вернемся к components/ItemList.js.

Вверху файла import то, что нам нужно:

import { connect } from 'react-redux';
import { itemsFetchData } from '../actions/items';

connect - это то, что позволяет нам подключить компонент к хранилищу Redux, а itemsFetchData - это создатель действий, который мы написали ранее. Нам нужно импортировать только этого одного создателя действий, поскольку он обрабатывает dispatch другие действия.

После определения class нашего компонента мы собираемся сопоставить состояние Redux и отправку нашего создателя действий на свойства.

Мы создаем функцию, которая принимает state, а затем возвращает объект props. В таком простом компоненте, как этот, я удаляю префикс для свойств _91 _ / _ 92_, поскольку очевидно, что они связаны с items.

const mapStateToProps = (state) => {
    return {
        items: state.items,
        hasErrored: state.itemsHasErrored,
        isLoading: state.itemsIsLoading
    };
};

И затем нам нужна другая функция, чтобы иметь возможность dispatch наш itemsFetchData() создатель действий с опорой.

const mapDispatchToProps = (dispatch) => {
    return {
        fetchData: (url) => dispatch(itemsFetchData(url))
    };
};

Опять же, я удалил префикс items из возвращаемого свойства объекта. Здесь fetchData - это функция, которая принимает параметр url и возвращает dispatching itemsFetchData(url).

Эти 2 mapStateToProps() и mapDispatchToProps() еще ничего не делают, поэтому нам нужно изменить нашу последнюю строку export на:

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

Это connect переводит наш ItemList в Redux при сопоставлении свойств, которые мы будем использовать.

Последний шаг - преобразовать наш компонент для использования props вместо state и удалить остатки.

  • Удалите методы constructor() {} и fetchData() {}, поскольку они теперь не нужны.
  • Измените this.fetchData() в componentDidMount() на this.props.fetchData().
  • Измените this.state.X на this.props.X для .hasErrored, .isLoading и .items.

Теперь ваш компонент должен выглядеть так:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { itemsFetchData } from '../actions/items';
class ItemList extends Component {
    componentDidMount() {
        this.props.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
    }
    render() {
        if (this.props.hasErrored) {
            return <p>Sorry! There was an error loading the items</p>;
        }
        if (this.props.isLoading) {
            return <p>Loading…</p>;
        }
        return (
            <ul>
                {this.props.items.map((item) => (
                    <li key={item.id}>
                        {item.label}
                    </li>
                ))}
            </ul>
        );
    }
}
const mapStateToProps = (state) => {
    return {
        items: state.items,
        hasErrored: state.itemsHasErrored,
        isLoading: state.itemsIsLoading
    };
};
const mapDispatchToProps = (dispatch) => {
    return {
        fetchData: (url) => dispatch(itemsFetchData(url))
    };
};
export default connect(mapStateToProps, mapDispatchToProps)(ItemList);

Вот и все! Приложение теперь использует Redux и Redux Thunk для извлечения и отображения данных!

Это было не так уж сложно, правда?

И теперь вы мастер Redux: D

Что дальше?

Я разместил весь этот код на GitHub с коммитами для каждого шага. Я хочу, чтобы вы клонировали его, запустили и поняли, а затем добавили возможность пользователю удалять отдельные элементы списка на основе index элемента.

Я еще не упомянул, что в Redux состояние неизменяемо, что означает, что вы не можете его изменять, поэтому вместо этого вам нужно возвращать новое состояние в редукторах. Три редуктора, которые мы написали выше, были простыми и «просто работали», но удаление элементов из массива требует подхода, с которым вы, возможно, не знакомы.

Вы больше не можете использовать Array.prototype.splice() для удаления элементов из массива, так как это приведет к изменению исходного массива. Дэн объясняет, как удалить элемент из массива в этом видео, но если вы застряли, вы можете проверить (каламбур) ветку delete-items для решения.

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

Я бы по-прежнему рекомендовал читать документы Redux, смотреть видео Дэна и переделывать курс Уэса, поскольку, надеюсь, теперь вы сможете понять некоторые другие, более сложные и более глубокие принципы.

Эта статья была размещена на Codepen для лучшего форматирования кода.