大型前端应用状态管理实战:从 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. 代码可维护性是首要目标

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

写在最后

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

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

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

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

相关推荐
问道飞鱼几秒前
【前端知识】微前端框架qiankun
前端·前端框架·qiankun
冷环渊3 分钟前
React基础学习
前端·学习·react.js
carcarrot6 分钟前
一些前端组件介绍
前端·javascript
A Genius17 分钟前
Pytorch实现MobilenetV2官方源码
人工智能·pytorch·python
疯狂的沙粒17 分钟前
JavaScript 单例模式的创建与应用
开发语言·前端·javascript·vue.js
道友老李32 分钟前
【OpenCV】直方图
人工智能·opencv·计算机视觉
通信仿真实验室33 分钟前
Google BERT入门(5)Transformer通过位置编码学习位置
人工智能·深度学习·神经网络·自然语言处理·nlp·bert·transformer
唐天下文化36 分钟前
飞猪携手新疆机场集团,共创旅游新体验,翻开新疆旅游新篇章
人工智能·旅游
正在走向自律37 分钟前
深度学习:重塑学校教育的未来
人工智能·深度学习·机器学习
neeef_se1 小时前
前端速通(CSS)
前端·css