发布于 2026-01-06 2 阅读
0

用通俗易懂的方式解释 React Hooks(?)

用通俗易懂的方式解释 React Hooks(?)

我用一种……我也不知道自己多大年纪的方式解释 React Hooks(是的,不止一个),但我会尽量用通俗易懂的例子来解释清楚。我写这篇文章是为了更好地理解和学习它们,因为我之前觉得它们有点让人困惑。我其实不是 React开发者,所以如果我说错了什么,请告诉我👇

希望这也能帮助你更好地理解 React Hooks!

什么是钩子?

React v16.8.0 版本正式引入了 Hooks。Hooks 不能在类组件中使用,而是用于函数组件。这并不意味着你不能再编写类组件,但我认为从现在开始,我们应该主要使用带有 Hooks 的函数组件。

功能组件示例

import React from 'react';

function MyComponent(){
    return (
        <h1>Hi friends!</h1>
    )
}

export default MyComponent;

请记住,在函数式组件中,每次状态改变时都会调用该函数,因此该函数会运行多次。

共有 10 个钩子(我们还会介绍如何创建自定义钩子)。您可以像这样导入要使用的钩子:

import { 
    useState, 
    useEffect, 
    createContext, 
    useContext, 
    useReducer, 
    useCallback, 
    useMemo, 
    useRef, 
    useImperativeHandle, 
    useLayoutEffect, 
    useDebugValue 
} from 'react';

React 文档`<h1>`、`<h2>` 和 ` <h3> ` 归类为基本 hooks useState而其余的则被视为附加 hooks。useEffectuseContext

使用状态

useState用于处理组件中的响应式值。该钩子会返回一个有状态的值,以及一个用于更新该值的函数。

const [person, setPerson] = useState({ name: 'Gaute', age: 28 });

需要将整个对象传递给更新函数。使用扩展语法可以简化这一过程。

完整示例:

import React, { useState }  from 'react';

function State(){
    const [person, setPerson] = useState({ name: 'Gaute', age: 28 });

    const birthday = () => {
        setPerson({ ...person, age: person.age + 1 });
    }

    return (
        <>
            <h1>{person.name}, {person.age}</h1>
            <button onClick={birthday}>Age</button>
        </>
    )
}

export default State;

使用效果

由于组件函数会多次重复运行,如何防止代码陷入无限循环?` useEffectthis` 用于变更、订阅、定时器、日志记录和其他副作用。你需要定义钩子函数在哪些值处触发。

`useEffect` 方法有两个参数,第一个参数是要运行的函数,第二个参数是一个数组,其中包含它监听的值的变化,并在变化时重新运行。它返回一个方法,该方法将在组件离开屏幕时被调用。

空数组仅用于运行一次。

useEffect(() => {
    console.log('Runned once at the beginning');
}, []);

完整示例:

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

function Effect() {
    const [person, setPerson] = useState({ name: 'Gaute', age: 28 });

    const birthday = () => {
        setPerson({ ...person, age: person.age + 1 });
    }

    useEffect(() => {
        console.log('Run once at the beginning');
        return () => console.log('Component leaves');
    }, []);

    useEffect(() => {
        console.log('Run when person changes', person);
    }, [person]);

    return (
        <>
            <h1>{person.name}, {person.age}</h1>
            <button onClick={birthday}>Age</button>
        </>
    )
}

export default Effect;

useContext

useContext可用于在所有子组件之间共享值/状态。调用此方法的组件useContext会在上下文值更改时始终重新渲染。

createContext让我们使用.创建一个用于描述我们上下文的文件。

likesContext.js

import { createContext } from 'react';

const LikesContext = createContext();

export default LikesContext;

然后我们将有一个提供程序组件,用于设置初始值并保存状态,该状态可供所有子组件使用。

likesProvider.js

import React, { useState } from 'react';
import LikesContext from './likesContext';
import LikesConsumer from './likesConsumer';

function LikesProvider() {
    const [likes, setLikes] = useState(0);
    return (
        <LikesContext.Provider value={{ likes, setLikes }}>
            <LikesConsumer />
        </LikesContext.Provider>
    )
}

export default LikesProvider;

然后我们可以创建子组件useContext,子组件将使用上下文从最近的父组件获取值。

likesConsumer.js

import React, { useContext } from 'react';
import LikesContext from './likesContext';

function LikesConsumer() {
    const { likes, setLikes } = useContext(LikesContext);

    return (
        <>
            <span>Likes: {likes}</span>
            <button onClick={() => setLikes(likes + 1)}>+1</button>
        </>
    )
}

export default LikesConsumer;

如果多个消费者使用同一服务提供商,你会发现他们更新的是同一个状态。

使用 Reducer

useReduceruseState当你需要更复杂的设置器时,这是一个替代方案。useReducer它接受一个用于改变状态的函数和一个初始值作为参数,并返回一个有状态的值和一个用于更新该状态的函数(调用作为第一个参数提供的函数)。

const [statefulValue, updateValue] = useReducer((previousValue, inputToUpdateValue) => previousValue + inputToUpdateValue, 'initial value');

//statefulValue: 'initial value'
updateValue(' abc');
//statefulValue: 'initial value abc'
updateValue(' 123');
//statefulValue: 'initial value abc 123'

这可能有点令人困惑,但这里有一个完整的示例,说明如何使用关键字更改状态以及如何为状态设置 setter 方法。

示例包含一个用于更新数字数组的 reducer 和一个用于将文本设置为小写的 reducer

import React, { useReducer } from 'react';

const reduce = (prevState, action) => {
    switch(action){
        case 'grow':
            return prevState.map(g => g + 1);
        case 'cut': 
            return prevState.map(_ => 0);
        case 'buy':
            return [...prevState, 0];
        default:
            return prevState;
    }
}

function Reduce() {
    const [grass, dispatch] = useReducer(reduce, []);
    const [name, setName] = useReducer((_, value) => value.toLowerCase(), '');

    return (
        <>
            <button onClick={() => dispatch('grow')}>Grow</button>
            <button onClick={() => dispatch('cut')}>Cut</button>
            <button onClick={() => dispatch('buy')}>Buy</button>
            {grass.join()}

            <input type="text" onChange={e => setName(e.target.value)}/> {name}
        </>
    )
}

export default Reduce;

使用回调

useCallback该方法将被缓存,而不会在组件函数每次重新运行时都重新创建它。这样做是为了提高性能。第一个参数是回调函数,第二个参数是一个依赖项数组,用于指定回调函数何时应该更新(例如useEffect)。

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

假设我们有一个用于计数秒数的组件。该组件的函数会被频繁调用。我们可以通过避免在每次渲染时都重新创建其他函数来提高性能。

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

function Callback() {
    const [seconds, setSeconds] = useState(0);
    const [face] = useState('😎');

    useEffect(() => {
        setTimeout(() => setSeconds(seconds + 1), 1000);
    }, [seconds]);

    //method recreated on every render
    const saySomethingTired = () => {
        console.log(`I'm tired 🥱`);
    }

    //Don't recreate me every time
    const saySomethingCool = useCallback(
        () => console.log(`You are cool ${face}`),
        [face]
    );

    return (
        <>
            <h1>{seconds}</h1>
            <button onClick={saySomethingTired}>Tired</button>
            <button onClick={saySomethingCool}>Cool</button>
        </>
    )
}

export default Callback;

我当时在想,为什么不把这种方法应用到所有方法中呢?答案是,并非总是值得的

使用备忘录

几乎和方法类似useCallback,但处理的是值而不是方法。也与Vue 中的计算属性有些类似。第一个参数是一个返回值的函数,第二个参数是一个依赖项数组,用于指定回调函数何时应该更新(例如useEffect)。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

假设我们有一个数组,想要将其排序后显示给用户。如果数组中还有其他值,例如计时器,导致组件函数运行多次,我们不希望每次都执行排序。这时,我们可以useMemo只依赖数组本身。

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

function Memo() {
    const [seconds, setSeconds] = useState(0);
    const [colors, setColors] = useState([{ name: 'red', code: '#ff0000' }, { name: 'blue', code: '#0000ff' }]);

    useEffect(() => {
        setTimeout(() => setSeconds(seconds + 1), 1000);
    }, [seconds]);

    const sortedColors = useMemo(
        () => colors.sort((a, b) => a.name.localeCompare(b.name)).map(c => c.code).join(', '),
        [colors]
    );

    return (
        <>
            <h1>{seconds}</h1>
            <p>{sortedColors}</p>
            <button onClick={() => setColors([...colors, { name: 'green', code: '#008000'}])}>Add green</button>
        </>
    )
}

export default Memo;

使用引用

useRef用于保存组件生命周期内始终存在的值,但修改该值时不会重新渲染。值存储在 `<value>` 中.current。它可以与 `<value>` 属性一起使用ref,以保存 DOM 元素。

示例:从输入元素复制值:

import React, { useRef } from 'react';

function Ref() {
    const inputEl = useRef();

    const copy = () => {
        inputEl.current.select();
        document.execCommand("copy");
    }

    return (
        <>
            <input type="text" ref={inputEl}/>
            <button onClick={copy}>Copy</button>
        </>
    )
}

export default Ref;

包含 setInterval 对象的示例:

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

function Ref() {
    const intervalRef = useRef();

    useEffect(() => {
        intervalRef.current = setInterval(() => {
            console.log('time has passed');
        }, 1000);
        return () => {
            clearInterval(intervalRef.current);
        };
    }, []);

    const stopCounting = () => clearInterval(intervalRef.current);

    return (
        <button onClick={stopCounting}>Stop</button>
    )
}

export default Ref;

使用命令句柄

useImperativeHandle用于自定义在使用 forwardRef 时向父级暴露的值。这应该与forwardRefref一起使用

child.js

import React, { useImperativeHandle } from 'react';

function Child(props, ref) {
    useImperativeHandle(ref, () => 'Some value');

    return <h1>Hello</h1>
}

export default React.forwardRef(Child);

parent.js

import React, { useRef, useEffect } from 'react';
import Child from './child';

function Parent() {
    const childRef = useRef();

    useEffect(() => {
        console.log(inputEl.current); 
        //output: 'Some value'
        //Not DOM element anymore
    }, []);

    return <Child ref={childRef}/>
}

export default Parent;

让我们沿用之前的例子useRef,但现在我们想把输入元素移动到一个包含更多元素的组件中。useImperativeHandle可以使用 `<input>` 属性将输入 DOM 元素暴露给父组件,从而保持复制方法的简洁性。

myInput.js

import React, { useRef, useImperativeHandle } from 'react';

function MyInput(props, ref) {
    const inputEl = useRef();

    useImperativeHandle(ref, () => inputEl.current);

    return (
        <>
            <span className="decoration">🦄</span>
            <input type="text" ref={inputEl}/>
        </>
    )
}

export default React.forwardRef(MyInput);

parent.js

import React, { useRef } from 'react';
import MyInput from './myInput';

function Parent() {
    const inputEl = useRef();

    const copy = () => {
        inputEl.current.select();
        document.execCommand("copy");
    }

    return (
        <>
            <MyInput ref={inputEl}/>
            <button onClick={copy}>Copy</button>
        </>
    )
}

export default Parent;

使用布局效果

useLayoutEffectuseEffect它的工作方式与 `$($($($($($($($($($($($($($($($($($( $( $( $( $( useEffect$( $( $( $( $( $( $( $( $( $($( $( $ useLayoutEffect( $( $( $( $( $( $( $( $( $ $( $( $ $( useLayoutEffect$ useEffect) useEffect...

这里有一个更改文本和背景颜色的示例。如果您useEffect仔细观察,会发现页面快速闪烁,因为浏览器会先更新文本,然后再更新背景颜色。而使用其他方法,useLayoutEffect它们会同时更新。

import React, { useState, useLayoutEffect, useRef } from 'react';

const quotes = [
    { text: 'The secret of getting ahead is getting started', color: 'blue' },
    { text: `Your limitation - It's only your imagination`, color: 'red' },
];

function LayoutEffect() {
    const [toggle, setToggle] = useState(true);
    const quoteRef = useRef();

    useLayoutEffect(() => {
        quoteRef.current.style.backgroundColor = quotes[toggle ? 0 : 1].color;
    }, [toggle]);

    return (
        <>
            <span ref={quoteRef}>{quotes[toggle ? 0 : 1].text}</span>
            <button onClick={() => setToggle(!toggle)}>Give me a new quote</button>
        </>
    )
}

export default LayoutEffect;

使用调试值

最后一个钩子。这个钩子只用于自定义钩子。所以我们先来看这个。

定制钩子

您可以创建自定义钩子,将逻辑从组件中移出,重用代码,以及/或者将其他钩子合并到一个钩子中。要做到这一点,请创建一个以 `.` 开头的函数use

这里有一个示例,用于useState保存useMemo一个家庭的值并按排序后返回。因此,使用该钩子的组件只需要知道家庭的值和添加方法即可。

useFamily.js

import { useState, useMemo } from 'react';

function useFamily(initialFamily) {
    const [persons, setPersons] = useState(initialFamily);

    const family = useMemo(
        () => persons.sort((a,b) => a.age - b.age),
        [persons]
    );

    const add = (person) => setPersons([...persons, person]);

    return {family, add};
}

export default useFamily;

kryptonFamily.js

import React from 'react';
import useFamily from './useFamily';

function Krypton() {
    const {family, add} = useFamily([{ name: 'Jor-El', age: 40 }, { name: 'Lara', age: 39 }]);

    return (
        <>
            <ul>
                {family.map(p => 
                    <li key={p.name}>Name: {p.name}, Age:{p.age}</li>
                )}
            </ul>
            <button onClick={() => add({ name: 'Kal-El', age: 0 })}>
                New Member
            </button>
        </>
    )
}


export default Krypton;

返回 useDebugValue

useDebugValue现在可以使用此功能在 React DevTools 中显示自定义 hooks 的标签。如果您已下载浏览器扩展程序(ChromeFirefox)并打开浏览器开发者工具(按 F12),React DevTools 将显示此功能。

现在我们可以添加标签,让我们知道家庭成员的数量。

useFamily.js

import { useState, useMemo, useDebugValue } from 'react';

function useFamily(initialFamily) {
    const [persons, setPersons] = useState(initialFamily);

    const family = useMemo(
        () => persons.sort((a,b) => a.age - b.age),
        [persons]
    );

    const add = (person) => setPersons([...persons, person]);

    useDebugValue(`Members: ${persons.length}`);
    return {family, add};
}

export default useFamily;

因此,我们可以在开发者工具中看到这些信息:
React Devtools 中的调试值

结论

这里有 10 个默认钩子以及一些自定义钩子。有些比较容易理解,有些比较复杂,有些你会经常用到,有些则用不到。但了解它们很重要,这样你才能更好地决定在哪里使用哪个钩子。

文章来源:https://dev.to/gautemeekolsen/explain-react-hooks-like-im-1nkp