用通俗易懂的方式解释 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 的标签。如果您已下载浏览器扩展程序(Chrome、Firefox)并打开浏览器开发者工具(按 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;
结论
这里有 10 个默认钩子以及一些自定义钩子。有些比较容易理解,有些比较复杂,有些你会经常用到,有些则用不到。但了解它们很重要,这样你才能更好地决定在哪里使用哪个钩子。
文章来源:https://dev.to/gautemeekolsen/explain-react-hooks-like-im-1nkp