React面试宝典【Part7:常用自定义hook和其他案例】

2026-08-03 React,面试

1. 手写 useDebounce 防抖 Hook

场景:搜索框输入、窗口 resize、滚动、高频点击。

好处
逻辑复用:一次封装,到处使用
自动清理副作用:组件卸载 / 依赖变化时自动清定时器
组件代码极干净,只关心业务,不关心定时器细节

import { useEffect, useState } from 'react';

function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

用法

const [value, setValue] = useState('');
const debouncedValue = useDebounce(value, 500);

2. 手写 useThrottle 节流 Hook

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

function useThrottle(value, limit = 300) {
  const [throttledValue, setThrottledValue] = useState(value);
  const lastRan = useRef(Date.now());

  useEffect(() => {
    const handler = setTimeout(() => {
      if (Date.now() - lastRan.current >= limit) {
        setThrottledValue(value);
        lastRan.current = Date.now();
      }
    }, limit - (Date.now() - lastRan.current));

    return () => clearTimeout(handler);
  }, [value, limit]);

  return throttledValue;
}

3. 手写 useFetch(loading、error、data)

场景:几乎所有请求接口的组件。

useFetch 的价值

  • 统一请求逻辑:loading、error、data 全部封装
  • 避免重复代码
  • 统一错误处理、统一加载样式
  • 组件只拿结果,不关心请求过程
import { useState, useEffect } from 'react';

function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      setError(null);
      try {
        const res = await fetch(url, options);
        if (!res.ok) throw new Error('请求失败');
        const result = await res.json();
        setData(result);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url, options]);

  return { data, loading, error };
}

4. 简易 useReducer 手写

场景:状态逻辑复杂、一个事件要改多个状态。

useReducer 的价值

  • 状态更新可预测,集中管理
  • 复杂逻辑收敛到 reducer,组件更干净
  • 方便日志、回放、测试
import { useState } from 'react';

function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState);

  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }

  return [state, dispatch];
}

使用示例

function reducer(state, action) {
  switch (action.type) {
    case 'inc': return { count: state.count + 1 };
    default: return state;
  }
}

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

5. 实现受控表单组件

场景:登录、表单页、大批量输入框。

封装后价值

  • 一个 onChange 管理所有表单项
  • 统一做校验、提交、重置
  • 代码极度简洁、可复用
import { useState } from 'react';

function FormControl() {
  const [form, setForm] = useState({
    username: '',
    password: ''
  });

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

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="username"
        value={form.username}
        onChange={handleChange}
        placeholder="用户名"
      />
      <input
        name="password"
        type="password"
        value={form.password}
        onChange={handleChange}
        placeholder="密码"
      />
      <button type="submit">提交</button>
    </form>
  );
}

6. 简易虚拟列表思路(面试口述版)

场景:长列表(1000+ 条数据)

价值

  • 只渲染可视区域 DOM,性能提升几十倍
  • 滚动流畅,不阻塞主线程
  • 逻辑复用,所有长列表都能用

核心思想:只渲染可视区域内的项,滚动时动态替换内容,减少 DOM 数量。

步骤

  1. 固定容器高度,设置 overflow-y: auto
  2. 知道每项高度 itemHeight
  3. 监听 scrollTop
  4. 计算开始索引:startIdx = Math.floor(scrollTop / itemHeight)
  5. 计算结束索引:endIdx = startIdx + visibleCount
  6. 截取数据 list.slice(startIdx, endIdx) 渲染
  7. paddingTop 撑起滚动条高度,模拟长列表
function VirtualList({ list, itemHeight = 50, visibleCount = 10 }) {
  const [scrollTop, setScrollTop] = useState(0);
  const containerHeight = itemHeight * visibleCount;

  const startIdx = Math.floor(scrollTop / itemHeight);
  const endIdx = startIdx + visibleCount;
  const showList = list.slice(startIdx, endIdx);

  return (
    <div
      style={{ height: containerHeight, overflowY: 'auto' }}
      onScroll={e => setScrollTop(e.target.scrollTop)}
    >
      <div style={{ paddingTop: startIdx * itemHeight }}>
        {showList.map((it, i) => (
          <div key={i} style={{ height: itemHeight }}>{it}</div>
        ))}
      </div>
    </div>
  );
}

7. HOC 高阶组件

场景:跨组件复用逻辑(权限、埋点、加载、样式)

好处
高阶复用:不修改组件,直接增强能力
权限、日志、样式、加载都能统一封装
经典 React 逻辑复用模式(Hook 出现前的主流)

// 定义 HOC
function withLoading(WrappedComponent) {
  return function WithLoadingComponent({ loading, ...props }) {
    if (loading) return <div>加载中...</div>;
    return <WrappedComponent {...props} />;
  };
}

// 使用
function List({ data }) {
  return <div>{data}</div>;
}
const ListWithLoading = withLoading(List);

8. Render Props

function MouseRender({ children }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  return (
    <div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })}>
      {children(pos)}
    </div>
  );
}

// 使用
function App() {
  return (
    <MouseRender>
      {({ x, y }) => <div>x:{x}, y:{y}</div>}
    </MouseRender>
  );
}