React/ReactNative 状态管理: redux 如何使用

2,567 阅读5分钟

本文正在参加「金石计划」

有同学反馈开发 ReactNative 应用时状态管理不是很明白,接下来几篇文章我们来对比下 React 及 ReactNative 状态管理常用的几种框架的使用和优缺点。

首先来看下 redux 怎么使用。

以下是使用 React 和 Redux 创建 todo list 的一般过程,完整代码见文章末尾:

  1. 安装和配置开发环境:

    1. 安装 Node.js 和 create-react-app 脚手架,用于快速创建 React 应用程序

      1. npx create-react-app playpage_ts -template typescript
    2. 安装 React 和 Redux 关联库 redux 和 react-redux

      1. npm install @reduxjs/toolkit react-redux
  2. 定义数据结构,这里我们假设 TODO 就是一个文本

export type TODO = {
    text: string
}

//1.定义状态数据
export type State = {
    todos: TODO[]
}
  1. 定义行为 action,比如添加、删除:
//2.定义行为

//action
export const ADD_TODO = 'ADD_TODO';
export const DELETE_TODO = 'DELETE_TODO';

//action creator
const ACTION_CREATOR_ADD_TODO = (text:string) => {
    return {type: ADD_TODO, payload: text}
};

const ACTION_CREATOR_DELETE_TODO = (text:string) => {
    return {type: DELETE_TODO, payload: text}
};

//分发数据变更操作,接收 store.dispatch
export function DISPATCH_ADD_TODO(dispatch: any) {
    return (text: string) => {
        dispatch(ACTION_CREATOR_ADD_TODO(text))
    }
}

export function DISPATCH_DELETE_TODO(dispatch: any) {
    return (text: string) => {
        dispatch(ACTION_CREATOR_DELETE_TODO(text))
    }
}

上面的代码里,首先定义了行为类型( action type):ADD_TODO 和 DELETE_TODO,它们用于唯一标识一个状态改变行为。

然后创建了两个 action creator :ACTION_CREATOR_ADD_TODO 和 ACTION_CREATOR_DELETE_TODO,它们用于创建符合 reducer 约定的 action 对象,其中 type 标识行为类型,payload 表示传递的数据。

最后创建了两个函数:DISPATCH_ADD_TODO 和 DISPATCH_DELETE_TODO,它们用于分发数据变更操作,简化后续 connect 里的代码。

  1. 然后创建行为处理函数 todoReducer:
import { State, TODO } from "./model";
import { ADD_TODO, DELETE_TODO } from "./todoActions";

const initState : State = {
    todos: [
        {
            text: "clean room"
        }
    ]
};

//3.创建行为处理函数
const todoReducer = (state: State = initState, action: any): State => {
    switch(action.type) {
        case ADD_TODO:
            //返回一个新的状态树
            return {
                todos: [...state.todos, {text: action.payload}]
            };
        case DELETE_TODO:
            console.log('todoReducer delete >>>' + action.payload)
            const todos = state.todos.filter((item: TODO, index: number)=> {
                return item.text != action.payload
            });
            return {
                todos
            }
        default:
            console.log('return default>>>' + JSON.stringify(state))
            return state;
    }
};

export default todoReducer;

todoReducer 的作用是根据行为,返回新的状态。

参数是先前的状态 state 和要执行的行为 action,根据 action type 行为类型,返回不同的数据。

需要注意的是,reducer 中不能修改老数据,只能新建一个数据。

  1. 创建一个 store,参数就是上面创建的行为处理函数:
import { createStore } from 'redux';
import todoReducer from './reducers';

//4.创建 store,提供全局的状态和行为处理
const store = createStore(
    todoReducer
);

//监听数据变化
store.subscribe( () => {
    console.log("store changed >>>" + JSON.stringify(store.getState()))
})

export default store;
import { createStore } from 'redux';
import rootReducer from './reducers';

const store = createStore(
  rootReducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

export default store;
  1. 分发给子元素:
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import ReduxTodoApp from './ReduxTodoApp';
import store from './redux/store';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

//5.分发给子元素
root.render(
  <Provider store={store}>
    <ReduxTodoApp/>
  </Provider>
);

上面的代码中,我们使用使用 react-redux 的 Provider 包围了 App 组件,这样整个 App 组件都可以获取到 Store 中的状态和行为处理函数。

ReduxTodoApp 是下一步要创建的 UI 组件

  1. 创建 UI 组件:
import {useState} from "react";
import { connect } from "react-redux";
import { State, TODO } from "./redux/model";
import {DISPATCH_ADD_TODO, DISPATCH_DELETE_TODO } from "./redux/todoActions";

//6.数据和 action 函数需要通过 prop 访问
function ReduxTodoApp (prop: {todos: TODO[], addTodo: any, deleteTodo: any}) {

    const {todos, addTodo, deleteTodo} = prop;
    const [text, setText] = useState('');

    const handleInput = (e: any) => {
        setText(e.target.value)
    }

    const handleAddTodo = () => {
        addTodo(text)
        setText('')
    }

    const handleDeleteTodo = (text: string) => {
        deleteTodo(text)
    }

    return (
        <div style={{display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center'}}>
            <h1>This Is Redux TODO App.</h1>
            <ul>
                {todos.map((todo: TODO, index: any) => {
                    return (
                        <li key={index}>
                            <span>{todo.text}</span>
                            <button style={{marginLeft: '12px'}} onClick={() => handleDeleteTodo(todo.text)}>finish</button>
                        </li>
                    )
                })}
            </ul>

            <div style={{display: 'flex', flexDirection: 'row'}}>
                <input value={text} onChange={handleInput}/>
                <button onClick={handleAddTodo}>Add Todo</button>
            </div>
        </div>
    )
}

//外部的数据(即state对象)如何转换为 UI 组件的参数
//mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。
const mapStateToProps = (state: State) => {
    console.log('mapStateToProps >>> ' + JSON.stringify(state))
    //返回的是一个对象,需要根据属性名访问
    return {
        todos: state.todos
    }   
}

//建立 UI 组件的参数到store.dispatch方法的映射
//定义了哪些用户的操作应该当作 Action,传给 Store
const mapDispatchToProps = (dispatch: any, ownProps: any) => {
    //返回一个 Prop 对象
    return {
        addTodo: DISPATCH_ADD_TODO(dispatch),
        deleteTodo: DISPATCH_DELETE_TODO(dispatch)
    }
};

//5.组件接收数据和分发行为
export default connect(mapStateToProps, mapDispatchToProps) (ReduxTodoApp);

上面的代码中,使用 connect 包装了 UI 组件。

connect 的第一个参数 mapStateToProps 用于返回当前 UI 组件需要的数据,这里我们获取到 Store 中的 todos 列表。

第二个参数 mapDispatchToProps 用于返回当前 UI 组件需要向外分发的状态操作行为,这里我们需要分发两个行为:添加 todo 和删除 todo,通过调用第二步中创建的 DISPATCH_ADD_TODO 和 DISPATCH_DELETE_TODO 实现。

正如名称所示,mapStateToProps 和 mapDispatchToProps,最终都会向 Props 里添加成员。

这样,我们的 UI 组件的 props 就会包含 mapStateToProps 中返回的状态与 mapDispatchToProps 中的函数,也就是这样:

{
  todos: TODO[], 
  addTodo: any, 
  deleteTodo: any
}

注意名称需要一致。

然后我们就可以在 UI 组件中通过 prop.todos 获取数据,通过 prop.addTodo 或者 prop.deleteTodo 通知修改数据。

总结一下,通过最原始的 redux 管理状态分这几步:

  1. 定义数据结构类型,也就是前面的 State

  2. 定义要进行的数据修改行为 (action type),也就是前面的 ADD_TODO 和 DELETE_TODO

    1. 然后创建 action creator,创建 reducer 里需要的 action 对象
    2. 然后创建调用 store.dispatch 的函数,简化 mapDispatchToProps 的代码
  3. 有了行为后,然后就是处理行为,也就是 reducer

    1. 在其中根据 action type,返回不同的状态
  4. 有了 reducer 后,store 就齐全了,可以通过 createStore 创建一个全局唯一的 store

  5. 通过 react-redux 的 Provider 包裹整个 app 组件,把 store 分发给所有组件

  6. 最重要的一步:在 UI 组件里获取数据和分发行为

    1. 使用 react-redux 的 connect 包裹 UI 组件

      1. connect 的第一个参数返回一个对象,在其中获取 UI 组件里需要的数据
      2. 第二个参数返回一个对象,其中包括要向外分发的行为
    2. 在 UI 组件里通过 props.xxx 的方式获取数据和分发行为

步骤有些繁琐,但重要的是,行为和行为处理都拆分开,及时业务变得复杂,后续拓展也比较轻松。

如果要分析某个状态修改操作,在 reducer 里增加日志即可定位到,这就是 redux 宣称的优势:“可追溯”。

完整代码:github.com/shixinzhang…