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

React State 的最佳实践指南

2021-10-152.4k 阅读

React State 基础概念

在 React 应用中,State(状态)是一个核心概念。它用于存储组件的数据,这些数据可能会随着时间或用户交互而发生变化。React 组件通过 State 来决定如何渲染,当 State 发生变化时,React 会重新渲染组件,以反映最新的数据状态。

例如,创建一个简单的计数器组件:

import React, { useState } from 'react';

function 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;

在这个例子中,count 就是组件的 State,setCount 是用于更新 count 值的函数。每次点击按钮,count 增加 1,组件会重新渲染以显示新的计数值。

State 的不可变性

在 React 中,保持 State 的不可变性是非常重要的最佳实践。这意味着永远不要直接修改 State,而是应该创建一个新的 State 对象。

例如,假设我们有一个包含数组的 State,并且想要向数组中添加一个新元素。错误的做法是直接修改数组:

import React, { useState } from 'react';

function BadArrayUpdate() {
  const [items, setItems] = useState([]);

  const addItem = () => {
    // 错误:直接修改 State
    items.push('new item');
    setItems(items);
  };

  return (
    <div>
      <button onClick={addItem}>Add Item</button>
      <ul>
        {items.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default BadArrayUpdate;

这样做可能不会按预期工作,因为 React 可能无法检测到 State 的变化。正确的做法是创建一个新数组:

import React, { useState } from 'react';

function GoodArrayUpdate() {
  const [items, setItems] = useState([]);

  const addItem = () => {
    // 正确:创建新数组
    const newItems = [...items, 'new item'];
    setItems(newItems);
  };

  return (
    <div>
      <button onClick={addItem}>Add Item</button>
      <ul>
        {items.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default GoodArrayUpdate;

这里使用了展开运算符 ... 来创建一个包含原数组所有元素以及新元素的新数组。同样,对于对象也应该保持不可变性。例如:

import React, { useState } from 'react';

function ObjectUpdate() {
  const [user, setUser] = useState({ name: 'John', age: 30 });

  const updateAge = () => {
    // 正确:创建新对象
    const newUser = {...user, age: user.age + 1 };
    setUser(newUser);
  };

  return (
    <div>
      <p>{user.name} is {user.age} years old.</p>
      <button onClick={updateAge}>Increment Age</button>
    </div>
  );
}

export default ObjectUpdate;

通过这种方式,React 能够准确检测到 State 的变化并进行相应的重新渲染。

合理使用 State

并非所有数据都适合放在 State 中。只有那些会随时间变化并且影响组件渲染的数据才应该被放入 State。

例如,一个显示欢迎消息的组件,消息内容在组件整个生命周期内都不会改变,这种情况下将消息放在 State 中就是不合理的:

import React, { useState } from 'react';

// 不合理的 State 使用
function UnnecessaryState() {
  const [message, setMessage] = useState('Welcome to my app!');

  return (
    <div>
      <p>{message}</p>
    </div>
  );
}

export default UnnecessaryState;

更好的做法是将其作为一个普通变量:

import React from 'react';

// 合理的做法
function NoUnnecessaryState() {
  const message = 'Welcome to my app!';

  return (
    <div>
      <p>{message}</p>
    </div>
  );
}

export default NoUnnecessaryState;

另一方面,对于用户输入等会变化的数据,就适合放在 State 中。比如一个文本输入框:

import React, { useState } from 'react';

function InputComponent() {
  const [inputValue, setInputValue] = useState('');

  const handleChange = (e) => {
    setInputValue(e.target.value);
  };

  return (
    <div>
      <input type="text" value={inputValue} onChange={handleChange} />
      <p>You entered: {inputValue}</p>
    </div>
  );
}

export default InputComponent;

State 提升

当多个组件需要共享 State 时,应该将 State 提升到它们最近的共同父组件中。这有助于保持数据的一致性和可维护性。

假设我们有两个组件 Child1Child2,它们都需要访问和更新同一个 State:

import React, { useState } from 'react';

function Child1({ count, increment }) {
  return (
    <div>
      <p>Child1: {count}</p>
      <button onClick={increment}>Increment from Child1</button>
    </div>
  );
}

function Child2({ count, increment }) {
  return (
    <div>
      <p>Child2: {count}</p>
      <button onClick={increment}>Increment from Child2</button>
    </div>
  );
}

function Parent() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <Child1 count={count} increment={increment} />
      <Child2 count={count} increment={increment} />
    </div>
  );
}

export default Parent;

在这个例子中,count State 被提升到了 Parent 组件中,Child1Child2 通过 props 来访问和更新这个 State。这样,无论哪个子组件触发 increment 函数,两个子组件都会显示相同的更新后的值。

使用 useReducer 管理复杂 State

对于复杂的 State 逻辑,useReducer 是一个比 useState 更合适的选择。useReducer 类似于 Redux 中的 reducer,它接受一个 reducer 函数和初始 State。

例如,我们创建一个购物车组件,需要管理商品的添加、移除和数量更新等复杂逻辑。首先定义 reducer 函数:

const cartReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_ITEM':
      return [...state, { id: action.id, name: action.name, quantity: 1 }];
    case 'REMOVE_ITEM':
      return state.filter(item => item.id!== action.id);
    case 'INCREMENT_QUANTITY':
      return state.map(item =>
        item.id === action.id
         ? {...item, quantity: item.quantity + 1 }
          : item
      );
    case 'DECREMENT_QUANTITY':
      return state.map(item =>
        item.id === action.id && item.quantity > 1
         ? {...item, quantity: item.quantity - 1 }
          : item
      );
    default:
      return state;
  }
};

然后在组件中使用 useReducer

import React, { useReducer } from 'react';

function Cart() {
  const [cart, dispatch] = useReducer(cartReducer, []);

  const addItem = (id, name) => {
    dispatch({ type: 'ADD_ITEM', id, name });
  };

  const removeItem = (id) => {
    dispatch({ type: 'REMOVE_ITEM', id });
  };

  const incrementQuantity = (id) => {
    dispatch({ type: 'INCREMENT_QUANTITY', id });
  };

  const decrementQuantity = (id) => {
    dispatch({ type: 'DECREMENT_QUANTITY', id });
  };

  return (
    <div>
      <h2>Shopping Cart</h2>
      <ul>
        {cart.map(item => (
          <li key={item.id}>
            {item.name} - Quantity: {item.quantity}
            <button onClick={() => incrementQuantity(item.id)}>Increment</button>
            <button onClick={() => decrementQuantity(item.id)}>Decrement</button>
            <button onClick={() => removeItem(item.id)}>Remove</button>
          </li>
        ))}
      </ul>
      <button onClick={() => addItem(1, 'Product 1')}>Add Product 1</button>
    </div>
  );
}

export default Cart;

通过 useReducer,我们将复杂的 State 更新逻辑集中在 reducer 函数中,使组件代码更加清晰和易于维护。

避免不必要的 State 更新

不必要的 State 更新会导致性能问题,因为每次 State 更新都会触发组件重新渲染。可以通过 shouldComponentUpdate 生命周期方法(在类组件中)或 React.memo(在函数组件中)来避免不必要的重新渲染。

对于函数组件,使用 React.memo 很简单。例如:

import React from'react';

const MyComponent = React.memo((props) => {
  return <div>{props.value}</div>;
});

export default MyComponent;

React.memo 会浅比较 props,如果 props 没有变化,组件将不会重新渲染。

在类组件中,可以使用 shouldComponentUpdate 方法:

import React, { Component } from'react';

class MyClassComponent extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    // 仅当 props.value 变化时才重新渲染
    return this.props.value!== nextProps.value;
  }

  render() {
    return <div>{this.props.value}</div>;
  }
}

export default MyClassComponent;

通过这种方式,可以有效减少不必要的 State 更新带来的性能开销。

异步操作与 State

在 React 应用中,经常会遇到异步操作,如 API 调用。在处理异步操作时,管理 State 变得更加复杂。

一种常见的模式是使用 useStateuseEffect 结合来处理异步数据获取。例如,从 API 获取用户数据:

import React, { useState, useEffect } from'react';

function UserProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch('https://example.com/api/user');
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const data = await response.json();
        setUser(data);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, []);

  if (loading) {
    return <p>Loading...</p>;
  }

  if (error) {
    return <p>Error: {error.message}</p>;
  }

  if (!user) {
    return null;
  }

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

export default UserProfile;

在这个例子中,user State 用于存储获取到的用户数据,loading State 用于表示数据是否正在加载,error State 用于捕获可能发生的错误。useEffect 钩子在组件挂载时触发一次,执行异步数据获取操作,并根据不同的状态更新相应的 State,从而正确渲染组件。

状态管理库与 React State

虽然 React 自身的 State 管理在很多情况下已经足够,但对于大型应用,使用状态管理库如 Redux 或 MobX 可能更合适。

Redux 采用单向数据流,通过一个全局的 store 来管理应用的 State。所有的 State 更新都通过 action 来触发,reducer 函数根据 action 来更新 State。例如:

// actions.js
const increment = () => ({ type: 'INCREMENT' });

// reducer.js
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    default:
      return state;
  }
};

// store.js
import { createStore } from'redux';
const store = createStore(counterReducer);

// component.js
import React from'react';
import { useSelector, useDispatch } from'react-redux';

function Counter() {
  const count = useSelector(state => state);
  const dispatch = useDispatch();

  const incrementCount = () => {
    dispatch(increment());
  };

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

export default Counter;

MobX 则采用响应式编程模型,通过 observable 来定义可观察的 State,当 observable 数据发生变化时,依赖它的组件会自动重新渲染。例如:

import { makeObservable, observable, action } from'mobx';

class CounterStore {
  count = 0;

  constructor() {
    makeObservable(this, {
      count: observable,
      increment: action
    });
  }

  increment() {
    this.count++;
  }
}

const counterStore = new CounterStore();

// component.js
import React from'react';
import { observer } from'mobx-react';

const Counter = observer(() => {
  return (
    <div>
      <p>Count: {counterStore.count}</p>
      <button onClick={() => counterStore.increment()}>Increment</button>
    </div>
  );
});

export default Counter;

这些状态管理库可以帮助我们更好地组织和管理复杂应用的 State,但在使用时需要权衡其引入的复杂性和额外的学习成本。

调试 React State

在开发过程中,调试 React State 是很重要的。React DevTools 是一个非常有用的工具,它可以让我们在浏览器中查看组件的 State 和 props。

在 Chrome 或 Firefox 浏览器中安装 React DevTools 扩展后,打开应用的开发者工具,就可以看到 React 标签。在这里可以浏览组件树,查看每个组件的 State 和 props,还可以跟踪 State 的变化。

另外,在代码中使用 console.log 也是一种简单的调试方法。例如,在 State 更新函数中打印 State:

import React, { useState } from'react';

function DebuggingComponent() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
    console.log('New count:', count + 1);
  };

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

export default DebuggingComponent;

这样可以在控制台中看到 State 更新的情况,有助于发现问题。

与 Server - Side State 的同步

在实际应用中,前端的 React State 通常需要与服务器端的状态进行同步。这可以通过 RESTful API 或 GraphQL 等方式实现。

以 RESTful API 为例,当用户在前端更新了 State,需要将这些变化发送到服务器。例如,用户在购物车中添加了商品:

import React, { useState, useEffect } from'react';

function Cart() {
  const [cart, setCart] = useState([]);

  const addItemToCart = async (item) => {
    const newCart = [...cart, item];
    setCart(newCart);

    try {
      await fetch('https://example.com/api/cart', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(newCart)
      });
    } catch (error) {
      console.error('Error updating cart on server:', error);
    }
  };

  useEffect(() => {
    const fetchCart = async () => {
      try {
        const response = await fetch('https://example.com/api/cart');
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const data = await response.json();
        setCart(data);
      } catch (error) {
        console.error('Error fetching cart from server:', error);
      }
    };

    fetchCart();
  }, []);

  return (
    <div>
      <h2>Shopping Cart</h2>
      <button onClick={() => addItemToCart({ id: 1, name: 'Product 1' })}>Add Product 1</button>
      <ul>
        {cart.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

export default Cart;

在这个例子中,当用户添加商品到购物车时,前端 State 首先更新,然后通过 fetch 发送 POST 请求将新的购物车数据发送到服务器。在组件挂载时,会从服务器获取购物车的初始数据并更新前端 State。

React State 在不同场景下的应用

  1. 表单处理 在处理表单时,State 用于存储用户输入的值。例如,一个登录表单:
import React, { useState } from'react';

function LoginForm() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Username:', username, 'Password:', password);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Username:
        <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} />
      </label>
      <label>
        Password:
        <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      </label>
      <button type="submit">Login</button>
    </form>
  );
}

export default LoginForm;

这里 usernamepassword State 分别存储用户名和密码输入框的值,通过 onChange 事件更新 State,在表单提交时可以使用这些 State 值进行后续操作。

  1. 多步骤向导 对于多步骤向导,State 可以用于跟踪当前步骤和用户输入的数据。例如:
import React, { useState } from'react';

function Wizard() {
  const [step, setStep] = useState(1);
  const [formData, setFormData] = useState({});

  const nextStep = () => {
    setStep(step + 1);
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({...formData, [name]: value });
  };

  if (step === 1) {
    return (
      <div>
        <h2>Step 1</h2>
        <input type="text" name="name" placeholder="Your name" onChange={handleChange} />
        <button onClick={nextStep}>Next</button>
      </div>
    );
  } else if (step === 2) {
    return (
      <div>
        <h2>Step 2</h2>
        <input type="email" name="email" placeholder="Your email" onChange={handleChange} />
        <button onClick={nextStep}>Next</button>
      </div>
    );
  } else if (step === 3) {
    return (
      <div>
        <h2>Summary</h2>
        <p>Name: {formData.name}</p>
        <p>Email: {formData.email}</p>
      </div>
    );
  }
}

export default Wizard;

在这个例子中,step State 跟踪当前向导步骤,formData State 存储用户在各个步骤输入的数据。

  1. 模态框控制 在处理模态框时,State 可以用于控制模态框的显示和隐藏。例如:
import React, { useState } from'react';

function Modal() {
  const [isModalOpen, setIsModalOpen] = useState(false);

  const openModal = () => {
    setIsModalOpen(true);
  };

  const closeModal = () => {
    setIsModalOpen(false);
  };

  return (
    <div>
      <button onClick={openModal}>Open Modal</button>
      {isModalOpen && (
        <div className="modal">
          <div className="modal-content">
            <span className="close" onClick={closeModal}>&times;</span>
            <p>This is a modal.</p>
          </div>
        </div>
      )}
    </div>
  );
}

export default Modal;

isModalOpen State 决定了模态框是否显示,通过 openModalcloseModal 函数来更新 State 从而控制模态框的显示与隐藏。

性能优化与 React State

  1. 批量更新 State 在 React 中,多次 State 更新会自动批量处理,以减少不必要的重新渲染。例如:
import React, { useState } from'react';

function BatchUpdate() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const updateBoth = () => {
    setCount1(count1 + 1);
    setCount2(count2 + 1);
  };

  return (
    <div>
      <p>Count1: {count1}</p>
      <p>Count2: {count2}</p>
      <button onClick={updateBoth}>Update Both</button>
    </div>
  );
}

export default BatchUpdate;

这里点击按钮时,虽然有两次 State 更新,但 React 会批量处理,只触发一次重新渲染。然而,在某些情况下,如在异步操作或原生 DOM 事件处理中,批量更新可能不会生效。这时可以使用 unstable_batchedUpdates(React 18 之前)或 flushSync(React 18 及之后)来手动实现批量更新。

  1. Memoization(记忆化) 对于一些计算开销较大的 State 更新,可以使用 memoization 来避免重复计算。例如,计算一个数组中所有数字的平方和:
import React, { useState, useMemo } from'react';

function SquaredSum() {
  const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);
  const squaredSum = useMemo(() => {
    return numbers.reduce((acc, num) => acc + num * num, 0);
  }, [numbers]);

  const addNumber = () => {
    setNumbers([...numbers, numbers.length + 1]);
  };

  return (
    <div>
      <p>Squared Sum: {squaredSum}</p>
      <button onClick={addNumber}>Add Number</button>
    </div>
  );
}

export default SquaredSum;

这里使用 useMemo 来记忆化 squaredSum 的计算结果,只有当 numbers State 变化时才会重新计算,避免了每次重新渲染都进行不必要的计算。

  1. 虚拟 DOM 与 State 变化 React 使用虚拟 DOM 来高效地更新实际 DOM。当 State 发生变化时,React 会创建一个新的虚拟 DOM 树,并与之前的虚拟 DOM 树进行比较,计算出最小的 DOM 变化集,然后只更新实际 DOM 中发生变化的部分。理解这一点有助于我们更好地优化 State 管理,减少不必要的 State 变化,从而提高应用性能。例如,在一个列表组件中,如果只更新列表中某一项的属性,而不是整个列表的 State,React 可以更精准地更新 DOM,提高性能。
import React, { useState } from'react';

function List() {
  const [items, setItems] = useState([
    { id: 1, text: 'Item 1', isChecked: false },
    { id: 2, text: 'Item 2', isChecked: false }
  ]);

  const toggleItem = (id) => {
    setItems(items.map(item =>
      item.id === id
       ? {...item, isChecked:!item.isChecked }
        : item
    ));
  };

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          <input type="checkbox" checked={item.isChecked} onChange={() => toggleItem(item.id)} />
          {item.text}
        </li>
      ))}
    </ul>
  );
}

export default List;

在这个例子中,当用户点击复选框时,只更新了列表中对应项的 isChecked 属性,React 会根据虚拟 DOM 比较,只更新相应的 DOM 元素,而不是整个列表。

结论

React State 是构建交互式和动态用户界面的关键部分。通过遵循最佳实践,如保持 State 的不可变性、合理使用 State、避免不必要的更新等,我们可以构建出高效、可维护的 React 应用。同时,结合状态管理库、异步操作处理以及性能优化技巧,可以更好地应对复杂应用场景的需求。在实际开发中,不断积累经验,根据项目的具体情况选择最合适的 State 管理方式,将有助于提升开发效率和应用质量。