React Hook 从入门应用到编写自定义 - Everything you need to known about React Hook
Feb 27, 2026•9 minread
More to explore
You might also like...
Good to know:
- 1. Please retain the original link for reference, thank you!
- 2. All resources are collected from the Internet and are used for study purposes only.
- 3. Please do not use for commercial purposes.
- 4. Any question please contact: siranchao@gmail.com
Last update: Feb 27, 2026
前言
每次提及
Hook时,只能说得出useState以及useEffect来。是得好好总结总结,全面认识Hook。本文分两大板块:
Hook基础,官方Hook的使用;Hook,常见Hook的实现。点击查看>>>代码托管地址
Hook 基础
React Hooks的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。React Hooks就是那些钩子。看一个简单的例子:
function BaseHook(){ const [name,setName] = useState('jack'); const handleClick = ()=>{ setName('frank'); } return ( <div> {name} <button onClick={handleClick}>设置姓名</button> </div> )}原本函数组件是无状态的,现在利用
useState Hook,你会发现它拥有了类似Class组件中state了。Hook 解决什么问题
在类组件中有以下几个问题:
render props(渲染属性)或者HOC(高阶组件)来处理;this指向问题。Hook的优点:Hook在function组件中使用,不用维护复杂的生命周期,不用担心this指向问题。Hook 使用规则
Hook,不要在循环、条件判断或者子函数中调用;React的函数组件中调用Hook,不要在其他JavaScript函数中调用。function BaseHook(){ let name,setName; if(true){ [name,setName] = useState('jack'); } const handleClick = ()=>{ setName('frank'); } return ( <div> {name} <button onClick={handleClick}>设置姓名</button> </div> )}编译结果:

这是
eslint插件:eslint-plugin-react-hooks做的校验。useState
useState()用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。const [state, setState] = useState(initialState);特性:
initialState参数只会在组件的初始化渲染中起作用,后续渲染时会被忽略;state需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的state,此函数只在初始渲染时被调用。[1] 实例:
function UseStateHook(){ const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> )}代码解析:
"count"的state变量,初始值为0,后续通过setCount改变它能让视图重新渲染;count=0只会在组件的初始渲染中起作用,后续渲染时会被忽略。[2] 计算
state:function ComputeState(){ const [count,setCount] = useState(()=>{ const newCount = Math.random(); return newCount; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me ComputeState </button> </div> )}useReducer
React本身不提供状态管理功能,通常需要使用外部库,这方面最常用的库是Redux。Redux的核心概念是,组件发出action与状态管理器通信。状态管理器收到action以后,使用Reducer函数算出新的状态,Reducer函数的形式是(state, action) => newState。useReducer是一个用于状态管理的Hook Api。是useState的替代方案。那么
useReducer和useState的区别是什么呢?答案是useState是使用useReducer构建的。const [state, dispatch] = useReducer(reducer, initialState);上面是
useReducer()的基本用法,它接受Reducer函数和状态的初始值作为参数,返回一个数组。数组的第一个成员是状态的当前值,第二个成员是发送action的dispatch函数。计数器示例:
const initialState = 0; const reducer = (state, action) => { switch (action) { case 'increment': return state + 1 case 'decrement': return state - 1 case 'reset': return initialState default: return state }} function UseReducerHook (){ const [count, dispatch] = useReducer(reducer, initialState); return ( <div> <div>Count - {count}</div> <button onClick={() => dispatch('increment')}>增加</button> <button onClick={() => dispatch('decrement')}>减少</button> <button onClick={() => dispatch('reset')}>重置</button> </div> )}由于
Hooks可以提供共享状态和Reducer函数,所以它在这些方面可以取代Redux。但是它没法提供中间件(middleware)这样高级功能。useContext
我们知道
React提供了context,让我们在层级很深的组件中共享状态。在函数组件中就是使用useContext。还是上面那个计数器的案例,我们使用
useContext来改造下:const AppContext = React.createContext({}); const initialState = 0; const reducer = (state, action) => { switch (action) { case 'increment': return state + 1 case 'decrement': return state - 1 case 'reset': return initialState default: return state }} function ShowCount(){ const { count } = useContext(AppContext); return ( <div>Count - {count}</div> )} function Action(){ const { dispatch } = useContext(AppContext); return ( <> <button onClick={() => dispatch('increment')}>增加</button> <button onClick={() => dispatch('decrement')}>减少</button> <button onClick={() => dispatch('reset')}>重置</button> </> )} function UseReducerHook (){ const [count, dispatch] = useReducer(reducer, initialState); return ( <AppContext.Provider value={{ count, dispatch }}> <div> <ShowCount /> <Action /> </div> </AppContext.Provider> )}这里主要是把展示数字和操纵按钮分离成不同的两个组件,并且在父组件中创建一个
context,下发count与dispatch。这样所有子组件,孙子组件都可以共享context中数据。useEffect
useEffect就是一个Effect Hook,给函数组件增加了操作副作用的能力。它跟class组件中的componentDidMount、componentDidUpdate和componentWillUnmount具有相同的用途,只不过被合并成了一个API。useEffect(() => { // Async Action}, [dependencies])Effect的依赖项,只要这个数组发生变化,useEffect()就会执行。第二个参数可以省略,这时每次组件渲染时,就会执行useEffect()。React将在DOM更新之后调用它,React保证了每次运行useEffect的时候,DOM已经更新完毕。示例:
import React,{useState,useEffect} from 'react'; const Person = ({ personId }) => { const [loading, setLoading] = useState(true); const [person, setPerson] = useState({}); useEffect(() => { setLoading(true); fetch(`https://v1/api/people/${personId}/`) // 这是一个虚拟的请求 .then(response => response.json()) .then(data => { setPerson(data); setLoading(false); }); }, [personId]); if (loading === true) { return <p>Loading ...</p>; } return ( <div> <p>You're viewing: {person.name}</p> <p>Height: {person.height}</p> <p>Mass: {person.mass}</p> </div> ); }; function UseEffectHook(){ const [show, setShow] = useState("1"); return ( <> <Person personId={show} /> <div> Show: <button onClick={() => setShow("1")}>button 1</button> <button onClick={() => setShow("2")}>button 2</button> </div> </> )} export default UseEffectHook;Person组件参数personId发生变化,useEffect()就会执行。组件第一次渲染时,useEffect()也会执行。需要清除的 effect
在
class组件中,我们去监听原生DOM事件时会在componentDidMount这个生命周期中去做,因为在这里可以获取到已经挂载的真实DOM。我们也会在组件卸载的时候去取消事件监听避免内存泄露。那么在useEffect中该如何实现呢?useEffect(() => { function handleClick(status) { document.title = `You clicked ${count} times`; } document.body.addEventListener("click",handleClick,false); return function cleanup() { document.body.removeEventListener("click",handleClick,false); };});通过在
useEffect中返回一个函数,它便可以清理副作用。清理规则是:
useCallback
把内联回调函数及依赖项数组作为参数传入
useCallback,它将返回该回调函数的memoized版本,该回调函数仅在某个依赖项改变时才会更新。const memoizedCallback = useCallback( () => { doSomething(a); }, [a],);通俗来讲当参数
a发生变化时,会返回一个新的函数引用赋值给memoizedCallback变量,因此这个变量就可以当做useEffect第二个参数。这样就有效的将逻辑分离出来。针对上述请求数据例子,我们使用
useCallback改写下:const Person = ({ fetchData }) => { const [loading, setLoading] = useState(true); const [person, setPerson] = useState({}); useEffect(() => { setLoading(true); fetchData().then(response => response.json()) .then(data => { setPerson(data); setLoading(false); }); }, [fetchData]); //{2} if (loading === true) { return <p>Loading ...</p>; } return ( <div> <p>You're viewing: {person.name}</p> <p>Height: {person.height}</p> <p>Mass: {person.mass}</p> </div> ); }; function UseEffectHook(){ const [personId, setPersonId] = useState("1"); const fetchData = useCallback(()=>{ return fetch(`https://v1/api/people/${personId}/`); },[personId]);//{1} return ( <> <Person fetchData={fetchData} /> <div> Show: <button onClick={() => setPersonId("1")}>button 1</button> <button onClick={() => setPersonId("2")}>button 2</button> </div> </> )}经过
useCallback包装过的函数可以当作普通变量作为useEffect的依赖。useCallback做的事情,就是在其依赖变化时,返回一个新的函数引用,触发useEffect的依赖变化,并激活其重新执行。现在我们不需要在
useEffect依赖中直接对比personId参数了,而可以直接对比fetchData函数。useEffect只要关心fetchData函数是否变化,而fetchData参数的变化在useCallback时关心,能做到:依赖不丢、逻辑内聚,从而容易维护。useMemo
把"创建"函数和依赖项数组作为参数传入
useMemo,它仅会在某个依赖项改变时才重新计算memoized值。 这种优化有助于避免在每次渲染时都进行高开销的计算。跟
useCallback非常类似的功能,useMemo相当于Class组件的pureComponent。我们再接着上述案例使用
useMemo修改下:const fetchData = useMemo(()=>{ return ()=>fetch(`https://v1/api/people/${personId}/`);},[personId]);其余都不变,唯独多加了一层
()=>fn结构。如果没有提供依赖项数组,
useMemo在每次渲染时都会计算新的值。useRef
学习
useRef之前,我们先搞清楚几个问题。Refs是什么;Refs;forwardRef;Refs。Refs
Refs是一个获取DOM节点或React元素实例的工具。在React中Refs提供了一种方式,允许用户访问DOM节点或render方法中创建的React元素。类组件如何创建 ref
ref属性用于HTML元素时,构造函数中使用React.createRef()创建的ref接收底层DOM元素作为其current属性;ref属性用于自定义class组件时,ref对象接收组件的挂载实例作为其current属性;ref属性,因为他们没有实例。class Child extends React.Component{ render() { return <div>Child</div>; }} class ClassRefComp extends React.Component { constructor(props) { super(props); this.myRef = React.createRef(); } componentDidMount(){ console.log(this.myRef.current); } render() { return ( <> {/* this.myRef.current 获取到 Child 实例 */} <Child ref={this.myRef} /> {/* this.myRef.current 获取 div 元素 */} {/* <div ref={this.myRef} /> */} </> ) }}不能在函数组件上使用
ref属性例如下面做会报错:
function Child2(){ return ( <div>不能在函数组件上使用 ref 属性</div> )} class ClassRefComp extends React.Component { constructor(props) { super(props); this.myRef = React.createRef(); } componentDidMount(){ console.log(this.myRef.current); } render() { return ( <> <Child2 ref={this.myRef} /> </> ) }}Child2是函数组件,不能使用ref属性,因为他们没有实例,解决方案:class组件React.forwardRef进行包装React.forwardRef
let Child2 = (props,ref)=>{ return ( <div ref={ref}>不能在函数组件上使用 ref 属性</div> )} Child2 = React.forwardRef(Child2);代码解释:
React.createRef创建了一个React ref并将其赋值给myRef变量;ref为JSX属性,将其向下传递给<Child2 ref={this.myRef}>;React传递ref给forwardRef内函数(props, ref) => ...,作为其第二个参数;ref参数到<div ref={ref}>,将其指定为JSX属性;ref挂载完成,ref.current将指向<div>DOM 节点。React.forwardRef解决了,函数组件没有实例,无法像类组件一样可以接收ref属性的问题到这里想必你也应该清楚
Refs是什么以及在类组件中如何使用了,至于它的一些其他用法例如"回调Refs"、"高阶组件中转发Refs"这里就不一一讲解了。函数组件如何创建ref
useRef返回一个可变的ref对象,其current属性被初始化为传入的参数(initialValue)。返回的ref对象在组件的整个生命周期内保持不变。useRef的作用:DOM元素的节点;state不能存储跨渲染周期的数据,因为state的保存会触发组件重渲染)。function FuncRefComp(){ const inputRef = useRef(null); const handleChange = ()=>{ console.log(inputRef.current.value); } return ( <input ref={inputRef} type="text" onChange={handleChange} /> )}useImperativeHandle(不常用)
useImperativeHandle可以让你在使用ref时,自定义暴露给父组件的实例值,不能让父组件想干嘛就干嘛;ref这样的命令式代码。useImperativeHandle应当与forwardRef一起使用;ref。useImperativeHandle(ref, createHandle, [deps])示例:
function UseImperativeHandleRef(props, ref) { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return <input ref={inputRef} />;} UseImperativeHandleRef = forwardRef(UseImperativeHandleRef); function UseRefHook(){ const inputRef = useRef(); useEffect(()=>{ console.log(inputRef.current); }) return <UseImperativeHandleRef ref={inputRef} />}在本例中,渲染
<UseImperativeHandleRef ref={inputRef} />的父组件可以调用inputRef.current.focus()。useLayoutEffect(不常用)
useEffect在全部渲染完毕后才会执行;useLayoutEffect会在 浏览器layout之后,painting之前执行;useEffect相同,但它会在所有的DOM变更之后同步调用effect;DOM布局并同步触发重渲染;useLayoutEffect内部的更新计划将被同步刷新;useEffect以避免阻塞视图更新。自定义 Hook
如果函数的名字以
use开头,并且调用了其他的Hook,则就称其为一个自定义Hook。Hook是一种复用状态逻辑的方式,它不复用state本身,事实上Hook的每次调用都有一个完全独立的state。简单的自定义
Hook:function useTitle(title){ useEffect(()=>{ document.title = title; },[title]);}使用:
function CustomHook (){ useTitle('my use title'); return ( <div>CustomHook</div> )}我们来看一个稍微复杂点的例子,通过自定义
hook实现input双向数据绑定。input 实现双向数据绑定
function useBind(initVal){ let [value,setValue] = useState(initVal); let onChange = function(event){ setValue(event.currentTarget.value); } return { value, onChange }}使用:
function CustomHook (){ const valueObj = useBind(""); return <input {...valueObj} />}写到这里应该可以感受到
hook的逻辑复用的能力。对比
HOC,大量使用HOC的情况下让我们的代码变得嵌套层级非常深,使用自定义hook,我们可以实现扁平式的状态逻辑复用,而避免了大量的组件嵌套。既然自定义
Hook这么香,那么有什么优秀的轮子值得我们来深入学习吗?阿里开源的
ahooks。它是一个React Hooks库,致力提供常用且高质量的Hooks。首先安装:
npm install ahooks --saveimport React from "react";import { useToggle } from "ahooks"; function AHooks(){ const [ state, { toggle } ] = useToggle(); return ( <div> <p>Current Boolean: {String(state)}</p> <p> <button onClick={() => toggle()}>Toggle</button> </p> </div> );} export default AHooks;它的使用可以自行查阅文档,我们今天挑选几个常用
Hook来分析其源码。useUpdate
强制组件重新渲染的
hookconst useUpdate = () => { const [, setState] = useState(0);//{1} return useCallback(() => setState((num) => {return num + 1}));//{2}};分析:
setState方法则会导致state变化,从而刷新组件。useUpdateEffect
一个只在依赖更新时执行的
useEffect hook。使用上与useEffect完全相同,只是它忽略了首次渲染,且只在依赖项更新时运行。const useUpdateEffect = (effect, deps) => { const isMounted = useRef(false); //{1} useEffect(() => { // {2} if (!isMounted.current) { isMounted.current = true; } else { return effect(); } }, deps); };解析:
useRef存一个布尔值;useEffect时,设置其值为true。再次执行时就可以执行effect回调函数。usePersistFn
持久化
function的Hook,在某些场景中,你可能会需要用useCallback记住一个回调,但由于内部函数必须经常重新创建,记忆效果不是很好,导致子组件重复render。对于超级复杂的子组件,重新渲染会对性能造成影响。通过usePersistFn,可以保证函数地址永远不会变化。function usePersistFn(fn) { const ref = useRef(() => { throw new Error('Cannot call function while rendering.'); }); ref.current = fn; const persistFn = useCallback(((...args) => ref.current(...args)), [ref]); return persistFn; }解析:
useCallback的第一个参数传入ref,由于ref在整个生命周期内是不会发生变化的,因此useCallback的返回值不会更新。useMount and useUnmount
组件挂载和组件卸载生命周期
Hook。使用示例:
const MyComponent = () => { useMount(() => { console.log('mount'); // 挂载时触发 }); useUnmount(() => { console.log('unmount'); // 卸载时触发 }); return <div>Hello World</div>; }; function CustomHook (){ const [state, { toggle }] = useToggle(false); return ( <div> <button type="button" onClick={() => toggle()}> {state ? 'unmount' : 'mount'} </button> {state && <MyComponent />} </div> )}源码分析:
# mountconst useMount = (fn) => { const fnPersist = usePersistFn(fn); useEffect(() => { if (fnPersist && typeof fnPersist === 'function') { fnPersist(); } }, []);}; # unmountconst useUnmount = (fn) => { const fnPersist = usePersistFn(fn); useEffect( () => () => { if (fnPersist && typeof fnPersist === 'function') { fnPersist(); } }, [], );};以上是稍微简单的
hook编写,通过编写这些hook,至少让我们知道,自定义hook并没有想象的那么复杂,接下来看几个有点难度的hook。useDebounce
用来处理防抖值的
Hook。示例:
DebouncedValue只会在输入结束500ms后变化。import React,{useState} from "react";import {useDebounce} from "./customHooks"; function DebounceHook (){ const [value, setValue] = useState(); const debouncedValue = useDebounce(value, 500 ); return ( <div> <input value={value} onChange={(e) => setValue(e.target.value) } placeholder="Typed value" style={{ width: 280 }} /> <p style={{ marginTop: 16 }}>DebouncedValue: {debouncedValue}</p> </div> )} export default DebounceHook;实现:
const useDebounceFn = (fn,wait)=>{ const _wait = wait || 0; const timer = useRef(); const fnRef = useRef(fn); fnRef.current = fn; const cancel = useCallback(()=>{ if(timer.current){ clearTimeout(timer.current); } },[]) const run = useCallback((...args)=>{ cancel(); timer.current = setTimeout(()=>{ fnRef.current(...args); },_wait) },[_wait,cancel]) useEffect(()=> cancel,[]); return { run, cancel } } const useDebounce =(value,wait)=>{ const [debounced, setDebounced] = useState(value); const { run } = useDebounceFn(() => { setDebounced(value); }, wait); useEffect(() => { run(); }, [value]); return debounced; }解析:
useDebounce收到value值变化时,会调用useEffect钩子;useEffect钩子调用run方法,也就是useDebounceFn这个钩子返回的;run方法中实现了防抖的核心功能。useThrottle
用来处理节流值的
Hook。节流的处理,一定时间内只触发一次。示例:
ThrottledValue每隔500ms变化一次。import React,{useState} from "react";import {useThrottle} from "./customHooks"; function ThrottleHook (){ const [value, setValue] = useState(); const throttledValue = useThrottle(value, 500 ); return ( <div> <input value={value} onChange={(e) => setValue(e.target.value)} placeholder="Typed value" style={{ width: 280 }} /> <p style={{ marginTop: 16 }}>throttledValue: {throttledValue}</p> </div> )} export default ThrottleHook;实现:
const useThrottleFn = (fn,wait)=>{ const _wait = wait || 0; const timer = useRef(); const fnRef = useRef(fn); fnRef.current = fn; const currentArgs = useRef([]); const cancel = useCallback(()=>{ if(timer.current){ clearTimeout(timer.current); } timer.current = undefined; },[]) const run = useCallback((...args)=>{ currentArgs.current = args; if(!timer.current){ timer.current = setTimeout(()=>{ fnRef.current(...args); timer.current = undefined; },_wait) } },[_wait,cancel]) useEffect(()=> cancel,[]); return { run, cancel }} const useThrottle =(value,wait)=>{ const [throttled, setThrottled] = useState(value); const { run } = useThrottleFn(() => { setThrottled(value); }, wait); useEffect(() => { run(); }, [value]); return throttled;}解析:
useThrottle收到value值变化时,会调用useEffect钩子;useEffect钩子调用run方法,也就是useThrottleFn这个钩子返回的;run方法中实现了节流的核心方法,每次判断定时器是否开启,如果没有开启则开启定时器执行函数;总结
通过本文,希望您可以快速掌握官方提供的常用
Hook的使用,以及编写自定义Hook进行逻辑封装。