Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用只能在 React 的函数组件中调用 Hook。
- 不要在其他 JavaScript 函数中调用。
useState
useState 向组件中添加状态变量
状态是只读的,不可以直接修改。函数组件,每个更新函数从新执行,state 被重置,而不是被修改。state 可以理解为 readOnly。
所以修改的时候,解构很常用。此外,如果使用函数进行修改,一定要返回符合重置数据操作。
比如:Array.concat() 返回一个新数组,刚好将之前的数组覆盖掉就是可以的;而 Array.push() 虽然也是对数组新增了元素,但是它返回的是新的数组的长度,将长度 set 到了新的 state 中,明显事与愿违,是不可以的。
对于对象类型的状态变量,应该传递一个新的对象来更改
需要对象展开,并重新赋值,进行增加或者修改。
如果需要删除,则使用 filter。
import { useState } from "react";
function App() {
const [arr, setArr] = useState([
{ id: 1, name: 'zhangsan' },
{ id: 2, name: 'lisi' },
{ id: 3, name: 'wangwu' }
]);
const content = arr.map((item, index) => {
return <li key={item.id}>{item.name}</li>
})
const deleteVar = () => {
setArr(arr.filter(item => item.name !== 'zhangsan'))
// 从这里就可以看出 setXxx 里可以穿入一个函数比如:setCount(count=>count+1),这就是另一种写法
}
return (
<>
<ul>
{content}
</ul>
<button onClick={deleteVar}>点击删除zhangsan</button>
</>
)
}
export default App
这里有一点尤其需要注意, state 更新是异步的。也就是说当 state 修改时,无法立刻取到新值的问题(state 的异步更新)。
解决办法:
使用 useRef ,useRef.current = useState 详情可见:useState 无法获取最新的值 。 相当于使用 useRef 来保存更新后的值,然后获取直接从 useRef 来获取。
const [current, setCurrent] = useState('sub1');
const currentRef = useRef(current);
const onClick = (e) => {
console.log('click ', e);
currentRef.current = e.key;
setCurrent(currentRef.current);
props.getCurrent(currentRef.current);
};
这里比如使用 setTimeout 等异步函数时,不能及时获取到最新的值,也是相同的原因,解决办法也都是通过 useRef 。这个问题我们成为闭包陷阱。
出现这个问题的主要原因是 state 是一个值类型,而 ref 是一个引用类型,所以涉及异步操作的时候,引用地址肯定是不变的,所以值类型还是旧值,引用类型由于是得到的地址,不会固定属性的变化,还是可以获得最新的值。
如果说一个变量不用于JSX 中显示(页面组件中显示),那就不要用 useState 来管理它,用 useRef 。
还有一个需要注意的点是合并更新,这里可以使用函数的写法解决,因为函数只有在执行了之后才会获取值,直接赋值相当于值的直接覆盖:
我们还可以使用 immer 操作 state,进行更加直观的修改操作:
npm i immer
const [count, setCount] = useState({name: 'hh'})
const handleCount = () => {
setCount(produce(draft => {
draft.name = 'jj'
}))
}
同时也解决了比如我们上面提到的 关于 Array.push() 的问题,在 immer 中都是可以直接使用的。
useReducer
让 React 管理多个相对关联的状态数据
import { useReducer } from "react"
// 1. 定义reducer函数,根据不同的action返回不同的状态
function reducer(state, action) {
switch (action.type) {
case 'ADD':
return state + action.payload
case 'SUB':
return state - 1
default:
return state
}
}
function App() {
// 2. 组件中调用 useReducer, 0 是初始化参数
const [state, dispatch] = useReducer(reducer, 0)
return (
<div className="App">
{state}
{/* 3. 调用dispatch 产生一个新的状态,匹配事件(可传参) 更新 UI */}
<button onClick={() => { dispatch({ type: 'ADD', payload:100 }) }}>+</button>
<button onClick={() => { dispatch({ type: 'SUB' }) }}>-</button>
</div>
)
}
export default App;
useRef
- 进行数据存储 值可以手动修改 不是响应式数据
- 获取 dom 节点
数据存储
import {useRef, useState} from "react";
function App() {
const [count, setCount] = useState(0)
const preCount = useRef()
function handleClick() {
preCount.current = count
setCount(count + 1)
}
return (
<>
<p>最新的 count:{count}</p>
<p>上次的 count:{preCount.current}</p>
<button onClick={handleClick}>增加count</button>
</>
)
}
export default App;
这里需要注意,该组件的重新渲染是因为 state 发生了变化,导致重新渲染,进而 ref 的值获取到最新的值,渲染到页面上。如果该组件并没有使用 state ,即使 ref 改变,组件也并不会更新,获取不到 ref 的值。
获取 dom 节点
import {useRef, useState} from "react";
function App() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}
export default App;
获取组件
import {forwardRef, useImperativeHandle, useRef} from "react";
// 默认子组件不对外开放自身的功能 需要 forwardRef 进行包裹
const Child = forwardRef(function (props, ref) {
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
myFn: () => {
console.log('子组件myFn方法')
}
}))
return (
<div>Child 子组件</div>
)
})
function App() {
const childRef = useRef();
function handleClick() {
childRef.current.myFn();
}
return (
<>
<Child ref={childRef}/>
<button onClick={handleClick}>
按钮
</button>
</>
);
}
export default App;
useContext
多层级通信 - useContext
官方文档:useContext
import {createContext, useContext, useState} from "react";
function Section({children}) {
const level = useContext(LevelContent)
return (
<section className="section">
{/*逐渐从上层提供的LevelContext中取值*/}
<LevelContent.Provider value={level + 1}>
{/*children 也就是 heading 需要使用 useContext*/}
{children}
</LevelContent.Provider>
</section>
);
}
function Heading({children}) {
const level = useContext(LevelContent)
switch (level) {
case 1:
return <h1>{children}</h1>;
case 2:
return <h2>{children}</h2>;
case 3:
return <h3>{children}</h3>;
case 4:
return <h4>{children}</h4>;
case 5:
return <h5>{children}</h5>;
case 6:
return <h6>{children}</h6>;
default:
throw Error('未知的 level:' + level);
}
}
const LevelContent = createContext(0)
function App() {
return (
<Section>
<Heading>主标题</Heading>
<Section>
<Heading>副标题</Heading>
<Heading>副标题</Heading>
<Heading>副标题</Heading>
<Section>
<Heading>子标题</Heading>
<Heading>子标题</Heading>
<Heading>子标题</Heading>
<Section>
<Heading>子子标题</Heading>
<Heading>子子标题</Heading>
<Heading>子子标题</Heading>
</Section>
</Section>
</Section>
</Section>
)
}
export default App;
useEffect
useEffect 在组件中创建由渲染本身引起的操作(如发送 Ajax 请求,更改 DOM 等),即非用户操作。
副作用函数随着依赖项的触发而执行。
纯函数与副作用函数相对,结果是可控可预期的,比如:
function test(x) { return x * 2 }
常见的副作用有发送网络请求,添加监听的注册和取消注册,手动修改 DOM,之前写在生命 周期里,现在写入 useEffect 中。
清理副作用一般在组件卸载时执行,也就是说先清理之前的Effect 副作用函数,再执行新的 Effect 副作用。
useEffect(() =>{
// 实现副作用逻辑(组件创建或者更新时执行)
return ()=> {
// 清除副作用逻辑(组件销毁时执行)
}
}, [] )
import {forwardRef, useEffect, useImperativeHandle, useRef, useState} from "react";
function App() {
const [count, setCount] = useState(0)
const handleIncrement = ()=>setCount(count + 1)
const handleDecrement = ()=>setCount(count - 1)
useEffect(() => {
console.log('useEffect')
}, [count]);
return (
<>
<div style={{ padding: 10}}>
<button onClick={handleIncrement}>+</button>
<span> {count} </span>
<button onClick={handleDecrement}>-</button>
</div>
</>
);
}
export default App;
React18 开始,useEffect
在开发环境下执行两次。
模拟组件挂载、销毁、重新挂载的完整流程,及早发现后续的问题。如果只挂载一次,有可能卸载组件时有问题。
而且,实际项目中某些组件真的有可能会被挂载很多次(如重置 state),要及早模拟这种情况,避免出现重复挂载的问题(如弹窗重复、bindEvent 重复)
生产环境下,不会再执行两次。
useMemo
组件重新渲染时缓存计算的结果。
实例:count1计算斐波那契数列,count2和count1可以触发数值变化。使用memo可以使只有在count1变化时触发斐波那契数列计算函数,而count2变化时不触发斐波那契数列计算函数。
import { useMemo } from "react";
import { useState } from "react";
function fib(n) {
console.log('计算函数执行')
if (n < 3) {
return 1
}
return fib(n - 1) + fib(n - 2)
}
function App() {
const [count1, setCount1] = useState(0)
const [count2, setCount2] = useState(0)
console.log('组件重新渲染')
const result = useMemo(() => {
return fib(count1)
}, [count1])
return (
<div className="App">
<button onClick={() => { setCount1(count1 + 1) }}>change count1: {count1}</button>
<button onClick={() => { setCount2(count2 + 1) }}>change count2: {count2}</button>
result: {result}
</div>
)
}
export default App
useCallback
useCallback 缓存函数
应用场景:之前做的一个项目需要在首页就获取登录用户的数据,然后存到 store 中,供全局使用。此时使用 useCallback 就可以保证获取数据的函数只在进入首页时加载一次,提高性能。
import { memo, useCallback, useState} from "react";
// memo 将组件变更为记忆组件
// 如果向组件传入的 prop 没有发生变化 将不会收到外部父组件的影响
const Button = memo(function ({onClick}) {
console.log('Button渲染了')
return <button onClick={onClick}>点击触发子组件</button>
})
function App(callback, deps) {
const [count, setCount] = useState(0)
// 此时不仅需要 memo 还需要将函数设为useCallback 缓存函数
// 因为父组件重新渲染 那么handleClick 将被分配一块新的内存地址 和之前的不是同一个函数
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleClick = useCallback(() => {
console.log('点击按钮')
},[])
const handleUpdate = () => {
setCount(count + 1)
}
return (
<>
<div>
<p>Count: {count}</p>
<button onClick={handleUpdate}>点击更新count</button>
<Button onClick={handleClick}></Button>
</div>
</>
);
}
export default App;
自定义 hook
自定义 hook 能够调用诸如 useState,useRef 等,普通函数则不能,一般使用 use 开头。
import {useState} from "react";
function useMyBook(props) {
const [bookName, setBookName] = useState('React 学习')
return {
bookName, setBookName
}
}
export default useMyBook;
import React, {Component, useState} from 'react';
import Money from "./components/Money";
import useMyBook from "./useMyBook";
function App() {
const {bookName, setBookName } = useMyBook()
const [value, setValue] = useState('')
const handleChange = (e) => {
setValue(e.target.value)
}
const handleClick = () => {
setBookName(value)
}
return (
<div>
<div>{bookName}</div>
<input type="text" value={value} onChange={handleChange} />
<button onClick={handleClick}>确定</button>
</div>
);
}
export default App;