React系列教程(4)React Redux快速入门(上)

简介: React系列教程(4)React Redux快速入门

豆约翰习惯将掌握某一技术分为5个层次:初窥门径,小试牛刀,渐入佳境,得心应手,玩转自如

本篇属于React框架中的第1层次即初窥门径



注:对原文有增补(补充了请求api接口的完整实例以及配套源码)


想要理解 Redux 完整的工作机制真的让人头疼。特别是作为初学者。

术语太多了!Actions、reducers、action creators、middleware、pure functions、immutability、thunks 等等。

怎么把这些全都与 React 结合起来构建一个可运行的应用?

你可以花几个小时阅读博客以及尝试从复杂的“真实世界”应用中研习以将它拼凑起来。

在本篇 Redux 教程中,我会渐进地解释如何将 Redux 与 React 搭配使用 —— 从简单的 React 开始 —— 以及一个非常简单的 React + Redux 案例。我会解释为什么每个功能都很有用(以及什么情况下做取舍)。

接着我们会看到更加进阶的内容,手把手,直到你全部都理解为止。我们开始吧 :)

请注意:本教程相当齐全。也就意味篇幅着比较长


Redux 的好处


如果你稍微使用过一段时间的 React,你可能就了解了 props 和单向数据流。数据通过 props 在组件树间向传递。就像这个组件一样:


image.png

count 存在 App 的 state 里,会以 prop 的形式向下传递:



image.png

image.png

要想数据向上传递,需要通过回调函数实现,因此必须首先将回调函数向下传递到任何想通过调用它来传递数据的组件中。


image.png

你可以把数据想象成电流,通过彩色电线连接需要它的组件。数据通过线路上下流动,但是线路不能在空气中贯穿 —— 它们必须从一个组件连接到另一个组件。


多级传递数据是一种痛苦


迟早你会陷入这类场景,顶级容器组件有一些数据,有一个 4 级以上的子组件需要这些数据。这有一个 Twitter 的例子,所有头像都圈出来了:


image.png


我们假设根组件 App 的 state 有 user 对象。该对象包含当前用户头像、昵称和其他资料信息。

为了把 user 数据传递给全部 3 个 Avatar 组件,必须要经过一堆并不需要该数据的中间组件。


image.png

获取数据就像用针在采矿探险一样。等等,那根本没有意义。无论如何,这很痛苦。也被称为 “prop-drilling”。

更重要的是,这不是好的软件设计。中间组件被迫接受和传递他们并不关心的 props。也就意味着重构和重用这些组件会变得比原本更难。

如果不需要这些数据的组件根本不用看到它们的话不是很棒吗?

Redux 就是解决这个问题的一种方法。

相邻组件间的数据传递

如果你有些兄弟组件需要共享数据,React 的方式是把数据向传到父组件中,然后再通过 props 向下传递。

但这可能很麻烦。Redux 会为你提供一个可以存储数据的全局 "parent",然后你可以通过 React-Redux 把兄弟组件和数据 connect 起来。


使用 React-Redux 将数据连接到任何组件


使用 react-reduxconnect 函数,你可以将任何组件插入 Redux 的 store 以及取出需要的数据。


image.png


学习 Redux,从简单 React 开始


我们将采用增量的方法,从带有组件 state 的简单 React 应用开始,一点点添加 Redux,以及解决过程中遇到的错误。我们称之为“错误驱动型开发” :)

这是一个计数器:


image.png

image.png

这本例中,Counter 组件有 state,包裹着它的 App 是一个简单包装器。

Counter.js

import React from 'react';
    class Counter extends React.Component {
      state = { count: 0 }
      increment = () => {
        this.setState({
          count: this.state.count + 1
        });
      }
      decrement = () => {
        this.setState({
          count: this.state.count - 1
        });
      }
      render() {
        return (
          <div>
            <h2>Counter</h2>
            <div>
              <button onClick={this.decrement}>-</button>
              <span>{this.state.count}</span>
              <button onClick={this.increment}>+</button>
            </div>
          </div>
        )
      }
    }
    export default Counter;

快速回顾一下,它是如何运行的:

  • count state 存储在 Counter 组件
  • 当用户点击 "+" 时,会调用按钮的 onClick 处理器执行 increment 函数。
  • increment 函数会更新 state 的 count 值。
  • 因为 state 改变了,React 会重新渲染 Counter 组件(以及它的子元素),这样就会显示新计数值。


在 React 应用中添加 Redux

yarn add \ # or npm i --save
redux \
react-redux \
redux-thunk \
redux-devtools-extension \
react-router-dom


redux vs react-redux


redux 给你一个 store,让你可以在里面保存 state,取出 state,以及当 state 发生改变时做出响应。但那就是它所有能做的事。

实际上是 react-redux 把各个 state 和 React 组件连接起来。

没错:redux 对 React 根本不了解。

redux 库可以脱离 React 应用使用。它可以和 Vue、Angular 甚至后端的 Node/Express 应用一起使用。


Redux 有全局唯一 Store


我们将首先从 Redux 中的一小部分入手:store。

我们已经讨论过 Redux 怎样在一个独立 store 里保存你应用的 state。以及怎样提取 state 的一部分把它作为 props 嵌入你的组件。

你会经常看到 "state" 和 "store" 这两个词互换使用。技术上来讲,state 是数据,store 是保存数据的地方。

因此:作为我们从简单的 React 到 Redux 重构的第一步,我们要创建一个 store 来保持 state。


创建 Redux Store


Redux 有一个很方便的函数用来创建 stores,叫做 createStore。很合逻辑,嗯?

我们在 index.js 中创建一个 store。引入 createStore 然后像这样调用:

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import { createStore } from 'redux';
import Counter from './Counter'
const store = createStore();
const App = () => (
    <Provider store={store}>
        <Counter/>
    </Provider>
);
ReactDOM.render(<App />, document.getElementById('root'));

这样会遇到 "Expected the reducer to be a function." 错误。


image.png

Store 需要一个 Reducer


因此,有件关于 Redux 的事:它并不是非常智能。

你可能期待通过创建一个 store,它会给你的 state 一个合适的默认值。或许是一个空对象?

但是并非如此。这里没有约定优于配置。

Redux 不会对你的 state 做任何假设。它可能是一个 object、number、string,或者任何你需要的。这取决于你。

我们必须提供一个返回 state 的函数。这个函数被称为 reducer(我们马上就知道为什么了)。那么我们创建一个非常简单的 reducer,把它传给 createStore,然后看会发生什么:

index.js

function reducer(state, action) {
      console.log('reducer', state, action);
      return state;
    }
    const store = createStore(reducer);

修改完后,打开控制台

你应该可以看到类似这样的日志信息:


image.png

(INIT 后面的字母和数字是 Redux 随机生成的)

注意在你创建 store 的同时 Redux 如何调用你的 reducer。(为了证实这点:调用 createStore 之后立即输出 console.log,看看 reducer 后面会打印什么)

同样注意 Redux 如何传递了一个 undefinedstate,同时 action 是一个有 type 属性的对象。

我们稍后会更多地讨论 actions。现在,我们先看看 reducer


Redux Reducer 是什么?


"reducer" 术语看起来可能有点陌生和害怕,但是本节过后,我认为你会同意如下观点,正如俗话所说的那样,“只是一个函数”。

你用过数组的 reduce 函数吗?

它是这样用的:你传入一个函数,遍历数组的每一个元素时都会调用你传入的函数,类似 map 的作用 —— 你可能在 React 里面渲染列表而对 map 很熟悉。

你的函数调用时会接收两个参数:上一次迭代的结果,和当前数组元素。它结合当前元素和之前的 "total" 结果然后返回新的 total 值。

结合下面例子看会更加清晰明了:

var letters = ['r', 'e', 'd', 'u', 'x'];
    // `reduce` 接收两个参数:
    //   - 一个用来 reduce 的函数 (也称为 "reducer")
    //   - 一个计算结果的初始值
    var word = letters.reduce(
      function(accumulatedResult, arrayItem) {
        return accumulatedResult + arrayItem;
      },
    ''); // <-- 注意这个空字符串:它是初始值
    console.log(word) // => "redux"

你给 reduce 传入的函数理所应当被叫做 "reducer",因为它将整个数组的元素 reduces 成一个结果。

Redux 基本上是数组 reduce 的豪华版。前面,你看到 Redux reducers 如何拥有这个显著特征:

(state, action) => newState

含义:它接收当前 state 和一个 action,然后返回 newState。看起来很像 Array.reduce 里 reducer 的特点!

(accumulatedValue, nextItem) => nextAccumulatedValue

Redux reducers 就像你传给 Array.reduce 的函数作用一样!:) 它们 reduce 的是 actions。它们把一组 actions(随着时间)reduce 成一个单独的 state。不同之处在于 Array 的 reduce 立即发生,而 Redux 则随着正运行应用的生命周期一直发生。


给 Reducer 一个初始状态


记住 reducer 的职责是接收当前 state 和一个 action 然后返回新的 state。

它还有另一个职责:在首次调用的时候应该返回初始 state。它有点像应用的“引导页”。它必须从某处开始,对吧?

惯用的方式是定义一个 initialState 变量然后使用 ES6 默认参数给 state 赋初始值。

既然要把 Counter state 迁移到 Redux,我们先立马创建它的初始 state。在 Counter 组件里,我们的 state 是一个有 count 属性的对象,所以我们在这创建一个一样的 initialState。

index.js

const initialState = {
      count: 0
    };
    function reducer(state = initialState, action) {
      console.log('reducer', state, action);
      return state;
    }

如果你再看下控制台,你会看到 state 打印的值为 {count: 0}。那就是我们想要的。

所以这告诉我们一条关于 reducers 的重要规则。

Reducers 重要规则一:reducer 绝不能返回 undefined。

通常 state 应该总是已定义的。已定义的 state 是良好的 state。而undefined不那么好(并且会破坏你的应用)。


Dispatch Actions 来改变 State


是的,一下来了两个名字:我们将 "dispatch" 一些 "actions"。


什么是 Redux Action?


在 Redux 中,具有 type 属性的普通对象就被称为 action。就是这样,只要遵循这两个规则,它就是一个 action:

{
      type: "add an item",
      item: "Apple"
    }

This is also an action:

{
      type: 7008
    }

Here's another one:

{
      type: "INCREMENT"
    }


Actions 的格式非常自由。只要它是个带有 type 属性的对象就可以了。

为了保证事务的合理性和可维护性,我们 Redux 用户通常给 actions 的 type 属性赋简单字符串,并且通常是大写的,来表明它们是常量。

Action 对象描述你想做出的改变(如“增加 counter”)或者将触发的事件(如“请求服务失败并显示错误信息”)。

尽管 Actions 名声响亮,但它是无趣的,呆板的对象。它们事实上不任何事情。反正它们自己不做。

为了让 action 点事情,你需要 dispatch。


Redux Dispatch 工作机制


我们刚才创建的 store 有一个内置函数 dispatch。调用的时候携带 action, reducer 会被redux调用,,并收到action参数(然后 reducer 的返回值会更新 state)。

我们在 store 上试试看。

index.js

const store = createStore(reducer);
    store.dispatch({ type: "INCREMENT" });
    store.dispatch({ type: "INCREMENT" });
    store.dispatch({ type: "DECREMENT" });
    store.dispatch({ type: "RESET" });

在你的 CodeSandbox 中添加这些 dispatch 调用然后检查控制台


image.png

image.png

每一次调用 dispatch 最终都会调用 reducer!

同样注意到 state 每次都一样?{count: 0} 一直没变。

这是因为我们的 reducer 没有作用于那些 actions。不过很容易解决。现在就开始吧。


在 Redux Reducer 中处理 Actions


为了让 actions 做点事情,我们需要在 reducer 里面写几行代码来根据每个 action 的 type 值来对应得更新 state。

有几种方式实现。

你可以创建一个对象来通过 action 的 type 来查找对应的处理函数。

或者你可以写一大堆 if/else 语句

if(action.type === "INCREMENT") {
      ...
    } else if(action.type === "RESET") {
      ...
    }

或者你可以用一个简单的 switch 语句,也是我下面采用的方式,因为它很直观,也是这种场景的常用方法。

尽管有些人讨厌 switch,如果你也是 —— 随意用你喜欢的方式写 reducers 就好 :)

下面是我们处理 actions 的逻辑:

index.js

function reducer(state = initialState, action) {
      console.log('reducer', state, action);
      switch(action.type) {
        case 'INCREMENT':
          return {
            count: state.count + 1
          };
        case 'DECREMENT':
          return {
            count: state.count - 1
          };
        case 'RESET':
          return {
            count: 0
          };
        default:
          return state;
      }
    }


试一下然后在控制台看看会输出什么。


image.png


快看!count 变了!

我们准备好把它连接到 React 了,在此之前让我们先谈谈这段 reducer 代码。


如何保持纯 Reducers


另一个关于 reducers 的规则是它们必须是纯函数。也就是说不能修改它们的参数,也不能有副作用(side effect)。

Reducer 规则二:Reducers 必须是纯函数。

“副作用(side effect)”是指对函数作用域之外的任何更改。不要改变函数作用域以外的变量,不要调用其他会改变的函数(比如 fetch,跟网络和其他系统有关),也不要 dispatch actions 等。

技术角度来看 console.log 是副作用(side effect),但是我们忽略它。

最重要的事情是:不要修改 state 参数。

这意味着你不能执行 state.count = 0state.items.push(newItem)state.count++ 及其他类型的变动 —— 不要改变 state 本身,及其任何子属性。

你可以把它想成一个游戏,你唯一能做的事就是 return { ... }。这是个有趣的游戏。开始会有点恼人。但是通过练习你会变得更好。


全部规则


必须返回一个 state,不要改变 state,不要 connect 每一个组件,要吃西兰花,11 点后不要外出…这简直没完没了。就像一个规则工厂,我甚至不知道那是什么。

是的,Redux 就像一个霸道的父母。但它是出于爱。函数式编程的爱。

Redux 建立在不变性的基础上,因为变化的全局 state 是一条通往废墟之路。

你试过在全局对象里面保存你的 state 吗?起初它还很好。美妙并且简单。任何东西都能接触到 state 因为它一直是可用的并且很容易更改。

然后 state 开始以不可预测的方式发生改变,想要找到改变它的代码变得几乎不可能。

为了避免这些问题,Redux 提出了以下规则。

  • State 是只读的,唯一修改它的方式是 actions。
  • 更新的唯一方式:dispatch(action) -> reducer -> new state。
  • Reducer 函数必须是“纯”的 —— 不能修改它的参数,也不能有副作用(side effect)。


如何在 React 中使用 Redux


此时我们有个很小的带有 reducerstore,当接收到 action 时它知道如何更新 state

现在是时候将 Redux 连接到 React 了。

要做到这一点,要用到 react-redux 库的两样东西:一个名为 Provider 的组件和一个 connect 函数。

通过用 Provider 组件包装整个应用,如果它想的话,应用树里的每一个组件都可以访问 Redux store。

index.js 里,引入 Provider 然后用它把 App 的内容包装起来。store 会以 prop 形式传递。

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import { createStore } from 'redux';
import Counter from './Counter'
const initialState = {
    count: 0
};
function reducer(state = initialState, action) {
    console.log('reducer', state, action);
    switch(action.type) {
        case 'INCREMENT':
            return {
                count: state.count + 1
            };
        case 'DECREMENT':
            return {
                count: state.count - 1
            };
        case 'RESET':
            return {
                count: 0
            };
        default:
            return state;
    }
}
const store = createStore(reducer);
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "DECREMENT" });
store.dispatch({ type: "RESET" });
const App = () => (
    <Provider store={store}>
        <Counter/>
    </Provider>
);
ReactDOM.render(<App />, document.getElementById('root'));


这样之后,CounterCounter 的子元素,以及子元素的子元素等等——所有这些现在都可以访问 Redux stroe。

但不是自动的。我们需要在我们的组件使用 connect 函数来访问 store。


React-Redux Provider 工作机制


Provider 可能看起来有一点点像魔法。它在底层实际是用了 React 的 Context 特性

Context 就像是连接每个组件的秘密通道,使用 connect 就可打开秘密通道的大门。

目录
相关文章
|
2月前
|
前端开发 JavaScript 测试技术
从零开始搭建react+typescript+antd+redux+less+vw自适应项目
从零开始搭建react+typescript+antd+redux+less+vw自适应项目
52 0
|
2月前
|
开发框架 前端开发 JavaScript
使用React、Redux和Bootstrap构建社交媒体应用
使用React、Redux和Bootstrap构建社交媒体应用
15 0
|
3月前
|
前端开发
React快速入门
React快速入门
14 0
|
4月前
|
JavaScript 前端开发
React结合Redux实现Todolist
React结合Redux实现Todolist
19 0
|
5月前
|
前端开发 JavaScript API
中国农村大学生学习了这个【React教程】迎娶导师女儿,出任CEO走上人生巅峰
中国农村大学生学习了这个【React教程】迎娶导师女儿,出任CEO走上人生巅峰
37 0
|
7月前
|
前端开发 JavaScript 容器
React的魅力: React-Router-集中式管理和Redux-核心概念
React的魅力: React-Router-集中式管理和Redux-核心概念
43 1
|
8月前
|
JavaScript 前端开发 中间件
React(六) —— redux
React(六) —— redux
|
8月前
|
JavaScript 前端开发 算法
|
8月前
|
Web App开发 JavaScript 前端开发
React | Redux的使用详解(二)
React | Redux的使用详解(二)
|
8月前
|
存储 缓存 JavaScript
React | Redux的使用详解(一)
React | Redux的使用详解(一)