View on GitHub

Yinjie - GitHub.io

Welcome to the Yinjie's notes

【翻译】在 react 中使用 redux

原文地址: http://codepen.io/stowball/post/a-dummy-s-guide-to-redux-and-thunk-in-react

如果你像我一样,在读了 redux 文档、看过了 Dan’s 教学视频、完成了 We’s 课程,还是没有搞明白如何使用 redux,那么希望此篇文章能够帮到你。

在我试用过好几次的 redux 后才理解了 redux 的用法,将一个从 ajax 接口接收 json 数据的应用改变成使用 redux/redux thunk 的模式。 因此我觉得应该将此过程记录下来,如果你不懂什么是 thunk,那也别着急,我们将在 “Redux way” 的异步调用中使用到它。

此篇文章假设你已经有了 react 和 ES6 的基础,掌握这些应该也是比较简单的。

非 Redux 模式

我们先来写一个 react component:components/ItemList.js,调用一个接口获取列表数据。

基础内容

首先,我们在 state 中设置一些静态的数据:包括主要显示内容的 items,还有两个 boolean 标识用来区分正在加载和加载错误时的渲染内容。

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;

这看起来不太像是一个非常漂亮的开局,但至少是算是好的。

当渲染完毕后,component 会展示出这4个列表元素,如果你将 isLoadinghasErrored 置成 true,一个对应的 <p> 会替代列表显示 。

改成动态组件

把这些元素都写死的话,这个 component 肯定没法用的,so 我们需要从一个 json api 中获取 items 数据,同时也需要我们在适当的时候将 isLoadinghasErrored 正确的置位。

接口提供的 items 数据和我们造的是完全一致的,当然在真实情况下,它可能是一个畅销书的列表、一个最新的博文列表,或是其它符合你业务场景的数据。

获取数据我们用 fetchfetch 使用起来比传统的 XMLHttpRequest 更方便,而且返回的 response 结果是一个标准的 promise 对象(这对于 Thunk 模式非常重要)。 fetch 无法在所有浏览器中得到支持,所以你需要在项目中安装以下组件:

npm install whatwg-fetch --save

开始转换这一部分,非常简单:

  1. 将初始的 items 置成一个空的数组;
  2. 添加获取数据的方法和控制 isLoadinghasErrored 的状态:
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 }));
}
  1. 在组件加载完毕后我们调用它;
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() {
    }
}

你的组件通过一个 REST 端获取数据,我们希望在items获取之前能够看到一个简单的 loading... 。 如果接口获取失败,也应该有一个相应的错误提示。

然而,在现实情况下,一个 component 不应当包括抓取数据的逻辑,数据也不应当存储在 componentstate 中, 这个时候, Redux 就应声而来了。

转换成 Redux 模式

理解 Redux

在这之前,我们需要理解几个 Redux 的核心原则:

  1. 有一个全局的 state 来控制整个应用的各个 state,在这个例子中,它的作用和我们初始化 state 的行为是一致的,就是所谓的‘真相只有一个’;
  2. 唯一改变 state 的方法就是通过引入的 action,它是一个描述哪个数据应当被修改的对象(方法)。Action Creators 是一些 function,它们被用来发起这些改变,所作的就是返回一个 action;
  3. 当一个 action 被发起,reducer 就上场了,它会根据相关的 action 改变真实的 state 属性,或是当这个 action 不适用与这个 reducer 时,返回当前的 state;
  4. reducer 应该是一个“纯函数”,不应当对传入的 state 产生任副作用,或是改变 state 本体,它应该返回一个修改后的副本;
  5. 单个的 reducer 被合并成一个 rootReducer,从页产生出那些互不相关的 state 属性;
  6. Store 的作用是将这些整合到一块去,它通过 rootReducer 方法和任何中间件来表示状态(在本例中为Thunk),并允许您 dispatch action;
  7. 在 React 中使用 Redux,<Provider /> 包裹了整个的应用,并且将 store 向下传递给在子结点;

随着我们将项目转换成使用 redux 的模式,这些原则也将变得更加清晰。

设计我们的 state

从我们已经完成的那些内容来看,认识到我们的 state 需要3个属性:items, hasErroredisLoading,这样它就能在我们所预期的所有情况下工作了,因此,我们也需要三个相关的 action。

到这里,需要说明为什么 Action Creators 和 Actions 不一样,而且也不是 1对1 的关系:我们需要第四个 actions Creator,来根据我们所获取到的数据结果调用其它3个不同的 action。 这第4个 action creator 基本上和我们原始的 fetchData() 方法作用是一致的,但是它不会直接显式的调用 this.setState({ isLoading: true }) 来改变组件的 state,而是 dispatch 一个 action 来作这件事:dispatch(isLoading(true))

编写 action

我们来写一个 actions/items.js,作为 action creators,我们从三个简单的方法开始:

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
    };
}

如前面所述,action creator 返回的是 action。 我们将每个方法 export,这样就可以代码库任何地方来使用它。

前两个 action creators 将 boolean 值作为参数,同时返回一个对象,对象包括一个特定字符串的 type,和对应属性的 bool 值。

第三个方法 itemsFetchSuccess() 将会在数据成功获取后调用,同时将所需的数据作为 items 进行传递, 利用 es6 “property shorthands” 的特性,我们可以将 items 赋值给名称同为 items 的属性。

注意:你所用到的 type 的值和你需要返回的 property 属性名是非常重要的,因为你还要在 reducer 中用到它。

现在,我们已经有了3个 action 来表示所谓的 state, 现在我们来修改原有的 fetchData 方法,转换成 itemsFetchData() action。

默认情况下,Redux action creators 不支持像 fetch 这样的异步数据,因此,在这里我们需要使用 redux thunk 模式。 Thunk 允许你编写一个返回 function 的 action creators,而不是返回一个对象。 这个内部函数可以接收 store 方法中的 dispatch 和 getState 作为参数,但是我们只使用 dispatch。

举个简单的例子,比如是在5秒后手动触发 itemsHasErrored()

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);
    };
}

现在我们知道什么是 thunk 模式了,如此,我们就可以编写 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)));
    };
}

编写 reducers

action creators 已经搞定,现在我们来写 reducers,用来调用 actions 并且给应用返回一个新的 state。

注意:在 Redux 中,每个 redux 被调用时不会管你其中的 actions 写的是什么,所以如果某个其中的 action 并没有被应用到, 你也必须返回原始的 state。

每一个 reducers 都包括两个参数:修改前的 stateaction 对象,你也可以用 es6 参数默认值的特性来给 state 初始化默认值。

在每一个 reducer 中,我们用 switch 语法来匹配某一个 action.type,虽然在一些简单的 reducer 中这些看起来并没有什么必要, 但是你的 reducer 在理论上肯定会有很多条件,用 if/else 的话,就显的很淩乱了。

如果某一个 action.type 被匹配到,我们接下来就要返回这个 action 相关的属性, 就像文章早些时候提到的,typeaction[propertyName] 被定义在你的 action creator 中。

了解这些后,我们来搞一个 reducer :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;
    }
}

注意一下每一个 reducer 都是怎么根据 store 的 state 属性处理结果来命名的。action.type 不一定是完全对应的,前两个 reducer 的命名是完全有意义的,最后一个 items 就不太相符了。

这是因为它可能在多个条件下返回的都是 items 数组:可能是在正常获取成功的情况下返回全部;可能是删除操作后返回一个子集,或者是列表被全删除后返回一个空列表。

需要重申一下,不管 reducer 传入了多少的条件,每一个 reducer 返回的是 state 的离散属性。这个原则在刚开始还让我纠结了一会儿。

这个单个的 reducer 创建后,我们需要一个 rootReducer 将它们联合起来,产生一个单独的对象。

reducers/index.js 如下:

import { combineReducers } from 'redux';
import { items, itemsHasErrored, itemsIsLoading } from './items';

export default combineReducers({
    items,
    itemsHasErrored,
    itemsIsLoading
});

我们从 items.js 中引入每一个 reducers,再通过 Redux’s combineReducers() 进行统一 export。 在这里,reducer (export) 的名称和 store 的属性名一致,也可以用 es6 的 shorthand 特性。

在这里注意一下,我是如何特意规定 reducer 的命名前缀的。 当你的应用复杂性不断增加时,也不会受到如 “hasErrored”、“isLoading” 等全局属性的干扰,因为你很有可能有许多不同的功能会出现错误或正在加载的状态。 因此给每一个 reducer 增加业务前缀,这样使你的应用 state 更加细化和灵活。

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
});

还有一种方向是导入时用别名 as,但笔者更喜欢代码的一致性,别名会增加代码的阅读难度。

配置 store 作为你 app 的数据源

这个非常直观,创建一个 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)
    );
}

调整一下 app’s index.js,引入 ProviderconfigureStore,装载我们写好的 store,并在 <ItemList /> 外面包裹一层 <Provider />,如此将 store 作为 props 传入 app。

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 成功加载完毕后,我们就可以修改组件来满足

用 Redux 的 store 和 方法 来改写 component

我们先跳到 components/ItemList.js

在文件的顶部,引入我们所需的模块:

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

connect 用来联接 component 和 redux 的 store,itemsFetchData 是之前写的 action。 我们目前只需要引入这一个 action creator,它负责处理调用其它 action。

定义好 component 类,我们来做一个 Redux’s state 和 action 调用方法的映射,用来将两者对应到组件的 props 里。

定义一个方法 mapStateToProps,返回 props 对象。在上面那个简单的 component 里,我去掉了 has/is 前面的业务前缀, 因为在这里,很明显它就是和 items 关联的。

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

接着我们需要另外一个方法来通过 props dispatch 之前写的 itemsFetchData() action。

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

我们在返回对象的属性中去掉了 items 的前缀。 fetchData 是被定义成一个接收 url 参数然后返回一个 dispatch 调用 itemsFetchData(url) 的方法。

目前,mapStateToProps()mapDispatchToProps()这两个方法还没有做任何事情,我们需要改变最终的 export 如下:

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

此处 connect 方法通过 props 映射的方式将 ItemList 组件接入 Redux,供我们在 view 组件中使用。

最后一步,将组件从使用 state 转换成使用 props,然后按些去掉多余的东西:

  • 删掉 constructor() {}fetchData() {} 方法,现在它们已经不需要了。
  • componentDidMount() 中的 this.fetchData() 改成 this.props.fetchData()
  • .hasErrored.isLoading.items 对应的 state 值 this.state.X 改成 this.props.X

最终代码:

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 来抓取和展示数据的 react 应用。

也不是很难嘛~

后话

笔者已将上述例子放置到 github,每一步都有 commit。 你可以 clone 下来,运行后理解一下,再试着添加一个用户根据不同项目的索引,删除列表中某个列表元素的功能。

之前我没有提到过,在这里我需要说一下,redux 中的 state 是不可变的,你不应该去修改它,只能是通过 reducer 来返回一个新的 state。 我们之前写得3个 reducer 是非常简单的,仅作演示。但是,如果要在一个数组中按索引删除一个元素,就需要和以上不同的方法了。

此类情况不能使用 splice 来从数组中删除元素,这样会改变原始数组。

Dan 展示了如何从数组中删除一个元素 如果你有疑问,可以 checkout 出 delete-items 分支看一眼。

我希望通过这篇博文能够解释一下 redux 和 thunk 的概念,通过教你怎样将 react 应用转换成使用 redux(thunk) 模式。 当然,我也通过这篇文章巩固了我对此的理解,本人也是很高兴的。

最后还是建议你阅读 redux 文档、看一下 Dan 的视频Wes 的教程,通这些来了解 redux 更深一层的原理。