MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 与 Redux:构建大型应用的状态管理方案

2021-10-313.3k 阅读

React 状态管理基础

在 React 应用开发中,状态管理是至关重要的一环。React 组件自身具有状态(state),用于控制组件内部的数据变化以及触发重新渲染。例如,一个简单的计数器组件:

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  const increment = () => {
    setCount(count + 1);
  };
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;

在上述代码中,useState 钩子函数用于在函数式组件中添加状态。count 是当前状态值,setCount 是用于更新状态的函数。当点击按钮时,increment 函数调用 setCount 来更新 count 的值,从而触发组件重新渲染,界面上显示的计数也随之更新。

组件状态的局限性

然而,当应用规模逐渐增大,组件之间的状态传递和共享变得复杂起来。React 遵循单向数据流原则,父组件通过 props 将数据传递给子组件。但对于非父子关系组件或者多层嵌套组件间的状态共享,单纯使用 props 传递会变得繁琐且难以维护。

比如,假设有一个多层嵌套的组件结构:App -> Parent -> Child -> GrandChild。如果 GrandChild 组件需要获取或更新 App 组件的某个状态,按照常规的 props 传递方式,需要逐层传递,这样不仅增加了代码冗余,还使得组件间耦合度变高。

Redux 简介

Redux 是为了解决 React 应用中状态管理问题而诞生的一个可预测状态容器。它提供了一种集中式管理应用状态的方式,使得状态的变化变得可预测且易于维护。

Redux 的核心概念

  1. Store:整个应用的状态都存储在一个单一的对象树中,这个对象树保存在一个叫做 Store 的地方。Store 包含了应用的所有状态,并且提供了获取状态(getState() 方法)以及更新状态(dispatch(action) 方法)的能力。
  2. Action:是一个普通的 JavaScript 对象,用于描述发生了什么。它必须包含一个 type 属性,用于标识动作的类型,其他属性可以根据具体需求定义,用于携带数据。例如:
const ADD_TODO = 'ADD_TODO';
const addTodoAction = {
  type: ADD_TODO,
  payload: 'Learn Redux'
};
  1. Reducer:是一个纯函数,它接收当前状态和一个 action 作为参数,根据 action 的类型来决定如何更新状态,并返回新的状态。例如:
const initialState = {
  todos: []
};

const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TODO:
      return {
      ...state,
        todos: [...state.todos, action.payload]
      };
    default:
      return state;
  }
};

在上述 todoReducer 中,当接收到 ADD_TODO 类型的 action 时,它会将新的待办事项添加到 todos 数组中,并返回更新后的状态。如果是其他类型的 action,则直接返回当前状态。

将 Redux 与 React 集成

要在 React 应用中使用 Redux,需要借助 react - redux 库。它提供了一些重要的工具,帮助我们将 Redux 的状态和 action 与 React 组件连接起来。

Provider 组件

Provider 组件是 react - redux 库中非常关键的一个组件。它将 Redux 的 Store 传递给整个 React 应用,使得应用中的所有组件都能够访问到 Store 中的状态。使用方式如下:

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

ReactDOM.render(
  <Provider store = {store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

在上述代码中,Provider 组件包裹了整个 App 组件,并将 store 作为 props 传递进去。这样,App 及其所有子组件都可以通过 react - redux 提供的方法来访问和更新 Store 中的状态。

connect 函数(旧方式)

在较旧的 react - redux 版本中,使用 connect 函数来连接 React 组件与 Redux Store。connect 函数接收两个参数:mapStateToPropsmapDispatchToProps

  1. mapStateToProps:是一个函数,它接收 Redux Store 的状态作为参数,并返回一个对象,这个对象中的属性会作为 props 传递给连接的 React 组件。例如:
const mapStateToProps = state => {
  return {
    todos: state.todos
  };
};

上述代码中,mapStateToProps 函数从 Store 的状态中提取出 todos 数组,并将其作为 todos props 传递给组件。 2. mapDispatchToProps:也是一个函数,它接收 dispatch 函数作为参数,并返回一个对象,这个对象中的属性是一些函数,这些函数内部通过调用 dispatch 来触发 action。例如:

import { ADD_TODO } from './actionTypes';

const mapDispatchToProps = dispatch => {
  return {
    addTodo: (text) => {
      dispatch({
        type: ADD_TODO,
        payload: text
      });
    }
  };
};

在上述代码中,mapDispatchToProps 函数返回一个对象,其中 addTodo 函数接收一个 text 参数,并通过 dispatch 触发 ADD_TODO 类型的 action。

然后,使用 connect 函数将 mapStateToPropsmapDispatchToProps 与组件连接起来:

import React from'react';
import { connect } from'react - redux';

const TodoList = ({ todos, addTodo }) => {
  const handleSubmit = (e) => {
    e.preventDefault();
    const newTodo = e.target.elements.todoInput.value;
    addTodo(newTodo);
    e.target.reset();
  };

  return (
    <div>
      <form onSubmit = {handleSubmit}>
        <input type = "text" name = "todoInput" placeholder = "Add a todo" />
        <button type = "submit">Add</button>
      </form>
      <ul>
        {todos.map((todo, index) => (
          <li key = {index}>{todo}</li>
        ))}
      </ul>
    </div>
  );
};

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

useSelector 和 useDispatch 钩子(新方式)

随着 React Hooks 的出现,react - redux 也提供了基于 Hooks 的方式来连接组件与 Redux Store,即 useSelectoruseDispatch

  1. useSelector:用于从 Redux Store 中选择(获取)数据,并订阅 Store 的变化。当 Store 中的数据发生变化时,组件会重新渲染。例如:
import React from'react';
import { useSelector } from'react - redux';

const TodoList = () => {
  const todos = useSelector(state => state.todos);
  return (
    <div>
      <ul>
        {todos.map((todo, index) => (
          <li key = {index}>{todo}</li>
        ))}
      </ul>
    </div>
  );
};

export default TodoList;

在上述代码中,useSelector 接收一个函数,这个函数接收 Store 的状态并返回需要的部分,这里返回了 state.todos。 2. useDispatch:用于获取 Redux 的 dispatch 函数,以便在组件中触发 action。例如:

import React from'react';
import { useDispatch } from'react - redux';
import { ADD_TODO } from './actionTypes';

const TodoForm = () => {
  const dispatch = useDispatch();
  const handleSubmit = (e) => {
    e.preventDefault();
    const newTodo = e.target.elements.todoInput.value;
    dispatch({
      type: ADD_TODO,
      payload: newTodo
    });
    e.target.reset();
  };

  return (
    <form onSubmit = {handleSubmit}>
      <input type = "text" name = "todoInput" placeholder = "Add a todo" />
      <button type = "submit">Add</button>
    </form>
  );
};

export default TodoForm;

在上述代码中,useDispatch 获取了 dispatch 函数,在 handleSubmit 函数中通过 dispatch 触发了 ADD_TODO 类型的 action。

Redux 中间件

Redux 中间件是 Redux 架构中的一个重要组成部分,它可以对 action 的分发进行拦截和处理,从而实现一些额外的功能。

为什么需要中间件

在实际应用中,我们经常会遇到一些异步操作,比如 API 调用。在 Redux 中,action 必须是一个普通的 JavaScript 对象,不能是异步操作。而中间件可以帮助我们解决这个问题,使得我们能够在 action 分发过程中执行异步操作。

常见的 Redux 中间件

  1. Redux - Thunk:是最常用的 Redux 中间件之一,它允许我们在 action 创建函数中返回一个函数而不是一个普通的 action 对象。这个返回的函数可以接收 dispatchgetState 作为参数,从而可以在函数内部进行异步操作并分发 action。例如:
import { FETCH_TODOS_SUCCESS, FETCH_TODOS_FAILURE } from './actionTypes';
import axios from 'axios';

const fetchTodos = () => {
  return async (dispatch) => {
    try {
      const response = await axios.get('/api/todos');
      dispatch({
        type: FETCH_TODOS_SUCCESS,
        payload: response.data
      });
    } catch (error) {
      dispatch({
        type: FETCH_TODOS_FAILURE,
        payload: error.message
      });
    }
  };
};

在上述代码中,fetchTodos 函数返回一个异步函数,在这个异步函数中进行了 API 调用。如果调用成功,分发 FETCH_TODOS_SUCCESS 类型的 action,并将 API 返回的数据作为 payload;如果调用失败,分发 FETCH_TODOS_FAILURE 类型的 action,并将错误信息作为 payload。

要使用 redux - thunk,需要在创建 Redux Store 时将其作为中间件引入:

import { createStore, applyMiddleware } from'redux';
import thunk from'redux - thunk';
import todoReducer from './reducers/todoReducer';

const store = createStore(todoReducer, applyMiddleware(thunk));

export default store;
  1. Redux - Saga:是另一个强大的 Redux 中间件,它使用生成器函数来管理副作用,比如异步操作。与 redux - thunk 不同,redux - saga 将异步操作放在单独的 saga 文件中,使得代码更加模块化和易于维护。

首先,安装 redux - saga

npm install redux - saga

然后,创建一个 saga 文件,例如 todoSaga.js

import { call, put, takeEvery } from'redux - saga/effects';
import { FETCH_TODOS_SUCCESS, FETCH_TODOS_FAILURE } from './actionTypes';
import axios from 'axios';

function* fetchTodos() {
  try {
    const response = yield call(axios.get, '/api/todos');
    yield put({
      type: FETCH_TODOS_SUCCESS,
      payload: response.data
    });
  } catch (error) {
    yield put({
      type: FETCH_TODOS_FAILURE,
      payload: error.message
    });
  }
}

export function* todoSaga() {
  yield takeEvery('FETCH_TODOS_REQUEST', fetchTodos);
}

在上述代码中,fetchTodos 是一个 saga 生成器函数,它使用 call 来执行异步的 API 调用,使用 put 来分发 action。todoSaga 函数使用 takeEvery 来监听 FETCH_TODOS_REQUEST 类型的 action,并在每次监听到时调用 fetchTodos 函数。

在创建 Redux Store 时,需要引入 redux - saga 中间件并启动 saga:

import { createStore, applyMiddleware } from'redux';
import createSagaMiddleware from'redux - saga';
import todoReducer from './reducers/todoReducer';
import { todoSaga } from './todoSaga';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(todoReducer, applyMiddleware(sagaMiddleware));

sagaMiddleware.run(todoSaga);

export default store;

在大型应用中使用 React 和 Redux

项目结构设计

在大型 React 和 Redux 应用中,合理的项目结构设计至关重要。一种常见的结构如下:

src/
├── actions/
│   ├── todoActions.js
│   └── userActions.js
├── reducers/
│   ├── todoReducer.js
│   ├── userReducer.js
│   └── rootReducer.js
├── sagas/
│   ├── todoSaga.js
│   └── userSaga.js
├── components/
│   ├── TodoList/
│   │   ├── TodoList.js
│   │   └── TodoItem.js
│   └── UserProfile/
│       ├── UserProfile.js
│       └── UserInfo.js
├── store/
│   ├── configureStore.js
│   └── index.js
├── App.js
└── index.js
  1. actions 目录:存放所有的 action 创建函数,每个模块的 action 可以放在单独的文件中,便于管理和维护。
  2. reducers 目录:包含各个模块的 reducer 函数,rootReducer.js 文件用于将所有的 reducer 合并成一个根 reducer。
  3. sagas 目录(如果使用 Redux - Saga):存放所有的 saga 函数,同样每个模块的 saga 可以放在单独的文件中。
  4. components 目录:包含所有的 React 组件,按照功能模块进行分组。
  5. store 目录:负责创建和配置 Redux Store,configureStore.js 文件用于创建 Store 并引入中间件等配置。

状态管理策略

  1. 单一数据源:Redux 强调单一数据源,即整个应用的状态都集中存储在一个 Store 中。这使得状态的管理和调试变得更加容易,因为所有的状态变化都可以在一个地方进行追踪。
  2. 模块化状态管理:虽然使用单一数据源,但并不意味着所有的状态都混在一起。可以将状态按照功能模块进行划分,每个模块有自己的 reducer 和 action。例如,在一个电商应用中,可以将用户相关的状态放在 userReducer 中管理,商品相关的状态放在 productReducer 中管理。
  3. 规范化数据结构:在大型应用中,规范化数据结构可以避免数据的冗余和不一致。例如,对于一个包含用户和订单的应用,不要在每个订单中重复存储用户信息,而是通过 ID 来引用用户。可以使用类似于 normalizr 这样的库来帮助规范化数据结构。

性能优化

  1. Memoization:在 React 组件中,可以使用 React.memo 来对函数式组件进行 memoization,避免不必要的重新渲染。例如:
import React from'react';

const TodoItem = React.memo(({ todo }) => {
  return <li>{todo}</li>;
});

export default TodoItem;

在上述代码中,TodoItem 组件使用 React.memo 进行包裹,只有当 todo props 发生变化时,组件才会重新渲染。 2. Selector 函数优化:在使用 useSelector 时,可以通过创建高效的 selector 函数来避免不必要的重新渲染。例如,如果一个组件只关心 Store 中某个对象的特定属性,可以在 selector 函数中直接返回该属性,而不是返回整个对象。

import React from'react';
import { useSelector } from'react - redux';

const selectTodoCount = state => state.todos.length;

const TodoCount = () => {
  const count = useSelector(selectTodoCount);
  return <p>Total todos: {count}</p>;
};

export default TodoCount;

在上述代码中,selectTodoCount 函数只返回 todos 数组的长度,这样只有当 todos 数组的长度发生变化时,TodoCount 组件才会重新渲染。 3. Middleware 优化:对于 Redux 中间件,如 redux - sagaredux - thunk,要注意合理使用,避免在中间件中进行过多不必要的操作。例如,在 redux - saga 中,尽量将复杂的业务逻辑拆分到多个 saga 函数中,提高代码的可维护性和性能。

错误处理

在 React 和 Redux 应用中,错误处理是不可或缺的一部分。

React 组件中的错误处理

在 React 组件中,可以使用 try - catch 块来捕获同步错误。对于异步操作,可以使用 Promise.catch 或者 async - await 结合 try - catch 来处理错误。例如:

import React from'react';

const TodoForm = () => {
  const handleSubmit = async (e) => {
    e.preventDefault();
    const newTodo = e.target.elements.todoInput.value;
    try {
      // 假设这里有一个异步 API 调用
      await someAsyncAPI(newTodo);
      // 成功后执行的逻辑
    } catch (error) {
      console.error('Error adding todo:', error);
      // 错误处理逻辑,比如显示错误提示
    }
    e.target.reset();
  };

  return (
    <form onSubmit = {handleSubmit}>
      <input type = "text" name = "todoInput" placeholder = "Add a todo" />
      <button type = "submit">Add</button>
    </form>
  );
};

export default TodoForm;

Redux 中的错误处理

在 Redux 中,当使用中间件(如 redux - thunkredux - saga)进行异步操作时,错误处理通常在 action 创建函数或者 saga 函数中进行。例如,在使用 redux - thunk 时:

import { ADD_TODO_SUCCESS, ADD_TODO_FAILURE } from './actionTypes';
import axios from 'axios';

const addTodo = (text) => {
  return async (dispatch) => {
    try {
      const response = await axios.post('/api/todos', { text });
      dispatch({
        type: ADD_TODO_SUCCESS,
        payload: response.data
      });
    } catch (error) {
      dispatch({
        type: ADD_TODO_FAILURE,
        payload: error.message
      });
    }
  };
};

在上述代码中,当 API 调用失败时,分发 ADD_TODO_FAILURE 类型的 action,并将错误信息作为 payload。在 reducer 中,可以根据这个 action 类型来更新状态,比如设置一个错误提示信息。

const initialState = {
  todos: [],
  error: null
};

const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TODO_SUCCESS:
      return {
      ...state,
        todos: [...state.todos, action.payload],
        error: null
      };
    case ADD_TODO_FAILURE:
      return {
      ...state,
        error: action.payload
      };
    default:
      return state;
  }
};

全局错误处理

对于整个应用的错误处理,可以使用 React 的 ErrorBoundary 组件。ErrorBoundary 是一种 React 组件,它可以捕获其子组件树中抛出的 JavaScript 错误,并显示一个备用 UI,而不是渲染崩溃的子组件树。例如:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, errorInfo) {
    console.log('Error in component:', error, errorInfo);
    this.setState({ hasError: true });
  }

  render() {
    if (this.state.hasError) {
      return <div>Something went wrong!</div>;
    }
    return this.props.children;
  }
}

然后,可以在应用的顶层组件中使用 ErrorBoundary

import React from'react';
import ReactDOM from'react-dom';
import ErrorBoundary from './ErrorBoundary';
import App from './App';

ReactDOM.render(
  <ErrorBoundary>
    <App />
  </ErrorBoundary>,
  document.getElementById('root')
);

这样,当 App 组件及其子组件中抛出错误时,ErrorBoundary 会捕获错误并显示备用 UI。

总结 React 和 Redux 在大型应用中的优势

  1. 可维护性:通过将状态集中管理在 Redux Store 中,并使用模块化的 reducer 和 action,使得代码结构更加清晰,易于理解和维护。当应用规模扩大时,开发人员可以快速定位和修改与状态相关的逻辑。
  2. 可预测性:Redux 的单向数据流和严格的状态更新规则,使得状态的变化变得可预测。任何状态的改变都必须通过 action 触发,并且 reducer 是纯函数,这使得调试和追踪状态变化变得更加容易。
  3. 性能优化:通过合理使用 React 的 memoization 和 Redux 的 selector 函数优化等技术,可以有效避免不必要的重新渲染,提高应用的性能。特别是在大型应用中,这对于提升用户体验至关重要。
  4. 团队协作:React 和 Redux 的架构模式使得团队成员之间的分工更加明确。前端开发人员可以专注于 React 组件的开发,而状态管理相关的逻辑可以由熟悉 Redux 的开发人员负责。同时,清晰的代码结构也便于团队成员之间的代码审查和协作开发。

总之,React 与 Redux 结合为构建大型应用提供了一套强大且可靠的状态管理方案,能够帮助开发团队高效地开发出高质量、可维护的前端应用。