当前位置: 首页 > article >正文

【更新中】【React】基础版React + Redux实现教程,自定义redux库和react-redux库

本项目是一个在react中,使用 redux 管理状态的基础版实现教程,用简单的案例练习redux的使用,旨在帮助学习 redux 的状态管理机制,包括 storeactionreducerdispatch 等核心概念。

项目地址:https://github.com/YvetW/redux-mini-demo

注:项目中包含多个版本的代码,有助于理解redux,运行前请先选择需要的版本,修改目录名为src。

在这里插入图片描述

版本:

  • React v19
  • Redux v5
  • React-Redux v9
  • TypeScript
  • Vite

1. 初始化项目

使用 Vite 创建 React+TypeScript 项目:

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install

主要目录结构

my-app/
├── src/
│   ├── redux/
│   │   ├── action-types.ts  # 定义 Action Types
│   │   ├── actions.ts       # 定义 Actions
│   │   ├── reducers.ts      # 定义 Reducers
│   │   ├── store.ts         # 创建 Store
│   ├── App.tsx              # 组件主入口
│   ├── main.tsx             # 入口文件,渲染 App
│   ├── index.css            # 样式文件
├── package.json             # 依赖管理
├── tsconfig.json            # TypeScript 配置
└── vite.config.ts           # Vite 配置

2. 在 React 组件中使用 Redux

本版本不使用 react-redux@reduxjs/toolkit,而是直接使用 Redux 的原生 createStore API(已过时,但有助于理解 Redux 的核心概念)。

2.1 创建 Action Types (action-types.ts)

  • 在 Redux 中,所有的 action 都应有一个唯一的 type,定义 type 常量。
  • 例如:INCREMENTDECREMENTADD_MESSAGE
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
export const ADD_MESSAGE = 'add_message'

2.2 创建 Reducers (reducers.ts)

  • Redux 需要使用 reducers 函数处理 state 变化。
  • 创建两个 reducercountmessages
  • 根据不同 action 类型更新 state,返回新的 state
import {combineReducers} from 'redux';
import {ADD_MESSAGE, DECREMENT, INCREMENT} from './action-types.ts';
import {Action} from './actions.ts';

// 管理count
const initCount: number = 0;

function count(state: number = initCount, action: Action): number {
    console.log('count', state, action);
    switch (action.type) {
        case INCREMENT:
            return state + action.data;
        case DECREMENT:
            return state - action.data;
        default:
            return state;
    }
}

// 管理messages
const initMessages: string[] = [];

function messages(state: string[] = initMessages, action: Action): string[] {
    console.log('messages', state, action);
    switch (action.type) {
        case ADD_MESSAGE:
            return [action.data, ...state]
        default:
            return state;
    }
}

export default combineReducers({count, messages});

// 整体state状态结构: {count: 2, messages: ['xx', 'xxx']}

2.3 创建 Actions (actions.ts)

  • Actions 是 Redux store 唯一的数据来源。
  • 创建 incrementdecrementaddMessage action。
import {INCREMENT, DECREMENT, ADD_MESSAGE} from './action-types.ts';

// 定义 CountAction 的类型
interface CountAction {
    type: typeof INCREMENT | typeof DECREMENT;
    data: number;
}

// 定义 MessageAction 的类型
interface MessageAction {
    type: typeof ADD_MESSAGE;  // 只有 ADD_MESSAGE 类型
    data: string;  // data 是一个字符串
}

// 联合类型
export type Action = CountAction | MessageAction;

export const increment = (number: number): CountAction => ({type: INCREMENT, data: number});

export const decrement = (number: number): CountAction => ({type: DECREMENT, data: number});

export const add_message = (message: string): MessageAction => ({type: ADD_MESSAGE, data: message});

2.4 创建 Store (store.ts)

  • 通过 createStore(rootReducer) 创建 Redux store,集中管理应用状态。
import { createStore } from 'redux';
import reducers from './reducers'; // 包含多个reducer的reducer

export default createStore(reducers)

2.5 App.tsx 中使用 store

  • 通过 props.store.getState() 获取 state,控制组件渲染。
  • 通过 props.store.dispatch(action) 触发状态更新。
import {useState} from 'react';
import {Store} from 'redux';
import {Action, increment, decrement, add_message} from './redux/actions.ts';

// 为 App 组件定义一个 Props 类型,这个类型包含 store
interface AppProps {
    store: Store<{
        count: number;
        messages: string[];
    }, Action>;
}

function App({store}: AppProps) {
    const [selectedValue, setSelectedValue] = useState<number>(1);
    const [message, setMessage] = useState<string>('');

    const count = store.getState().count;
    const messages = store.getState().messages;

    function handleIncrement() {
        store.dispatch(increment(selectedValue));
    }

    function handleDecrement() {
        store.dispatch(decrement(selectedValue));
    }

    // 奇数增加
    function handleIncrementIfOdd() {
        // setCount 是异步的,如果 Redux 状态更新了,count 可能不会立即反映出来,直接从 store.getState() 读取最新的 count
        if (store.getState().count % 2 === 1) {
            store.dispatch(increment(selectedValue));
        }
    }

    // 异步增加
    function handleIncrementIfAsync() {
        setTimeout(() => {
            store.dispatch(increment(selectedValue));
        }, 1000);
    }

    function handleAdd() {
        if (message.trim()) {
            store.dispatch(add_message(message));
            setMessage('');
        }
    }

    return (
        <div className="App">
            <div className="count">
                click <span>{count}</span> times
            </div>
            <select
                id="number" value={selectedValue}
                // <option> 的 value 默认是字符串类型(即使它是一个数字),这里强制转换为数字
                onChange={event => setSelectedValue(Number(event.target.value))}
            >
                {[1, 2, 3, 4, 5].map((item) => (
                    <option value={item} key={item}>{item}</option>
                ))}
            </select>
            <button onClick={handleIncrement}>+</button>
            <button onClick={handleDecrement}>-</button>
            <button onClick={handleIncrementIfOdd}>increment if odd</button>
            <button onClick={handleIncrementIfAsync}>increment async</button>
            <hr />
            <input
                type="text"
                value={message}
                onChange={event => setMessage(event.target.value)}
            />
            <button onClick={handleAdd}>add text</button>
            <ul>
                {
                    messages.map((msg, index) => (
                        <li key={index}>{msg}</li>
                    ))
                }
            </ul>
        </div>
    );
}

export default App;

2.6 main.tsx 中订阅 Store 并触发渲染

  • createRoot() 挂载 App 组件。
  • 通过 store.subscribe() 监听 state 变化,每次 dispatch 触发 state 变更时重新渲染 App
  • React 18 及以上使用 createRoot() 进行渲染,注意避免重复 createRoot() 调用。
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import './index.scss';
import App from './App';
import store from './redux/store.ts';

const root = createRoot(document.getElementById('root')!); // 只调用一次 createRoot()

// root.render() 负责后续更新
function render() {
    root.render(
        <StrictMode>
            <App store={store} />
        </StrictMode>,
    );
}

// 初次渲染
render();

// 订阅 store,state 变化时触发 render
store.subscribe(render);

3. 自定义 redux 库

自己定义一个简易的 redux 库,主要针对 createStore 和 combineReducers 的核心概念。

简单来说,createStore 用来创建存储和管理应用状态的对象,而 combineReducers 用来将多个子 reducer 合并成一个单一的 reducer。

注:完整版请看项目代码。

3.1 createStore

  • 作用:用于创建一个 Redux Store,负责管理应用的状态。
  • 接收的参数:
    • reducer:根 reducer,决定如何根据 action 更新 state
    • 可选的 preloadedState:初始化时的状态。
  • 返回的store对象包含:
    • dispatch:分发action,会触发reducer调用,返回一个新的state,调用所有绑定的listener
    • getState:得到内部管理的state对象
    • subscribe:监听 state 的变化,并在状态变化时触发回调。
export function createStore<S, A extends Action>(rootReducer: Reducer<S, A>): Store<S> {
    // 内部state,第一次调用reducer得到初始状态并保存
    let state: S;
    // 内部保存n个listener的数组
    const listeners: (() => void)[] = [];

    // 初始调用reducer得到初始state值
    state = rootReducer(undefined, {type: '@mini-redux'} as A);

    // 得到内部管理的state对象
    function getState() {
        return state;
    }

    // 分发action,会触发reducer调用,返回一个新的state,调用所有绑定的listener
    function dispatch(action: Action) {
        // 调用reducer,得到一个新的state,保存
        state = rootReducer(state, action as A);
        // 调用listeners中所有的监视回调函数
        listeners.forEach(listener => listener());
    }

    // 订阅state变化,产生新的状态,也就是dispatch时才执行
    function subscribe(listener: () => void) {
        listeners.push(listener); // 将listener加入到订阅列表

        // 返回一个取消订阅的函数(main.ts 通常不需要手动取消,只执行一次,store.subscribe(render) 只执行一次,不会不断创建新的 listener,所以 不会造成内存泄漏。)
        return () => {
            const index = listeners.indexOf(listener);
            if (index >= 0) {
                listeners.splice(index, 1);
            }
        };
    }

    // 返回一个store对象
    return {getState, dispatch, subscribe};
}

3.2 combineReducers

  • 作用:将多个子 reducer 组合成一个根 reducer。
  • 接收的参数:
    • reducers:一个对象,其中包含多个子 reducer,每个子 reducer 负责管理 state 的一部分。
  • 这个函数返回一个新的 reducer,能够根据 action 调用每个子 reducer,更新对应的 state 部分。
// 接收一个包含多个reducer函数的对象,返回一个新的reducer函数
export function combineReducers<S, A extends Action>(
    reducers: { [K in keyof S]: Reducer<S[K], A> }
) {
    return function (state: Partial<S> = {} as S, action: A): S { // 这个 rootReducer 函数会传给 createStore()
        return Object.keys(reducers).reduce((newState, key) => {
            // 确保类型安全:key 是 S 类型的有效键
            newState[key as keyof S] = reducers[key as keyof S](state[key as keyof S], action)
            return newState
        }, {} as S)
    };
}


4. react-redux

react-redux 是一个官方推荐的 React 库,它将 Redux 状态管理与 React 组件结合起来,帮助你更方便地在 React 应用中使用 Redux。它主要提供了两个核心的 API:Providerconnect。我们可以通过这些 API 将 Redux 的功能与 React 组件连接,轻松地将状态存储、分发操作等功能集成到 React 中。

1. Provider

Provider 是一个 React 组件,它的作用是将 Redux store 传递给整个应用。它让组件树中的所有组件都能访问到 Redux store,因此,Provider 通常包裹在应用的最外层。

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'; // 导入 Provider
import store from './store'; // 导入 store
import App from './App';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
  • store 是你通过 createStore 创建的 Redux store。
  • Provider 会将 Redux store 传递给组件树中的所有组件。

2. connect

connect 是一个高阶组件,它让你能够将 Redux 的 statedispatch 方法连接到 React 组件中,进而让组件能够访问 Redux store 中的状态,并向 store 发送 action。

connect 语法
import { connect } from 'react-redux';

connect 函数接受两个参数:

  • mapStateToProps:一个函数,用来将 Redux store 的状态映射到组件的 props 上。
  • mapDispatchToProps:一个函数,用来将 Redux 的 dispatch 方法映射到组件的 props 上,使得组件能够派发 action。
使用 connect
import React from 'react';
import { connect } from 'react-redux'; // 导入 connect

// 一个简单的 React 组件
const Counter = ({ count, increment }) => {
  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

// mapStateToProps:将 Redux store 中的状态映射为组件的 props
const mapStateToProps = (state) => ({
  count: state.count,
});

// mapDispatchToProps:将 Redux 的 dispatch 方法映射为组件的 props
const mapDispatchToProps = (dispatch) => ({
  increment: () => dispatch({ type: 'INCREMENT' }),
});

// 使用 connect 连接 Redux 状态和组件
export default connect(mapStateToProps, mapDispatchToProps)(Counter);

3. 核心概念

  • mapStateToProps:从 Redux 的 store 中提取你需要的部分数据,并将它们映射到组件的 props 中。这样,组件就能通过 props 访问 Redux 状态。

  • mapDispatchToProps:将 Redux 的 dispatch 方法映射到组件的 props,让组件能够触发 Redux 中的 action。你可以将 dispatchaction 包装在一起,简化代码。

  • connect:是一个高阶组件(HOC),通过将 mapStateToPropsmapDispatchToProps 传递给它,将 Redux 和 React 组件连接起来。通过 connect,React 组件就能接收到 Redux 的状态和分发 action 的能力。

4. 如何工作

  • 当应用的状态变化时,mapStateToProps 会重新运行,确保组件的 props 始终是最新的。
  • mapDispatchToProps 将会将 dispatchaction 绑定到组件的 props 中,让组件能够触发 action,从而更新状态。

5. useSelectoruseDispatch Hook(React-Redux v7.1+)

在 React-Redux v7.1 及以上版本中,你还可以使用 React Hooks 来代替 connect,使得代码更加简洁:

  • useSelector:让你从 Redux store 中获取状态数据。
  • useDispatch:让你获取 dispatch 方法并用于派发 action
示例:
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';

const Counter = () => {
  const count = useSelector((state) => state.count); // 从 Redux 中获取 count 状态
  const dispatch = useDispatch(); // 获取 dispatch 方法

  const increment = () => {
    dispatch({ type: 'INCREMENT' }); // 派发 action 更新 count
  };

  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;

结论

本项目通过最基础的 Redux 实现,帮助理解 Redux 的核心概念,包括 storeactionreducerdispatch,同时避免 react-redux 的复杂性,适合初学者学习 Redux 的工作原理。

如果要在实际项目中使用 Redux,建议使用 @reduxjs/toolkitreact-redux,可以大幅减少 Redux 代码量,提高开发效率。

后续会继续编写使用 @reduxjs/toolkitreact-redux 的教程,欢迎点赞收藏关注!感恩比心~


http://www.kler.cn/a/610969.html

相关文章:

  • fircrawl本地部署
  • 【2025】基于python+django的个性化阅读推荐系统设计与实现(源码、万字文档、图文修改、调试答疑)
  • 从0到1,用Tableau讲好数据故事
  • 用python写网络爬虫
  • 可以把后端的api理解为一个目录地址,但并不准确
  • 2024年SEVC SCI1区TOP:多策略灰狼算法MSGWO,深度解析+性能实测
  • 在 CentOS 系统中开机自动执行 Shell 脚本
  • Windows命令提示符(CMD) 的常用命令分类整理
  • SSL/TLS加密
  • jsBridge在vue中使用
  • 前端数据模拟利器 Mock.js 深度解析
  • Flutter使用自签证书打包ipa
  • 【身份证证件OCR识别】批量OCR识别身份证照片复印件图片里的文字信息保存表格或改名字,基于QT和腾讯云api_ocr的实现方式
  • 破局AI落地困局 亚信科技“四位一体手术刀“切开产业智能三重枷锁
  • Android Kotlin 实用扩展函数(持续更新)
  • 分布式锁实战:Redis与Redisson的深度解析
  • python之size,count的区别
  • 加载dll插件自动提示文字信息——cad c#二次开发
  • PyTorch DDP快速上手附代码
  • 【大模型开发】将vocab解码