React 速查手册:日常开发实用参考

写了这么多年 React 之后,我发现日常开发其实就是在反复使用那么一小撮模式。这份速查手册整理的不是理论,而是我每天敲代码时真正会用到的语法和写法——把它当成手边的字典就好。

组件:React 的原子

现代 React 已经全面拥抱函数式。除非你在维护遗留代码或者写 Error Boundary,否则就别再写类组件了。

jsx
// 函数组件——默认写法
function Welcome({ name }) {
  return <h1>Hello, {name}</h1>;
}

// 箭头函数写法——同样常见
const Welcome = ({ name }) => <h1>Hello, {name}</h1>;

export default Welcome;

组件就是一个接收 props、返回 JSX 的函数。整个心智模型就这么简单。

Hooks:逻辑的归宿

useState —— 局部状态

jsx
const [count, setCount] = useState(0);

// 直接更新
setCount(count + 1);

// 函数式更新——当新状态依赖旧状态时使用
setCount((prev) => prev + 1);

经验法则:如果新状态依赖旧状态,就用函数式更新,可以避免闭包陷阱带来的 bug。

useEffect —— 副作用

jsx
useEffect(() => {
  const id = setInterval(tick, 1000);
  return () => clearInterval(id); // 清理函数
}, [tick]);

依赖数组决定了 effect 何时重新执行:

  • [] —— 仅在挂载时执行一次
  • [a, b] —— 当 ab 变化时执行
  • 不传 —— 每次渲染都执行(几乎一定是 bug)

useRef —— 不触发重渲染的可变值

jsx
const inputRef = useRef(null);

useEffect(() => {
  inputRef.current.focus();
}, []);

return <input ref={inputRef} />;

useRef 不仅能引用 DOM,也很适合存放任何不应触发重渲染的可变值——定时器 ID、上一次的 props 等等。

useMemo 和 useCallback —— 性能优化

jsx
// 缓存昂贵的计算
const sorted = useMemo(() => items.sort(compare), [items]);

// 缓存传给已 memo 化子组件的回调
const handleClick = useCallback((id) => deleteItem(id), [deleteItem]);

不要无脑滥用这两个 Hook。只有在你测出了真实的性能问题,或者要把函数/对象传给 React.memo 包装过的组件时,才需要它们。

useContext —— 摆脱 Props 透传

jsx
const ThemeContext = createContext("light");

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  const theme = useContext(ThemeContext);
  return <div className={theme}>...</div>;
}

useReducer —— 复杂状态

jsx
function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "reset":
      return { count: 0 };
    default:
      throw new Error();
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0 });
dispatch({ type: "increment" });

当状态转换变得复杂、或者多个相关值会一起变化时,就该考虑 useReducer

每天都会用到的 JSX 模式

条件渲染

jsx
{
  isLoggedIn && <Dashboard />;
}
{
  isLoggedIn ? <Dashboard /> : <Login />;
}
{
  user?.name ?? "游客";
}

列表渲染

jsx
{
  todos.map((todo) => <li key={todo.id}>{todo.text}</li>);
}

永远要用稳定的 key。除非列表永不重新排序,否则不要拿数组下标当 key。

Props 展开

jsx
<Input {...commonProps} value={value} onChange={onChange} />

Fragment

jsx
<>
  <Header />
  <Main />
</>

受控表单

jsx
function LoginForm() {
  const [email, setEmail] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    submit(email);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <button type="submit">登录</button>
    </form>
  );
}

三条铁律:value 绑定到状态、onChange 更新状态、onSubmit 里调 preventDefault

自定义 Hook:复用利器

任何以 use 开头、并且内部调用了其他 Hook 的函数,都是自定义 Hook。要抽取的是逻辑,不是 JSX。

jsx
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;
    fetch(url)
      .then((r) => r.json())
      .then((d) => {
        if (!cancelled) setData(d);
      })
      .finally(() => {
        if (!cancelled) setLoading(false);
      });
    return () => {
      cancelled = true;
    };
  }, [url]);

  return { data, loading };
}

那个 cancelled 标志能避免组件卸载后还去调用 setState——这是一个常见但隐蔽的 bug。

性能工具

jsx
// 当 props 浅相等时跳过重渲染
const Card = React.memo(function Card({ title }) {
  return <div>{title}</div>;
});

// 代码分割:按需加载体积较大的组件
const Editor = React.lazy(() => import("./Editor"));

<Suspense fallback={<Spinner />}>
  <Editor />
</Suspense>;

React Router v6 路由

jsx
import {
  BrowserRouter,
  Routes,
  Route,
  Link,
  useNavigate,
  useParams,
} from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/users/:id" element={<User />} />
      </Routes>
    </BrowserRouter>
  );
}

function User() {
  const { id } = useParams();
  const navigate = useNavigate();
  return <button onClick={() => navigate("/")}>返回</button>;
}

常见坑

闭包陷阱(Stale Closure)。 函数捕获 state 时拿到的是渲染那一刻的值,之后再读还是老值。用函数式更新或者从 ref 里读,就能绕开。

开发模式下 effect 执行两次。 React 18+ 的 Strict Mode 会故意把 effect 跑两遍,用来暴露漏掉的清理逻辑。这是有意为之——把你的 effect 写成幂等的就好。

直接修改 state。 一定要创建新的对象/数组。arr.push(x) 不会触发重渲染,只有 setArr([...arr, x]) 才会。

列表忘了写 key React 靠 key 判断哪些元素变了。没有 key,输入框、动画、组件内部状态都会出现各种诡异的问题。

我习惯的项目结构

text
src/
  components/    # 通用可复用 UI
  pages/         # 路由级页面组件
  hooks/         # 自定义 Hook
  contexts/      # Context Provider
  api/           # 网络请求层
  utils/         # 纯函数工具

具体起什么名字其实不重要,关键是选一种结构、然后坚持下去

写在最后

React 的核心很小:组件、props、state、effect。一个代码库写起来是顺畅还是痛苦,往往跟 React 本身关系不大,更多取决于你有多自律——组件够不够小、effect 够不够窄、state 是不是放在了它该在的位置。拿不准的时候记住一条:state 上提到刚刚好够用就行,多上提一层都嫌多。