对于 Vue 开发者来说,React 的状态管理可能是最需要转变思维方式的部分之一。本文将从 Vue 开发者熟悉的角度出发,详细介绍 React 的状态管理方案,并通过实战示例帮助你快速掌握。

本地状态管理对比

Vue 的响应式系统

在 Vue 中,我们习惯使用 data 选项来定义组件的本地状态:

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++  // 直接修改状态
    }
  }
}
</script>

React 的 useState

而在 React 中,我们使用 useState Hook 来管理状态:

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    setCount(count + 1);  // 使用 setter 函数更新状态
  };
  
  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>+1</button>
    </div>
  );
}

主要区别:

  1. Vue 的状态是响应式的,可以直接修改
  2. React 的状态是不可变的,必须通过 setter 函数更新
  3. React 的状态更新是异步的,多个更新会被批处理

复杂状态管理

使用 useReducer

当组件状态逻辑较复杂时,可以使用 useReducer 来管理状态:

import React, { useReducer } from 'react';

// 定义 reducer 函数
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, {
        id: Date.now(),
        text: action.payload,
        completed: false
      }];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.payload
          ? { ...todo, completed: !todo.completed }
          : todo
      );
    case 'REMOVE_TODO':
      return state.filter(todo => todo.id !== action.payload);
    default:
      return state;
  }
}

function TodoList() {
  const [todos, dispatch] = useReducer(todoReducer, []);
  const [input, setInput] = useState('');
  
  const handleAdd = () => {
    if (!input.trim()) return;
    dispatch({ type: 'ADD_TODO', payload: input });
    setInput('');
  };
  
  return (
    <div>
      <input
        value={input}
        onChange={e => setInput(e.target.value)}
      />
      <button onClick={handleAdd}>添加</button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => dispatch({
                type: 'TOGGLE_TODO',
                payload: todo.id
              })}
            />
            <span>{todo.text}</span>
            <button onClick={() => dispatch({
              type: 'REMOVE_TODO',
              payload: todo.id
            })}>
              删除
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

这种模式类似于 Vuex 的 mutations,但更加轻量和灵活。

全局状态管理

Context API

React 的 Context API 类似于 Vue 的 provide/inject:

// ThemeContext.js
import React, { createContext, useContext, useState } from 'react';

const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  return useContext(ThemeContext);
}

// App.js
function App() {
  return (
    <ThemeProvider>
      <Layout />
    </ThemeProvider>
  );
}

// Layout.js
function Layout() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <div className={`app ${theme}`}>
      <button onClick={toggleTheme}>
        切换主题
      </button>
      <Content />
    </div>
  );
}

状态管理库对比

  1. Vuex vs Redux

Vuex:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  }
})

Redux:

// reducer.js
const initialState = { count: 0 };

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    default:
      return state;
  }
}

// actions.js
const increment = () => ({ type: 'INCREMENT' });
const incrementAsync = () => dispatch => {
  setTimeout(() => {
    dispatch(increment());
  }, 1000);
};

// store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

const store = createStore(
  counterReducer,
  applyMiddleware(thunk)
);
  1. Pinia vs Zustand

Pinia:

import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() {
      this.count++;
    }
  }
});

Zustand:

import create from 'zustand';

const useStore = create(set => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 }))
}));

实战示例:购物车

让我们通过一个购物车示例来实践状态管理:

// types.ts
interface Product {
  id: number;
  name: string;
  price: number;
}

interface CartItem extends Product {
  quantity: number;
}

// cartStore.js
import create from 'zustand';

const useCartStore = create((set, get) => ({
  items: [],
  totalAmount: 0,
  
  addToCart: (product) => set(state => {
    const existingItem = state.items.find(item => item.id === product.id);
    
    if (existingItem) {
      return {
        items: state.items.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        ),
        totalAmount: state.totalAmount + product.price
      };
    }
    
    return {
      items: [...state.items, { ...product, quantity: 1 }],
      totalAmount: state.totalAmount + product.price
    };
  }),
  
  removeFromCart: (productId) => set(state => {
    const item = state.items.find(item => item.id === productId);
    if (!item) return state;
    
    return {
      items: state.items.filter(item => item.id !== productId),
      totalAmount: state.totalAmount - (item.price * item.quantity)
    };
  }),
  
  updateQuantity: (productId, quantity) => set(state => {
    const item = state.items.find(item => item.id === productId);
    if (!item) return state;
    
    const quantityDiff = quantity - item.quantity;
    
    return {
      items: state.items.map(item =>
        item.id === productId
          ? { ...item, quantity }
          : item
      ),
      totalAmount: state.totalAmount + (item.price * quantityDiff)
    };
  })
}));

// ProductList.jsx
function ProductList() {
  const [products] = useState([
    { id: 1, name: '商品1', price: 100 },
    { id: 2, name: '商品2', price: 200 },
    { id: 3, name: '商品3', price: 300 }
  ]);
  
  const addToCart = useCartStore(state => state.addToCart);
  
  return (
    <div className="product-list">
      {products.map(product => (
        <div key={product.id} className="product-item">
          <h3>{product.name}</h3>
          <p>¥{product.price}</p>
          <button onClick={() => addToCart(product)}>
            加入购物车
          </button>
        </div>
      ))}
    </div>
  );
}

// Cart.jsx
function Cart() {
  const { items, totalAmount, updateQuantity, removeFromCart } = useCartStore();
  
  return (
    <div className="cart">
      <h2>购物车</h2>
      {items.map(item => (
        <div key={item.id} className="cart-item">
          <span>{item.name}</span>
          <input
            type="number"
            min="1"
            value={item.quantity}
            onChange={e => updateQuantity(item.id, +e.target.value)}
          />
          <span>¥{item.price * item.quantity}</span>
          <button onClick={() => removeFromCart(item.id)}>
            删除
          </button>
        </div>
      ))}
      <div className="cart-total">
        总计:¥{totalAmount}
      </div>
    </div>
  );
}

性能优化

  1. 状态分割

    // 不好的做法
    const [state, setState] = useState({
      user: null,
      posts: [],
      comments: []
    });
    
    // 好的做法
    const [user, setUser] = useState(null);
    const [posts, setPosts] = useState([]);
    const [comments, setComments] = useState([]);
  2. 使用 useMemo 缓存计算结果

    const totalPrice = useMemo(() => {
      return items.reduce((total, item) => total + item.price * item.quantity, 0);
    }, [items]);
  3. 使用 useCallback 缓存函数

    const handleUpdate = useCallback((id, value) => {
      updateQuantity(id, value);
    }, [updateQuantity]);
  4. 避免不必要的重渲染

    // CartItem.jsx
    const CartItem = memo(function CartItem({ item, onUpdate, onRemove }) {
      return (
     <div className="cart-item">
       <span>{item.name}</span>
       <input
         type="number"
         value={item.quantity}
         onChange={e => onUpdate(item.id, +e.target.value)}
       />
       <button onClick={() => onRemove(item.id)}>删除</button>
     </div>
      );
    });

调试技巧

  1. 使用 React DevTools
  2. 查看组件树
  3. 检查状态变化
  4. 分析重渲染原因
  5. 使用 Redux DevTools

    import { devtools } from 'zustand/middleware';
    
    const useStore = create(
      devtools(
     (set) => ({
       // store implementation
     })
      )
    );
  6. 使用日志中间件

    const useStore = create((set) => {
      const originalSet = set;
      set = (...args) => {
     console.log('prev state:', get());
     console.log('action:', args[0]);
     originalSet(...args);
     console.log('next state:', get());
      };
      
      return {
     // store implementation
      };
    });

最佳实践

  1. 状态设计原则
  2. 保持状态最小化
  3. 避免冗余数据
  4. 合理拆分状态
  5. 遵循单一数据源
  6. 更新模式
  7. 使用不可变更新
  8. 批量处理更新
  9. 避免深层嵌套
  10. 性能考虑
  11. 合理使用缓存
  12. 避免过度订阅
  13. 及时清理副作用

小结

  1. React 状态管理的特点:

    • 不可变性
    • 单向数据流
    • 函数式更新
    • 异步批处理
  2. 从 Vue 到 React 的转变:

    • 告别直接修改
    • 拥抱函数式
    • 重视性能优化
    • 合理使用 Hooks
  3. 开发建议:

    • 从简单开始
    • 循序渐进
    • 注重实践
    • 保持好奇

下一篇文章,我们将深入探讨 React 的组件设计模式,帮助你更好地组织和复用代码。

如果觉得这篇文章对你有帮助,别忘了点个赞 👍


远洋录
3 声望2 粉丝