大型前端应用状态管理实战:从 Redux 到 React Query 的演进之路

"又是状态管理的问题!"这是我们团队在重构一个大型电商后台时经常听到的抱怨。随着业务的快速发展,原本清晰的 Redux 架构逐渐变得臃肿不堪。本文将分享我们如何通过技术演进,最终构建出一个优雅高效的状态管理方案。🛠️

问题的源头

让我们先看看原始代码中的问题:

typescript 复制代码
// 典型的 Redux 代码
// actions/product.ts
export const fetchProducts = () => async (dispatch) => {
  dispatch({ type: 'FETCH_PRODUCTS_START' });
  try {
    const response = await api.getProducts();
    dispatch({ type: 'FETCH_PRODUCTS_SUCCESS', payload: response.data });
  } catch (error) {
    dispatch({ type: 'FETCH_PRODUCTS_ERROR', payload: error });
  }
};

// reducers/product.ts
const productReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'FETCH_PRODUCTS_START':
      return { ...state, loading: true };
    case 'FETCH_PRODUCTS_SUCCESS':
      return { 
        ...state, 
        loading: false, 
        data: action.payload 
      };
    case 'FETCH_PRODUCTS_ERROR':
      return { 
        ...state, 
        loading: false, 
        error: action.payload 
      };
    default:
      return state;
  }
};

主要问题:

  1. 大量的样板代码
  2. 异步操作处理复杂
  3. 数据缓存和更新困难
  4. 代码复用性差

演进过程

1. 优化 Redux 架构

首先,我们尝试通过自定义中间件和工具函数优化 Redux:

typescript 复制代码
// utils/createAsyncAction.ts
interface AsyncAction<T> {
  type: string;
  payload?: T;
  error?: Error;
  meta?: any;
}

function createAsyncAction<T>(type: string) {
  return {
    request: (): AsyncAction<void> => ({
      type: `${type}_REQUEST`
    }),
    success: (payload: T): AsyncAction<T> => ({
      type: `${type}_SUCCESS`,
      payload
    }),
    failure: (error: Error): AsyncAction<Error> => ({
      type: `${type}_FAILURE`,
      error
    })
  };
}

// 使用示例
const productActions = createAsyncAction<Product[]>('FETCH_PRODUCTS');

// 更简洁的异步操作
async function fetchProducts(dispatch) {
  try {
    dispatch(productActions.request());
    const data = await api.getProducts();
    dispatch(productActions.success(data));
  } catch (error) {
    dispatch(productActions.failure(error));
  }
}

2. 引入 Redux Toolkit

接下来,我们使用 Redux Toolkit 简化状态管理:

typescript 复制代码
// features/products/productsSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

export const fetchProducts = createAsyncThunk(
  'products/fetch',
  async (_, { rejectWithValue }) => {
    try {
      const response = await api.getProducts();
      return response.data;
    } catch (error) {
      return rejectWithValue(error);
    }
  }
);

const productsSlice = createSlice({
  name: 'products',
  initialState: {
    data: [],
    loading: false,
    error: null
  },
  reducers: {
    updateProduct: (state, action) => {
      const index = state.data.findIndex(p => p.id === action.payload.id);
      if (index !== -1) {
        state.data[index] = action.payload;
      }
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchProducts.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchProducts.fulfilled, (state, action) => {
        state.loading = false;
        state.data = action.payload;
      })
      .addCase(fetchProducts.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  }
});

3. 引入 React Query

最后,我们发现很多状态其实是服务器数据的缓存,于是引入了 React Query:

typescript 复制代码
// hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from 'react-query';

export function useProducts() {
  return useQuery('products', api.getProducts, {
    staleTime: 5 * 60 * 1000, // 5分钟内认为数据是新鲜的
    cacheTime: 30 * 60 * 1000, // 缓存30分钟
  });
}

export function useUpdateProduct() {
  const queryClient = useQueryClient();

  return useMutation(api.updateProduct, {
    // 乐观更新
    onMutate: async (newProduct) => {
      await queryClient.cancelQueries('products');

      const previousProducts = queryClient.getQueryData('products');

      queryClient.setQueryData('products', (old: Product[]) => {
        return old.map(p => 
          p.id === newProduct.id ? newProduct : p
        );
      });

      return { previousProducts };
    },

    // 错误回滚
    onError: (err, newProduct, context) => {
      queryClient.setQueryData('products', context.previousProducts);
    },

    // 成功后使缓存失效
    onSettled: () => {
      queryClient.invalidateQueries('products');
    }
  });
}

4. 状态分层管理

我们将状态按照特性进行分层:

typescript 复制代码
// 1. 服务器状态(使用 React Query)
function ProductList() {
  const { data: products, isLoading } = useProducts();
  const updateProduct = useUpdateProduct();

  if (isLoading) return <Spinner />;

  return (
    <div>
      {products.map(product => (
        <ProductCard
          key={product.id}
          product={product}
          onUpdate={updateProduct.mutate}
        />
      ))}
    </div>
  );
}

// 2. UI 状态(使用 useState)
function ProductCard({ product, onUpdate }) {
  const [isEditing, setIsEditing] = useState(false);

  return (
    <Card>
      {isEditing ? (
        <ProductForm
          product={product}
          onSubmit={(data) => {
            onUpdate(data);
            setIsEditing(false);
          }}
        />
      ) : (
        <ProductDisplay
          product={product}
          onEdit={() => setIsEditing(true)}
        />
      )}
    </Card>
  );
}

// 3. 全局 UI 状态(使用 Context)
const ThemeContext = createContext<Theme>('light');

function useTheme() {
  const theme = useContext(ThemeContext);
  if (!theme) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return theme;
}

// 4. 表单状态(使用 React Hook Form)
function ProductForm({ product, onSubmit }) {
  const { register, handleSubmit } = useForm({
    defaultValues: product
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} />
      <input {...register('price')} />
      <button type="submit">保存</button>
    </form>
  );
}

5. 性能优化

实现了智能的缓存策略:

typescript 复制代码
// hooks/useOptimisticUpdate.ts
function useOptimisticUpdate<T>(
  queryKey: string,
  mutationFn: (data: T) => Promise<T>
) {
  const queryClient = useQueryClient();

  return useMutation(mutationFn, {
    onMutate: async (newData) => {
      await queryClient.cancelQueries(queryKey);
      const snapshot = queryClient.getQueryData(queryKey);

      queryClient.setQueryData(queryKey, (old: T[]) => {
        return Array.isArray(old)
          ? [...old, newData]
          : [newData];
      });

      return { snapshot };
    },

    onError: (_, __, context) => {
      queryClient.setQueryData(queryKey, context.snapshot);
    },

    onSettled: () => {
      queryClient.invalidateQueries(queryKey);
    }
  });
}

// 使用示例
function ProductList() {
  const optimisticUpdate = useOptimisticUpdate('products', api.createProduct);

  const handleAdd = async (product) => {
    try {
      await optimisticUpdate.mutateAsync(product);
      toast.success('产品添加成功!');
    } catch (error) {
      toast.error('添加失败,请重试');
    }
  };

  return (/* ... */);
}

最终架构

我们的最终状态管理方案是这样的:

  1. 服务器状态:React Query

    • 自动缓存和重新验证
    • 乐观更新
    • 后台重新获取
  2. 客户端状态:

    • 局部 UI 状态:useState
    • 全局 UI 状态:Context
    • 表单状态:React Hook Form
  3. 性能优化:

    • 查询缓存
    • 乐观更新
    • 后台同步
    • 智能重新获取

收获与思考

经过这次重构,我们学到了很多:

  1. 不是所有状态都需要全局管理
  2. 选择合适的工具很重要
  3. 性能优化要从架构层面考虑
  4. 代码可维护性是首要目标

最让我欣慰的是团队成员的反馈:"现在的代码清晰多了,维护起来也轻松!"😊

写在最后

状态管理没有银弹,关键是要:

  • 理解不同类型的状态
  • 选择合适的管理方案
  • 持续优化和改进
  • 保持代码的可维护性

有什么问题欢迎在评论区讨论,我们一起学习进步!

如果觉得有帮助,别忘了点赞关注,我会继续分享更多实战经验~

相关推荐
__lost1 小时前
MATLAB画出3d的常见复杂有机分子和矿物的分子结构
开发语言·人工智能·matlab·化学·分子结构
每天都要写算法(努力版)1 小时前
【神经网络与深度学习】五折交叉验证(5-Fold Cross-Validation)
人工智能·深度学习·神经网络
郭不耐2 小时前
DeepSeek智能时空数据分析(六):大模型NL2SQL绘制城市之间连线
人工智能·数据分析·时序数据库·数据可视化·deepseek
一城烟雨_2 小时前
vue3 实现将html内容导出为图片、pdf和word
前端·javascript·vue.js·pdf
winfredzhang2 小时前
Deepseek 生成新玩法:从文本到可下载 Word 文档?思路与实践
人工智能·word·deepseek
KY_chenzhao3 小时前
ChatGPT与DeepSeek在科研论文撰写中的整体科研流程与案例解析
人工智能·机器学习·chatgpt·论文·科研·deepseek
树懒的梦想3 小时前
调整vscode的插件安装位置
前端·cursor
不爱吃于先生3 小时前
生成对抗网络(Generative Adversarial Nets,GAN)
人工智能·神经网络·生成对抗网络
cxr8283 小时前
基于Playwright的浏览器自动化MCP服务
人工智能·自动化·大语言模型·mcp
PPIO派欧云4 小时前
PPIO X OWL:一键开启任务自动化的高效革命
运维·人工智能·自动化·github·api·教程·ppio派欧云