Redux 及 React Redux 使用指南

342 阅读12分钟

写在前面

本文代码通过 create-react-app 脚手架进行搭建,所有的代码均可直接复制运行。

代码位置:react-redux-demo

本文主要讲解了 Redux 和 React-Redux 的使用,详细的概念以及设计思想请看 Redux 中文官网

# npm
npm install create-react-app -g

#yarn
yarn add create-react-app -g
  • 创建 React 项目
create-react-app my-app
  • 安装 redux
npm install redux
# or
yarn add redux

用法详解

Action 描述更新对象

是把数据从组件传到 store 的载体,是修改 store 数据的唯一来源。 是一个普通的 javascript 对象,必须包含一个 type 属性,用来通知 reducer 这个 action 需要做的操作类型。 比如:

{
    type: 'ADD',
    payload: 1
}

通过 store.dispatch(action)action 传给 store

Reducer 执行更新函数

描述 store 数据如何更新的纯函数,接受两个参数

  • statestore 中的 state 值,可以给 state 设置初始值

  • action:通过 store.dispatch(action) 传递的 action 对象

通过 actiontype 类型来判断如何更新 state 数据

比如:

function reducer(state = 0, { type, payload }) {
  switch (type) {
    case "ADD":
      return state + payload;
    case "DELETE":
      return state - payload;
    default:
      return state;
  }
}

Store

actionreducer 联系到一起的对象,具有以下职责

  • 维持应用的 state
  • 提供 getState() 方法获取 state
  • 提供 dispatch(action) 方法更新 state
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器

store 的创建方式

const store = createStore(reducer[, prevState, ehancer]);

实现一个简单的 React 计数器

  • 通过 store.dispatch(action) 通知数据更新
  • 通过 store 获取 state 数据
  • 编写 reducer 实现数据更新

store/index.js

import { createStore } from "redux";

// 创建 reducer 函数 ,更新 state 数据
const reducer = function(state = 0, { type }) {
  switch (type) {
    case "INCREMENT":
      return ++state;
    case "DECREMENT":
      return --state;
    default:
      return state;
  }
};

// 创建 store
const store = createStore(reducer);

export default store;

App.js

import React, { useEffect, useReducer, useCallback } from "react";
import store from "./store";

function App() {
  // 模拟 forceUpdate 方法
  const [, forceUpdate] = useReducer(x => x + 1, 0);

  useEffect(() => {
    // 订阅 store 监听事件
    const unsubscribe = store.subscribe(() => {
      forceUpdate();
    });
    return () => {
      // 组件销毁时移除事件订阅
      unsubscribe();
    };
  }, []);

  const increment = useCallback(
    // 分发 action
    () => store.dispatch({ type: "INCREMENT" }),
    []
  );

  const decrement = useCallback(
    // 分发 action
    () => store.dispatch({ type: "DECREMENT" }),
    []
  );

  return (
    <div className="App">
      <h1>Hello Redux</h1>
      {/* 获取当前 state 值 */}
      <p>count: {store.getState()}</p>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
    </div>
  );
}

export default App;

这个时候,计数器已经实现了,点击 increment 或者 decrement 按钮,会更新界面上的数据

那假如说,此时我们可能会处理多个业务场景,比如一个是计数器,一个是 TodoList,会有两个 reducer,这个时候该如何创建呢?请看下一个 API

combineReducers 合并 reducer

是一个高阶函数,作用是将多个 reducer 函数按照合并生成一个 reducer 函数。 接受一个对象,返回一个 reducer 函数。对象的键可以设置任意属性名,对象的值是对应的 reducer 函数。

在使用 store 中的 state 值时,state 中的对应的属性名就是之前传给 combineReducers 方法的对象的属性名。

比如:

const reducer = combineReducers({
  count: counterReducer,
  todos: todoReducer
});

获取 state 时:

const state = store.getState();
// state: { count: xxx, todos: xxx }

我们在上面的例子中再加一个展示 TodoList 的功能

store/index.js

import { createStore, combineReducers } from "redux";

// 创建 counterReducer 函数 ,更新 state 数据
const counterReducer = function(state = 0, { type }) {
  switch (type) {
    case "INCREMENT":
      return ++state;
    case "DECREMENT":
      return --state;
    default:
      return state;
  }
};

// 创建 todoReducer 函数,更新 state 数据
const todoReducer = function(state = [], { type, payload }) {
  switch (type) {
    case "INIT":
      return payload;
    case "ADD":
      state.push(payload);
      return [...state];
    default:
      return state;
  }
};

// 合并 reducer
const reducer = combineReducers({
  count: counterReducer,
  todos: todoReducer
});

// 创建 store
const store = createStore(reducer);

export default store;

App.js

import React, { useEffect, useReducer, useCallback, useState } from "react";
import store from "./store";

function App() {
  // 模拟 forceUpdate 方法
  const [, forceUpdate] = useReducer(x => x + 1, 0);
  const [value, setValue] = useState("");

  useEffect(() => {
    // 订阅 store 监听事件
    const unsubscribe = store.subscribe(() => {
      forceUpdate();
    });
    return () => {
      // 组件销毁时移除事件订阅
      unsubscribe();
    };
  }, []);

  const increment = useCallback(
    // 分发 action
    () => store.dispatch({ type: "INCREMENT" }),
    []
  );

  const decrement = useCallback(
    // 分发 action
    () => store.dispatch({ type: "DECREMENT" }),
    []
  );

  const add = useCallback(() => {
    if (value) {
      // 分发 action
      store.dispatch({ type: "ADD", payload: value });
      setValue("");
    }
  }, [value]);

  // 解构 state
  const { count, todos } = store.getState();

  return (
    <div className="App">
      <h1>Hello Redux</h1>
      <p>count: {count}</p>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
      <br />
      <br />
      <input
        placeholder="请输入待办事项"
        value={value}
        onChange={e => setValue(e.target.value)}
      />
      <button onClick={add}>add</button>
      <ul>
        {todos.map(todo => (
          <li key={todo}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

至此,计数器和 TodoList 的功能都已经实现了

我们现在一直用的都是 redux 自己的功能,如果我想使用一些插件该怎么做呢,比如我想使用 logger 插件打印一些日志,请看下一个 API

applyMiddleware 应用插件

使用 applyMiddleware 可以应用插件,扩展 redux 功能。

applyMiddleware 是一个函数,接受多个参数值,返回一个高阶函数供 createStore 使用。

const ehancer = applyMiddleware(middleware1[, middleware2, middleware3, ...]);

下面我们以 redux-logger 插件为例,使用 applyMiddleware

安装 redux-logger

npm install redux-logger -D
# or
yarn add redux-logger -D

store/index.js

// ...

// 合并 reducer
const reducer = combineReducers({
  count: counterReducer,
  todos: todoReducer
});

// 应用插件
const ehancer = applyMiddleware(logger);

// 创建 store
const store = createStore(reducer, ehancer);

export default store;

上面我们修改数据的时候一直都是在同步状态下进行,那如果现在有一个副作用操作,需要异步执行完成才能进行 state 更新,又该怎么做呢?就要用到 react-thunk 插件了

react-thunk

这个插件把 storedispatch 方法做了一层封装,可以接受一个函数作为 action。 当判断当前 action 是一个函数的时候,会自动执行,并将 dispatch 作为参数传给我们。

安装 react-thunk

npm install react-thunk -D
# or
yarn add react-thunk -D

下面我们看看 react-thunk 的用法以及使用场景

还是在刚才的例子上,我们想要在组件加载完成之后对 TodoList 添加一些初始值,这个过程是一个异步过程

react-thunk 插件应用到 store 中去 store/index.js

import thunk from "react-thunk";
// ...

// 应用插件
const ehancer = applyMiddleware(thunk, logger);

// 创建 store
const store = createStore(reducer, ehancer);

export default store;

App.js

// ...

useEffect(() => {
  // 派发一个异步 action,是一个函数
  store.dispatch(dispatch => {
    setTimeout(() => {
      dispatch({ type: "INIT", payload: ["吃饭", "睡觉", "敲代码"] });
    }, 1000);
  });
}, []);

// ...

我们现在分发 action 的时候,都是直接 dispatch 一个对象,代码少的情况下还好,多的话可能就比较复杂,还要和 reducer 中的 type 对应,所以写起来比较麻烦,下面我们介绍一个概念:ActionCreator

ActionCreator: action 创建函数

这不是一个 API 或者方法,只是一种思想和实现。就是通过调用一个函数生成一个对应的 action,在需要的时候我们直接调用这个函数,进行 dispatch 就可以了

比如:

const addTodo = todo => ({ type: "ADD", payload: todo });

这种写法我们 dispatch 的时候不用考虑 type ,也不用写键值对,只要传入正确的参数,就可以了。 下面我们就把刚才我们写的例子修改一下,使用这种 ActionCreator 的思想去编写 action

store/index.js 中添加 actionCreator 并导出

// ...

const increment = () => ({ type: "INCREMENT" });
const decrement = () => ({ type: "DECREMENT" });

const initTodos = todos => ({ type: "INIT", payload: todos });
const addTodo = todo => ({ type: "ADD", payload: todo });

// 异步 action,执行完成之后调用同步 action
const getAsyncTodos = () => dispatch =>
  setTimeout(() => dispatch(initTodos(["吃饭", "睡觉", "写代码"])), 1000);

export const actionCreators = { increment, decrement, getAsyncTodos, addTodo };

App.jsdispatch 中调用 actionCreator

import React, { useEffect, useReducer, useCallback, useState } from "react";
import store, { actionCreators } from "./store";

const {
  increment as inc,
  decrement as dec,
  getAsyncTodos,
  addTodo
} = actionCreators;

function App() {
  // 模拟 forceUpdate 方法
  const [, forceUpdate] = useReducer(x => x + 1, 0);
  const [value, setValue] = useState("");

  useEffect(() => {
    // 订阅 store 监听事件
    const unsubscribe = store.subscribe(() => {
      forceUpdate();
    });
    return () => {
      // 组件销毁时移除事件订阅
      unsubscribe();
    };
  }, []);

  useEffect(() => {
    // 派发一个异步 action,是一个函数
    store.dispatch(getAsyncTodos());
  }, []);

  const increment = useCallback(
    // 分发 action
    () => store.dispatch(inc()),
    []
  );

  const decrement = useCallback(
    // 分发 action
    () => store.dispatch(dec()),
    []
  );

  const add = useCallback(() => {
    if (value) {
      // 分发 action
      store.dispatch(addTodo(value));
      setValue("");
    }
  }, [value]);

  // 解构 state
  const { count, todos } = store.getState();

  return (
    <div className="App">
      <h1>Hello Redux</h1>
      <p>count: {count}</p>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
      <br />
      <br />
      <input
        placeholder="请输入待办事项"
        value={value}
        onChange={e => setValue(e.target.value)}
      />
      <button onClick={add}>add</button>
      <ul>
        {todos.map(todo => (
          <li key={todo}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

但是这个时候,感觉还是有点麻烦,每次触发 store 更新都要使用 dispatch(actionCreator(args)) 才行,能不能直接调用方法,就能触发 store 更新呢。 当然也可以了,redux 为我们提供了一个 bindActionCreators 函数

bindActionCreators

这个函数是将 dispatch 绑定到了 actionCreator 方法上,之后只要我们执行 actionCreator 就会触发 store 更新了,不用每次都 dispacth 了。

接受两个参数:

  • actionCreators:是一个对象,对象的属性名可以任意命名,属性值是对应的 actionCreator 方法
  • dispatchstore 中的 dipatch 属性

返回一个新的对象,对象的属性名是刚才传入的 actionCreators 中的属性名,属性值时包装后的方法,执行即可触发 store 更新

比如:

const finalActions = bindActionCreators({
    increment: () => ({ type: 'INCREMENT }),
    decrement: () => ({ type: 'DECREMENT })
},  dispatch)

// finalActions: { increment, decrement }

下面我们将 App.js 中的代码进行一波优化,看看最后的效果

import React, { useEffect, useReducer, useCallback, useState } from "react";
import { bindActionCreators } from "redux";
import store, { actionCreators } from "./store";

// 生成包装后的 actionCreator,执行之后就会触发 store 数据的更新
const { increment, decrement, getAsyncTodos, addTodo } = bindActionCreators(
  actionCreators,
  store.dispatch
);

function App() {
  // 模拟 forceUpdate 方法
  const [, forceUpdate] = useReducer(x => x + 1, 0);
  const [value, setValue] = useState("");

  useEffect(() => {
    // 订阅 store 监听事件
    const unsubscribe = store.subscribe(() => {
      forceUpdate();
    });
    return () => {
      // 组件销毁时移除事件订阅
      unsubscribe();
    };
  }, []);

  // 初始化 TodoList
  useEffect(() => {
    getAsyncTodos();
  }, []);

  const add = useCallback(() => {
    if (value) {
      // 分发 action
      addTodo(value);
      setValue("");
    }
  }, [value]);

  // 解构 state
  const { count, todos } = store.getState();

  return (
    <div className="App">
      <h1>Hello Redux</h1>
      <p>count: {count}</p>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
      <br />
      <br />
      <input
        placeholder="请输入待办事项"
        value={value}
        onChange={e => setValue(e.target.value)}
      />
      <button onClick={add}>add</button>
      <ul>
        {todos.map(todo => (
          <li key={todo}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

有没有发现,到目前为止,数据变化时,我们更新 React 组件还是通过自己去添加 subscribe 订阅,一个组件还好,那在项目开发的过程中,每个组件都这么写,岂不是太麻烦了。所以下面我们介绍 react-redux 的使用,可以帮我们解决这个问题

结合 Reatc 使用的正确姿势:react-redux

  • 安装
npm install react-redux
# or
yarn add react-redux
  • 常规用法

    • Provider: 使用 Provider 标签包裹根组件,将 store 作为属性传入,后续的子组件才能获取到 store 中的 statedispatch
    • connect:返回一个高阶组件,用来连接 React 组件与 Redux store,返回一个新的已与 Redux store 连接的组件类
  • hooks 用法

    • useDispatch:返回一个 dispatch 对象
    • useSelector:接受一个函数,将函数的返回值返回出来

Provider 使用

根目录下 index.js 文件

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import App from "./App";
import store from "./store";
import "./index.css";

ReactDOM.render(
  <React.StrictMode>
    {/* 使用 Provider 标签包裹住根组件,并将 store 作为参数传入 */}
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

connect 的使用

  • APIconnect([mapStateToProps],[mapDispatchToProps],[mergeProps],[options])

connect 的用法相对复杂一些,接受四个参数,返回的是一个高阶组件。用来连接当前组件和 Redux store

  1. mapStateToProps:函数类型,接受两个参数: stateownProps(当前组件的 props,不建议使用,会导致重渲染,损耗性能),必须返回一个纯对象,这个对象会与组件的 props 合并

    • (state[, ownProps]) => ({ count: state.count, todoList: state.todos })
  2. mapDispatchToProps:object | 函数

    • 不传递这个参数时,dispatch 会默认挂载到组件的的 props
    • 传递 object 类型时,会把 object 中的属性值使用 dispatch 包装后,与组件的 props 合并
    {
        increment: () => ({ type: "INCREMENT" }),
        decrement: () => ({ type: "DECREMENT" }),
    }
    
    • 对象的属性值都必须是 ActionCreator

    • dispatch 不会再挂载到组件的 props

    • 传递函数类型时,接收两个参数:dispatchownProps(当前组件的 props,不建议使用,会导致重渲染,损耗性能),必须返回一个纯对象,这个对象会和组件的 props 合并

    (state[, ownProps]) => ({
        dispatch,
        increment: dispatch({ type: "INCREMENT" }),
        decrement: dispatch({ type: "DECREMENT" })
    })
    
  3. mergeProps(很少使用) 函数类型。如果指定了这个参数,mapStateToProps()mapDispatchToProps()的执行结果和组件自身的 props 将传入到这个回调函数中。该回调函数返回的对象将作为 props 传递到被包装的组件中。你也许可以用这个回调函数,根据组件的 props 来筛选部分的 state 数据,或者把 props 中的某个特定变量与 ActionCreator 绑定在一起。如果你省略这个参数,默认情况下组件的 props 返回 Object.assign({}, ownProps, stateProps, dispatchProps) 的结果

    • mergeProps(stateProps, dispatchProps, ownProps): props
  4. options

    • context?: Object
    • pure?: boolean
    • areStatesEqual?: Function
    • areOwnPropsEqual?: Function
    • areStatePropsEqual?: Function
    • areMergedPropsEqual?: Function
    • forwardRef?: boolean

下面 改写一下 App.js 中 redux 的用法

  • mapDispatchToProps 参数不传时
import React, { useEffect, useCallback, useState, useMemo } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { actionCreators } from "./store";

function App({ count, todos, dispatch }) {
  const [value, setValue] = useState("");

  // 生成包装后的 actionCreator,执行之后就会触发 store 数据的更新
  const { increment, decrement, getAsyncTodos, addTodo } = useMemo(
    () => bindActionCreators(actionCreators, dispatch),
    [dispatch]
  );

  // 初始化 TodoList
  useEffect(() => {
    getAsyncTodos();
  }, [getAsyncTodos]);

  const add = useCallback(() => {
    if (value) {
      // 分发 action
      addTodo(value);
      setValue("");
    }
  }, [value, addTodo]);

  return (
    <div className="App">
      <h1>Hello Redux</h1>
      <p>count: {count}</p>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
      <br />
      <br />
      <input
        placeholder="请输入待办事项"
        value={value}
        onChange={e => setValue(e.target.value)}
      />
      <button onClick={add}>add</button>
      <ul>
        {todos.map(todo => (
          <li key={todo}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}

// count、todos 也会被挂载到组件的 props 中
const mapStateToProps = ({ count, todos }) => ({ count, todos });

// 第二个参数没有传递,dispatch 默认会挂载到组件的 props 中
export default connect(mapStateToProps)(App);
  • mapDispatchToProps 参数为对象时
import React, { useEffect, useCallback, useState } from "react";
import { connect } from "react-redux";
import { actionCreators } from "./store";

function App({ count, todos, increment, decrement, getAsyncTodos, addTodo }) {
  const [value, setValue] = useState("");

  // 初始化 TodoList
  useEffect(() => {
    getAsyncTodos();
  }, [getAsyncTodos]);

  const add = useCallback(() => {
    if (value) {
      // 分发 action
      addTodo(value);
      setValue("");
    }
  }, [value, addTodo]);

  return (
    <div className="App">
      <h1>Hello Redux</h1>
      <p>count: {count}</p>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
      <br />
      <br />
      <input
        placeholder="请输入待办事项"
        value={value}
        onChange={e => setValue(e.target.value)}
      />
      <button onClick={add}>add</button>
      <ul>
        {todos.map(todo => (
          <li key={todo}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}

// count、todos 会被挂载到组件的 props 中
const mapStateToProps = ({ count, todos }) => ({ count, todos });

// actionCreators 中的 actionCreator 会被 dispatch 进行包装,之后合并到组建的 props 中去
const mapDispatchToProps = { ...actionCreators };

export default connect(mapStateToProps, mapDispatchToProps)(App);
  • mapDispatchToProps 参数为函数时
import React, { useEffect, useCallback, useState } from "react";
import { connect } from "react-redux";
import { actionCreators } from "./store";
import { bindActionCreators } from "redux";

function App({
  count,
  todos,
  increment,
  decrement,
  getAsyncTodos,
  addTodo,
  dispatch
}) {
  const [value, setValue] = useState("");

  // 初始化 TodoList
  useEffect(() => {
    getAsyncTodos();
  }, [getAsyncTodos]);

  const add = useCallback(() => {
    if (value) {
      // 分发 action
      addTodo(value);
      setValue("");
    }
  }, [value, addTodo]);

  return (
    <div className="App">
      <h1>Hello Redux</h1>
      <p>count: {count}</p>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
      <br />
      <br />
      <input
        placeholder="请输入待办事项"
        value={value}
        onChange={e => setValue(e.target.value)}
      />
      <button onClick={add}>add</button>
      <ul>
        {todos.map(todo => (
          <li key={todo}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}

// count、todos 会被挂载到组件的 props 中
const mapStateToProps = ({ count, todos }) => ({ count, todos });

// mapDispatchToProps 为函数时,actionCreators 中的 actionCreator 需要自己处理,返回的对象会被合并到组件的 props 中去
const mapDispatchToProps = dispatch => ({
  dispatch,
  ...bindActionCreators(actionCreators, dispatch)
});

export default connect(mapStateToProps, mapDispatchToProps)(App);
  • 如果不需要更新 store 中的数据,则不需要传 mapDispatchToProps 参数
  • 如果不需要自己控制 dispatch,则传递 ActionCreators 对象即可
  • 如果需要自己完全控制,则传递一个回调函数

虽然上面使用 connect 是在 class 组件中,但是在函数组件中依然适用。

useDispatchuseSelector 的使用

上面我们在组件中使用的是 connect,但是在现在这个 hooks 盛行的时代,怎么能只有高阶组件呢,所以下面我们来探究一下 useDispatchuseSelector 的用法。

改写 App.js 文件

import React, { useEffect, useCallback, useState, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { bindActionCreators } from "redux";
import { actionCreators } from "./store";

function App() {
  const [value, setValue] = useState("");

  // 从 useDispatch 中获取 dispatch
  const dispatch = useDispatch();

  // 生成包装后的 actionCreator,执行之后就会触发 store 数据的更新
  const { increment, decrement, getAsyncTodos, addTodo } = useMemo(
    () => bindActionCreators(actionCreators, dispatch),
    [dispatch]
  );

  // 通过 useSelector 获取需要用到 state 值
  const { count, todos } = useSelector(({ count, todos }) => ({
    count,
    todos
  }));

  // 初始化 TodoList
  useEffect(() => {
    getAsyncTodos();
  }, [getAsyncTodos]);

  const add = useCallback(() => {
    if (value) {
      // 分发 action
      addTodo(value);
      setValue("");
    }
  }, [value, addTodo]);

  return (
    <div className="App">
      <h1>Hello Redux</h1>
      <p>count: {count}</p>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
      <br />
      <br />
      <input
        placeholder="请输入待办事项"
        value={value}
        onChange={e => setValue(e.target.value)}
      />
      <button onClick={add}>add</button>
      <ul>
        {todos.map(todo => (
          <li key={todo}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

使用 hooks 方式改写之后,感觉简洁了不少,数据来源也很清晰。至于用 connect 还是 hook 的方式,可以根据情况自己选择。

总结

  • ReduxAction、Reducer、Store 组成

    • 通过 Reducer 创建 Store
    • 通过 store.dispatch(action) 触发更新函数 reducer
    • 通过 reducer 更新数据
    • 数据更新触发订阅 subscribe
  • 通过 combineReducers 可以合并多个 reducer

  • 通过 applyMiddleware 可以使用插件

  • 通过 bindActionCreators 可以将 ActionCreator 转化成 dispatch 包装后的 ActionCreator

源码地址

react-redux-demo

如果觉得有所帮助,欢迎 Star!