一文读懂Redux,state、Action、Reducers、store的应用

State

案例:计数器

import React, { useState } from 'react';
import './style.css';

export default function App() {
  return (
    <div>
      <Counter />
    </div>
  );
}

function Counter() {
  // State:计数器值
  const [couter, setCounter] = useState(0);

  // Action:发生某些事情时导致状态更新的代码
  const increment = () => {
    setCounter((prevCount) => prevCount + 1);
  };

  // View:定义UI
  return (
    <div>
      Value: {couter} <button onClick={increment}>Increment</button>
    </div>
  );
}

上面代码具有以下部分:

  • state:驱动应用程序的来源
  • view:基于当前状态的UI声明性描述
  • actions:基于用户交互在应用中发生的事件,并触发状态更新

这个例子是一个单向数据流

  • state 描述了应用程序在特定时间点的状况;
  • 基于 state 来渲染UI;
  • 执行事件时, state 会根据发生的事情进行更新;
  • 基于新的 state 重新渲染UI

在这里插入图片描述
有多个组件需要共享和使用相同state时,情况会变得复杂。可以通过提升 state 到父组件来解决这个问题。

也可以通过**从组件中提取共享 state ** 来解决,并将其放入组件树之外的一个集中位置。这样使得组件树成为一个大的”view“,任何组件都可以访问state或出发action,无论在什么位置。

通过定义和分离 state 管理中涉及的概念并强制执行维护 view 和 state 之间独立性的规则,代码变得更结构化和易于维护。


这就是 Redux 背后的基本思想:应用中使用集中式的全局状态来管理,并明确更新状态的模式,以便让代码具有可预测性。

不可变性 Immutablility

JavaScript 的对象(object)和数组(array)默认都是 mutable(可变)的,即可以任意更改内容。

const obj = { a: 1, b: 2 }
// 引用不变,内容变化
obj.b = 3

const arr = ['a', 'b']
// 引用不变,内容变化
arr.push('c')
arr[1] = 'd'

如果想要不可变的方式来更新,代码必需先 复制 原来的 object/array,然后更新它的复制体。

js的展开运算符可以实现这个目的:

const obj = {
  a: {
    // 为了安全的更新 obj.a.c,需要先复制一份
    c: 3
  },
  b: 2
}

const obj2 = {
  // obj 的备份
  ...obj,
  // 覆盖 a
  a: {
    // obj.a 的备份
    ...obj.a,
    // 覆盖 c
    c: 42
  }
}

const arr = ['a', 'b']
// 创建 arr 的备份,并把 c 拼接到最后。
const arr2 = arr.concat('c')

// 或者,可以对原来的数组创建复制体
const arr3 = arr.slice()
// 修改复制体
arr3.push('c')

Redux 期望所有状态更新都是使用不可变的方式。

Actions

action 是一个具有type字段的普通 JavaScript 对象。可以将其视为描述应用程序中发生了什么事件。

type字段是一个字符串,给这个 action 一个描述性的名字,比如"todos/todoAdded"。通常将其写成**“域/事件名称”** :

  • action 所属的特征或类别
  • 发生的具体事情

action 对象具有其他字段,其中包含有关事件的附加信息,通常将其放在payload字段中。

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

Reducers

reducer 是一个函数,接收当前的state 和一个 action 对象,决定如何更新状态,并返回新的状态。

(state, action) => newState

可以将 reducer 视为一个事件监听器,根据接收到的action类型处理事件。

Reducer 规则:

  • 仅使用 stateaction 参数计算新的状态值
  • 禁止直接修改 state。必须通过复制现有的 state 并对复制的值进行更改的方式来做 不可变更新(immutable updates)。
  • 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码

Reducer函数内部逻辑步骤:

  • 检查 reducer 是否关心这个 action
    • 如果是,则复制 state,使用新值更新 state 副本,然后返回新 state;
  • 否则,返回原来的state不变。

例子:

const initialState = { value: 0 };

function conterReducer(state = initialState, action) {
  // 检查 reducer 是否关心这个 action
  if (action.type === 'conter/increment') {
    // 如果是,复制 state
    return {
      ...state,
      // 使用新值更新 state 副本
      value: state.value + 1,
    };
  }
  // 返回原来的 state 不变
  return state;
}

Store

当前 Redux 应用的状态存在于 store 对象中。

store 由通过传入一个 reducer 来创建,且有一个名为getState的方法,返回当前的状态值;

import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

console.log(store.getState())
// {value: 0}

Store 内部

store 实现的简化示例

function createStore(reducer, preloadedState) {
  let state = preloadedState
  const listeners = []

  function getState() {
    return state
  }

  function subscribe(listener) {
    listeners.push(listener)
    return function unsubscribe() {
      const index = listeners.indexOf(listener)
      listeners.splice(index, 1)
    }
  }

  function dispatch(action) {
    state = reducer(state, action)
    listeners.forEach(listener => listener())
  }

  dispatch({ type: '@@redux/INIT' })

  return { dispatch, subscribe, getState }
}

Redux store 汇集了构成应用程序的 state、actions 和 reducers。
store 有以下几个职责:

  • 在内部保存当前应用程序 state
  • 通过 store.getState() 访问当前 state;
  • 通过 store.dispatch(action) 更新状态;
  • 通过 store.subscribe(listener) 注册监听器回调;
  • 通过 store.subscribe(listener) 返回的 unsubscribe 函数注销监听器。

注意 Redux 应用程序中只有一个 store

Dispatch

Redux store 有一个dispatch方法。

更新 state 的唯一方法是调用 store.dispatch() 并传入一个 action 对象。

store 将执行所有 reducer 函数并计算出更新后的 state,调用 getState() 可以获取新 state。

store.dispatch({ type: 'counter/incremented' })

console.log(store.getState())
// {value: 1}

dispatch 一个 action 即"触发一个事件"。

发生了一些事情,我们希望 store 知道这件事。 Reducer 就像事件监听器一样,当它们收到关注的 action 后,它就会更新 state 作为响应。

Selectors

Selector 函数可以从 store 状态树中提取指定的片段。随着应用变得越来越大,会遇到应用程序的不同部分需要读取相同的数据,selector 可以避免重复这样的读取逻辑:

const selectCounterValue = state => state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2

案例:TodoList应用

一、需求

  • UI 应包括三个主要部分:
    • 一个输入框,让用户输入新待办事项的文本
    • 所有现有待办事项的列表
    • 页脚部分,显示未完成的待办事项数量,并显示过滤选项
  • 待办事项列表项应该有一个复选框来切换“完成”状态。还应该能够为预定义的颜色列表添加颜色编码的类别标签,并删除待办事项。
  • 计数器应该复数活动待办事项的数量:“0 items”、“1 items”、“3 items”等
  • 应该有按钮将所有待办事项标记为已完成,并通过删除它们来清除所有已完成的待办事项
  • 应该有两种方法可以过滤列表中显示的待办事项:
    • 待办事项可基于 All 、 Active 和 Completed 进行过滤
    • 基于选择一种或多种颜色进行过滤,并显示标签与这些颜色匹配的任何待办事项

在这里插入图片描述

二、设计State

React 和 Redux 的核心原则之一是 UI 应该基于 state。

应用功能:

  • 当前待办事项的实际列表
  • 当前的过滤选项

对于每个待办事项所需存储的信息:

  • 用户输入的文本
  • 是否完成(布尔值)
  • 唯一的ID
  • 颜色类别(如果已选)

对于过滤行为:

  • 状态:“全部(All)”、“活动(Active)” 和 “已完成(Completed)”
  • 颜色:” Red “、“ Yellow ”、“ Green ”、“ Blue ”、“ Orange ”、“ Purple ”

State结构

待办事项数组:[
	{
		id: 唯一编号,
		text: 用户输入文本,
		completed:true|false 是否完成,
		color: 可选的颜色类别,
	}
]

过滤选项:{
	已完成:,
	颜色类别:[],
}
const todoAppState = {
  todos: [
    { id: 0, text: 'Learn React', completed: true },
    { id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
    { id: 2, text: 'Build something fun!', completed: false, color: 'blue' },
  ],

  filters: {
    status: 'Active',
    colors: ['red', 'blue'],
  },
};

三、设计Actions

操作事件:

  • 根据用户输入的文本添加新的待办事项条目
  • 切换待办事项的完成状态
  • 为待办事项选择颜色类别
  • 删除待办事项
  • 将所有待办事项标记为已完成
  • 清除所有已完成的待办事项
  • 选择不同的 “已完成” 过滤器值
  • 添加新的滤色器
  • 移除滤色器

基于以上操作事件构建actions列表:

  • {type: 'todos/todoAdded', payload: todoText}
  • {type: 'todos/todoToggled', payload: todoId}
  • {type: 'todos/colorSelected, payload: {todoId, color}}
  • {type: 'todos/todoDeleted', payload: todoId}
  • {type: 'todos/allCompleted'}
  • {type: 'todos/completedCleared'}
  • {type: 'filters/statusFilterChanged', payload: filterValue}
  • {type: 'filters/colorFilterChanged', payload: {color, changeType}}

四、编写Reducers

创建根Reducer

Redux 应用程序实际上只有一个 reducer 函数: 将“ root reducer ”传递给 createStore 函数。那个根 reducer 函数负责处理所有被 dispatching 的 actions,并计算每次所有的新 state 结果。

  1. 首先在src下创建reducer.js文件,创建模拟数据并编写逻辑:

    // 模拟数据
    const initialState = {
      todos: [
        { id: 0, text: 'Learn React', completed: true },
        { id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
        { id: 2, text: 'Build something fun!', completed: false, color: 'blue' },
      ],
    
      filters: {
        status: 'Active',
        colors: ['red', 'blue'],
      },
    };
    
    // 使用 initialState 作为默认值
    export default function appReducer(state = initialState, action) {
      // reducer 通常会根据 action type 字段来决定发生什么
      switch (action.type) {
        // 根据不同 type 的 action 在这里做一些事情
        default:
          // 如果这个 reducer 不关心这个 action type,会返回原本的state
          return state;
      }
    }
    
    
  2. 添加处理 'todos/todoAdded' action 的逻辑

    1. 根据当前 action 类型与字符串进行匹配
    2. 返回包含状态的新对象
    // 模拟数据
    const initialState = {
      todos: [
        { id: 0, text: 'Learn React', completed: true },
        { id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
        { id: 2, text: 'Build something fun!', completed: false, color: 'blue' },
      ],
    
      filters: {
        status: 'Active',
        colors: ['red', 'blue'],
      },
    };
    
    function nextTodoId(todos) {
      const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
      return maxId + 1
    }
    
    // 使用 initialState 作为默认值
    export default function appReducer(state = initialState, action) {
      // reducer 通常会根据 action type 字段来决定发生什么
      switch (action.type) {
        // 根据不同 type 的 action 在这里做一些事情
    
        case 'todos/todoAdded': {
          // 需要返回一个新的 state 对象
          return {
            // 具有所有现有 state 数据
            ...state,
            // 但有一个用于 `todos` 字段的新数组
            todos: [
              // 所有旧待办事项
              ...state.todos,
              // 新的对象
              {
                // 在此示例中使用自动递增的数字 ID
                id: nextTodoId(state.todos),
                text: action.payload,
                completed: false,
              },
            ],
          };
        }
    
        default:
          // 如果这个 reducer 不关心这个 action type,会返回原本的state
          return state;
      }
    }
    
  3. 处理其他Actions:

    // 模拟数据
    const initialState = {
      todos: [
        { id: 0, text: 'Learn React', completed: true },
        { id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
        { id: 2, text: 'Build something fun!', completed: false, color: 'blue' },
      ],
    
      filters: {
        status: 'Active',
        colors: ['red', 'blue'],
      },
    };
    
    function nextTodoId(todos) {
      const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
      return maxId + 1
    }
    
    export default function appReducer(state = initialState, action) {
      switch (action.type) {
        // 添加待办事项
        case 'todos/todoAdded': {
          return {
            ...state,
            todos: [
              ...state.todos,
              {
                id: nextTodoId(state.todos),
                text: action.payload,
                completed: false,
              },
            ],
          };
        }
    
        // 切换完成状态
        case 'todos/todoToggled': {
          return {
            ...state,
            todos: state.todos.map((todo) => {
              if (todo.id !== action.payload) {
                return todo;
              }
    
              return {
                ...todo,
                completed: !todo.completed,
              };
            }),
          };
        }
    
        // 修改过滤器
        case 'filters/statusFilterChanged': {
          return {
            ...state,
            filters: {
              ...state.filters,
              status: action.payload,
            },
          };
        }
        default:
          return state;
      }
    }
    
    

由于代码过长导致可读性很差,所以应该将reducer拆分成多个较小的reducer函数。

拆分Reducers

作为其中的一部分,Redux reducer 通常根据更新的 Redux state 部分进行拆分。我们的 todo 应用 state 当前有两个顶级部分:state.todosstate.filters。因此,可以将大的根 reducer 函数拆分为两个较小的 reducer - todosReducerfiltersReducer

  1. 在src下新建features文件夹,创建todos文件夹,新建todosSlice.js文件,写入与todo相关的代码:

    const initialState = [
      { id: 0, text: 'Learn React', completed: true },
      { id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
      { id: 2, text: 'Build something fun!', completed: false, color: 'blue' },
    ];
    
    function nextTodoId(todos) {
      const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1);
      return maxId + 1;
    }
    
    export default function todosReducer(state = initialState, action) {
      switch (action.type) {
        default:
          return state;
      }
    }
    
    

    写入与todo有关的操作:

    const initialState = [
      { id: 0, text: 'Learn React', completed: true },
      { id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
      { id: 2, text: 'Build something fun!', completed: false, color: 'blue' },
    ];
    
    function nextTodoId(todos) {
      const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1);
      return maxId + 1;
    }
    
    export default function todosReducer(state = initialState, action) {
      switch (action.type) {
        case 'todos/todoAdded': {
          return [
            ...state,
            {
              id: nextTodoId(state),
              text: action.payload,
              completed: false,
            },
          ];
        }
    
        case 'todos/todoToggled': {
          return state.map((todo) => {
            if (todo.id !== action.payload) {
              return todo;
            }
    
            return {
              ...todo,
              completed: !todo.completed,
            };
          });
        }
        default:
          return state;
      }
    }
    
    
  2. 创建src/features/filters/filtersSlices.js,写入与过滤器相关代码

    const initialState = {
      status: 'All',
      colors: [],
    };
    
    export default function filtersReducer(state = initialState, action) {
      switch (action.type) {
        case 'filters/statusFilterChanged': {
          return {
            ...state,
            status: action.payload,
          };
        }
        default:
          return state;
      }
    }
    
    
  3. 完整的reducer实现:
    todosSlice.js

    const initialState = [
      { id: 0, text: 'Learn React', completed: true },
      { id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
      { id: 2, text: 'Build something fun!', completed: false, color: 'blue' },
    ];
    
    function nextTodoId(todos) {
      const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1);
      return maxId + 1;
    }
    
    export default function todosReducer(state = initialState, action) {
      switch (action.type) {
        // 根据用户输入的文本添加新的待办事项条目
        case 'todos/todoAdded': {
          return [
            ...state,
            {
              id: nextTodoId(state),
              text: action.payload,
              completed: false,
            },
          ];
        }
    
        // 切换待办事项的完成状态
        case 'todos/todoToggled': {
          return state.map((todo) => {
            if (todo.id !== action.payload) {
              return todo;
            }
    
            return {
              ...todo,
              completed: !todo.completed,
            };
          });
        }
    
        // 为待办事项选择颜色类别
        case 'todos/colorSelected': {
          const [color, todoId] = action.payload;
          return state.map((todo) => {
            if (todo.id != todoId) {
              return todo;
            }
    
            return {
              ...todo,
              color,
            };
          });
        }
    
        // 删除待办事项
        case 'todos/todoDeleted': {
          return state.filter((todo) => todo.id !== action.payload);
        }
    
        // 将所有待办事项标记为已完成
        case 'todos/allCompleted': {
          return state.map((todo) => {
            return {
              ...todo,
              completed: true,
            };
          });
        }
    
        // 清除所有已完成的待办事项
        case 'todo/completedCleared': {
          return state.filter((todo) => !todo.completed);
        }
    
        default:
          return state;
      }
    }
    
    

    filtersSlice.js

    const initialState = {
      status: 'All',
      colors: [],
    };
    
    export default function filtersReducer(state = initialState, action) {
      switch (action.type) {
        // 选择不同的 “已完成” 过滤器值
        case 'filters/statusFilterChanged': {
          return {
            ...state,
            status: action.payload,
          };
        }
    
        // 修改滤色器
        case 'filters/colorFilterChanged': {
          let { color, changeType } = action.payload;
          const { colors } = state;
    
          switch (changeType) {
            // 添加新的滤色器
            case 'added': {
              if (colors.includes(color)) {
                return state;
              }
    
              return {
                ...state,
                colors: state.colors.concat(color),
              };
            }
    
            // 移除滤色器
            case 'removed': {
              return {
                ...state,
                colors: state.colors.filter(
                  (existingColor) => existingColor !== color
                ),
              };
            }
    
            default:
              return state;
          }
        }
    
        default:
          return state;
      }
    }
    
    

组合 Reducers

存储在创建时需要 一个 根 reducer 函数。那么,如何才能在不将所有代码放在一个大函数中的情况下重新使用根 redux 呢?

由于 reducers 是一个普通的 JS 函数,我们可以将 slice reducer 重新导入 reducer.js,并编写一个新的根 reducer,它唯一的工作就是调用其他两个函数。

import todosReducer from './features/todos/todosSlice';
import filtersReducer from './features/filters/filtersSlice';

export default function rootReducer(state = {}, action) {
  // 返回一个新的根 state 对象
  return {
    // `state.todos` 的值是 todos reducer 返回的值
    todos: todosReducer(state.todos, action),
    // 对于这两个reducer,我们只传入它们的状态 slice
    filters: filtersReducer(state.filters, action),
  };
}

这些 reducer 中的每一个都在管理自己的全局状态部分。每个 reducer 的 state 参数都是不同的,并且对应于它管理的 state 部分。

combineReducers

Redux 核心库包含一个名为 combineReducers 的实用程序,它为我们执行相同的样板步骤。我们可以用 combineReducers 生成的较短的 rootReducer 替换手写的 rootReducer

npm i redux
import { combineReducers } from 'redux';

import todosReducer from './features/todos/todosSlice';
import filtersReducer from './features/filters/filtersSlice';

const rootReducer = combineReducers({
  todos: todosReducer,
  filters: filtersReducer,
});

export default rootReducer;

五、创建store

Redux 核心库有一个 createStore API 可以创建 store。新建一个名为 store.js 的文件,并导入 createStore 和根 reducer。然后,调用 createStore 并传入根 reducer :

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

const store = createStore(rootReducer);

export default store;

加载初始 State

createStore的第二个参数为preloadedState,可以用于添加初始数据。例如包含在从服务器接收到的 HTML 页面中的值,或保存在 localStorage 中并在用户再次访问该页面时读回的值,如下所示:

import { createStore } from 'redux'
import rootReducer from './reducer'

let preloadedState
const persistedTodosString = localStorage.getItem('todos')

if (persistedTodosString) {
  preloadedState = {
    todos: JSON.parse(persistedTodosString)
  }
}

const store = createStore(rootReducer, preloadedState)
Dispatching Actions

对代码进行测试:
index.js

import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

import App from './App';
import store from './store';
// 打印 Initial state
console.log('Initial state: ', store.getState());
// {todos: [....], filters: {status, colors}}

// 每次状态变化时,记录一下
// 请注意,subscribe() 返回一个用于解绑侦听器的函数
const unsubscribe = store.subscribe(() =>
  console.log('State after dispatch: ', store.getState())
);

// dispatch 一些 actions

store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' });
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about reducers' });
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about stores' });

store.dispatch({ type: 'todos/todoToggled', payload: 0 });
store.dispatch({ type: 'todos/todoToggled', payload: 1 });

store.dispatch({ type: 'filters/statusFilterChanged', payload: 'Active' });

store.dispatch({
  type: 'filters/colorFilterChanged',
  payload: { color: 'red', changeType: 'added' },
});

// 停止监听 state 的更新
unsubscribe();

// 再 dispatch 一个 action,看看发生了什么

store.dispatch({ type: 'todos/todoAdded', payload: 'Try creating a store' });

const rootElement = document.getElementById('root');
const root = createRoot(rootElement);

root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZhShy23

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值