Redux简明上手指南——react-redux和Redux Toolkit两种写法

1,558 阅读8分钟

介绍

本文是一篇写给给刚熟悉react后想要快速上手redux的开发者们的文章,你需要简单了解react组件以及props等基本用法。本文的信息均源自于redux和Redux Toolkit的官方网站:

Redux

Redux Toolkit

本文主要包括:

  • Redux核心概念介绍
  • 仅使用react-redux构建应用
  • 使用Redux Toolkit构建应用(推荐)

Redux核心概念

redux中核心的概念包括:action、reducer以及store

动机

我们使用redux是为了更加方便地管理某些state,尤其是在多个组件同时依赖于同一个state时。Redux为我们提供了一个能够跨组件管理state的方式。

action

action代表一种将要修改state的动作,本质是一个有着type属性的Javascript对象。type属性值是一个字符串,作为此action的标识。 在action中可以包含一些数据信息,通常放在payload中,例如:

const addTodoAction = {
  type: 'todos/todoAdded',
  payload: 'Buy milk'
}

reducer

reducer是一个函数,接收两个参数当前的state和一个action,返回值为新的state:(state, action) => newState 实际对state的修改操作都是在reducer中发生的,但是注意若state为一个对象,绝对不能修改原来的state对象,而是要对原对象进行浅拷贝,然后操作这个新对象之后再返回。这一点十分关键,如果不遵守会产生难以预料的错误。但是如果使用Redux Toolkit的话就不必额外担心这一点,这也是为什么推荐使用Redux Toolkit来构建应用。

另外reducer应该是一个‘纯函数’,这意味着在reducer中不应该有任何的异步逻辑或者产生其他'副作用',只能够对state进行操作。

store

store是当前管理的state的容器,可以通过传入一个reducer进行创建。其身上有一个getState方法,可以获取当前管理的state的值。还有一个dispatch方法,通过传入一个action对象,然后经过reducer处理来修改state的值,这也是修改state的唯一方法。

使用react-redux构建应用

以官网的计数器的案例为例子。

counter.png 首先需要在项目src目录下新建文件夹命名为redux,然后在其中新建三个文件:actions.js、reducer.js和store.js

image.png

在actions.js中编写以下内容:

// actions.js

export const increment = 'INCREMENT'
export const decrement = 'DECREMENT'

/* 我们不希望手动创建
{
    type: 'INCREMENT', 
    payload:1
}
这样的action对象
一般都是通过一个函数生成这样的对象
这个函数称为action creator
*/
// 创建increment这个action的函数
export const incrementCreator = 
    (payload) => {type: increment, payload}
export const decrementCreator = 
    (payload) => {type: decrement, payload}

然后在reducer.js中编写以下内容

// reducer.js

import {increment, decrement} from './actions.js'

// state初始值
const initState = 0

// reducer函数第一个参数为旧的state,第二个参数为传入的action对象
// 默认暴露reducer函数
export default (state=initState, action) => {
    // 使用if 或者 switch语句判断action的type类型,然后操作state,返回新的state,注意不能直接修改原state对象!
    if (action.type === increment) {
        return state + 1
    }
    if (action.type === decrement) {
        return state - 1
    }
    return state
}

然后在store.js中编写以下内容

// store.js

import { createStore } from 'redux';
import reducer from './reducer.js'

// 使用createStore函数创建store,参数为刚刚定义的reducer
// 默认暴露创建好的store
export default createStore(reducer)


至此我们的store就已经准备好了,接下来就是在组件中使用了。下面的写法如果不能直接理解也没有关系,在实际使用过后就会有新的理解。

使用Provider组件传递store

在文档中不推荐直接引入store对象,而是要在根组件外使用Provider来传递store。

接下来进入/src/index.js编写以下内容:

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

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
      {// 注意看这里,使用props的方式将store传入Provider组件}
      <Provider store={store}>
        <App />
      </Provider>
  </React.StrictMode>
);

编写UI组件和数据组件

我们需要在编写UI组件之后再编写一个数据组件,最后的目的是数据组件当做UI组件的父组件,由数据组件来接收数据,然后将数据和修改数据的方法通过props的方式传给UI组件,这样写的好处是能够将UI和数据分离开。

UI组件放在/src/components文件夹下,数据组件放在/src/containers文件夹下。

image.png

在数据组件中编写以下内容,注意其中connect函数的用法:

import {connect} from 'react-redux'
import Counter from '../../components/counter'
import {increament, decreament} from '../../redux/actions'

// 由于不是以组件标签的形式调用UI组件,因此不能直接通过标签属性的方式传递props
// 需要通过两个函数mapStateToProps和mapDispatchToProps,分别将数据和修改数据的方法以props的方式传递给UI组件
// mapStateToProps传递数据,接收的参数为store中的state,返回一个对象
// 在这个对象中键为props中接收的键名,值为其属性值
const mapStateToProps = (state) => {
  return {count: state}
}
// mapDispatchToProps传递操作数据的方法,接收的参数为dispatch方法,用于触发action,同样返回一个对象
// 在这个对象中键为UI组件接收的方法名,值应为一个dispatch某个acion的函数
const mapDispatchToProps = (dispatch) => {
  return {
    jia: (num) => dispatch(increament(num)),
    jian: (num) => dispatch(decreament(num))
  }
  
}
// 在这里调用connect函数,首先传入刚才定义的两个函数,在返回的函数中传入UI组件
// 这样在UI组件中就能够通过props访问到store中维护的state以及操作state的方法了
export default connect(mapStateToProps, mapDispatchToProps)(Counter)

然后在UI组件中编写以下内容:

import React, {useState} from 'react'
export default function Counter(props) {
  // 使用ES6解构语法获取props中的数据和方法
  const {jia, jian, count} = props
  const [num, setNum] = useState(1)
  function add(num) {
    jia(num*1)
  }
  function sub(num) {
    jian(num*1)
  }
  function handleChange(e) {
    setNum(e.target.value)
  }
  return (
    <div>
      <h1>当前的数值是:{count}</h1>
      <select defaultValue="1" onChange={(e) => handleChange(e)}>
        <option value="1">1</option>
        <option value="2">2</option>
        <option value="3">3</option>
      </select>
      <button onClick={() => add(num)}>+</button>
      <button onClick={() => sub(num)}>-</button>
    </div>
  )
}

最后别忘记在APP组件中引入编写好的数据组件,千万不要错误地引入UI组件,因为我们是通过数据组件来调用UI组件的。

// App.js

// 注意是container下的counter!
import Counter from './container/counter'
import './App.css';

function App() {
  return (
    <div className="App">
       <Counter/>
    </div>
  );
}

export default App;


至此我们的计数器案例应该可以正常使用了,但是使用Redux Toolkit我们可以更加轻松和优雅地来编写我们的代码,这也是官方推荐的用法。

使用Redux Toolkit构建应用

使用Redux Toolkit可以在定义Redux时让我们免去许多繁琐的步骤,而UI组件和数据组件则完全不需要修改。

让我们重新进入/src/redux文件夹。

首先是actions.js:

import {createAction} from '@reduxjs/toolkit'

export const increament = createAction('INCREAMENT')
export const decreament = createAction('DECREAMENT')


在这里我们使用createAction函数来创建action creator,而不需要我们手动地编写工厂函数。此外我们也不需要定义action的type对应的字符串,比如

export const increment = 'INCREMENT'

这句话看起来总是很多余。因此通过createAction函数创建的action creator函数的toString方法已经被修改了,你可以调用increment.toString()来获取标识字符串。同时你也可以通过increament.type来访问action的标识字符串,这在接下来的reducer的编写中提供了很大的方便。

接下来是reducers.js:

import { createReducer } from "@reduxjs/toolkit";
import { increament, decreament } from "./actions";

// createReducer方法为我们自动创建reducer函数
// 只需要我们传入两个参数:
// 第一个参数为state的初始值
// 第二个参数为一个对象,其键为所要处理的action的标识字符串,值为回调函数
// 在回调函数中接收两个参数state——上次的状态以及action——传入的action对象
// 可以通过传入的action对象实现获取payload的数据
export const counter = createReducer(0, {
  // 这里使用ES6语法来获取action的标识字符串
  // 在[]中的内容将会作为字符串被解析,若不是字符串则会调用其toString方法
  [increament]: (state, action) => state + action.payload,
  [decreament]: (state, action) => state - action.payload
});

// 官网中还有以下这种写法,不过我个人认为不如第一种写法简洁
// export const counter = createReducer(0, (builder) => {
//   builder
//     .addCase(increament.toString(), (state, action) => state + action.payload)
//     .addCase(decreament.toString(), (state, action) => state - action.payload)
// })

在这里主要的区别就是我们从手动创建reducer函数变为调用createReducer来创建reducer,因此要注意这个API的使用方法。

另外我们不需要担心对state的操作会破坏数据不可变性。因为在底层redux toolkit已经调用了immer库来保证数据的不可变性。因此这也是为什么推荐使用redux toolkit的原因之一。

最后是store.js:

import {configureStore} from '@reduxjs/toolkit'
import {counter} from './reducers'

export default configureStore({
  reducer: counter
})


可以看到与原来几乎没有变化,唯一的区别就是把createStore函数换成了configureStore函数,把reducer作为参数对象的一项属性传入进去。

其余的部分不需要更改,现在我们应该可以正常访问计数器案例了。你也可以按照这个框架来构建你自己的redux应用。

总结

  1. 理解redux的构成要素:action、reducer和store。action本质是一个有type属性的对象,一般通过action creator工厂函数创建,reducer本质是一个函数,在其中包含处理不同action的逻辑,返回值为新的state的值,注意不能够直接修改上一次的state除非你使用redux toolkit的API。store是包含着我们维护的state的容器,外部组件通过store来获取当前的state。修改state只能通过dispatch某个action,然后进入reducer的逻辑中进行修改。
  2. 推荐使用Redux Toolkit进行构建应用,因为可以使用更加简洁的语法以及避免许多不必要的bug。
  3. 本文所讲的内容只是最基本的redux的使用方法,其余如slice reducer、异步处理以及中间件等内容请前往官方文档查询。