React 手写题

函数式组件 + Hooks 为主

1 Tab 组件

接收 items 数组,点击切换 active tab
import { useState } from 'react'

const Tab = ({ items }) => {
  const [activeKey, setActiveKey] = useState(items[0].key)

  return (
    <div>
      <div>
        {items.map((item) => (
          <button
            key={item.key}
            onClick={() => setActiveKey(item.key)}
            style={{ backgroundColor: activeKey === item.key ? 'green' : 'gray' }}
          >
            {item.label}
          </button>
        ))}
      </div>
      <div>
        {items.find((item) => item.key === activeKey).children}
      </div>
    </div>
  )
}

2 useCount 自定义 Hook

const { num, add, cut } = useCount(0)
import { useState, useCallback } from 'react'

function useCount(initialValue) {
  const [num, setNum] = useState(initialValue)

  // useCallback 防止每次 render 都产生新函数引用,避免子组件无效渲染
  const add = useCallback(() => {
    setNum(prev => prev + 1) // 函数式更新避免闭包陷阱
  }, [])

  const cut = useCallback(() => {
    setNum(prev => prev - 1)
  }, [])

  return { num, add, cut }
}

3 usePrevious

返回 state 的上一个值(useRef + useEffect)
  • useEffect 在渲染之后执行,所以 return 的 ref.current 还是旧值
import { useEffect, useRef } from 'react'

function usePrevious(state) {
  const ref = useRef()

  // useEffect 在渲染之后执行,所以 return 时 ref.current 还是旧值
  useEffect(() => {
    ref.current = state // 渲染完成后才更新为新值
  }, [state])

  return ref.current // 返回的是本次渲染前的值
}

4 输入框 + 防抖

即时 setValue + useMemo 包裹防抖函数

题干:实现一个搜索输入框组件,要求:输入时不卡顿(即时响应),但停止输入 500ms 后才发送请求(防抖)。

  • 坑 1:debounce 要用 useMemo 包裹,否则每次 render 都创建新函数
  • 坑 2:要即时 setVal 让输入框不卡,防抖只控制请求发送
import { useState, useMemo } from 'react'

const debounce = (fn, delay) => {
  let timer = null
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

function Comp() {
  const [val, setVal] = useState('')
  // 要用 useMemo 包裹,否则每次 setVal 都会重新执行函数组件,产生新的 fnDeb
  const fnDeb = useMemo(
    () => debounce((value) => {
      console.log(`发请求: ${value}`)
    }, 500),
    []
  )

  const handleChange = (e) => {
    const value = e.target.value
    setVal(value)   // 即时 setVal,输入框不会卡住
    fnDeb(value)    // 防抖只控制请求发送
  }

  return <input onChange={handleChange} value={val} />
}

5 倒计时组件 + Hook 封装

setInterval + 函数式更新避免闭包陷阱

题干:实现一个倒计时组件 <CountDown time={60} />,传入秒数,每秒递减显示,归零停止。要求封装为 useCountDown Hook。

import { useState, useEffect } from 'react'

function useCountDown(initialTime) {
  const [remainTime, setRemainTime] = useState(initialTime)

  useEffect(() => {
    setRemainTime(initialTime)
    const timer = setInterval(() => {
      // 用函数式更新 prev => ... 避免闭包陷阱(不依赖外部 remainTime)
      setRemainTime(prev => {
        if (prev > 0) return prev - 1
        clearInterval(timer)
        return 0
      })
    }, 1000)
    return () => clearInterval(timer)
  }, [initialTime])

  return remainTime
}

const CountDown = ({ time }) => {
  const remainTime = useCountDown(time)
  return <p>剩余 {remainTime} 秒</p>
}