Hooks:useState、useEffect、useLayoutEffect、useContext、useReducer、useMemo、React.memo、callCallback、useRef、useImperativeHandle、自定义Hook、useDebugValue
useState(最常用)
在React的函数组件里,默认只有属性,没有状态。
使用状态
//数组第1项是读接口,第2项是写接口,初始值0
const [n,setN] = React.useState(0) //数字
const [user,setUser] = React.useState({name:'F'}) //对象
注意事项
1.不可局部更新
如果state是一个对象,是不能部分 setState 的。 因为setState不会帮我们合并属性。所以当只更新部分属性时,未更新的属性就会消失。
那怎么解决"未更新的属性会消失"的问题?
import React, {useState} from "react";
import ReactDOM from "react-dom";
function App() {
const [user,setUser] = useState({name:'Frank', age: 18})
const onClick = ()=>{
setUser({
name: 'Jack'
})
}
return (
<div className="App">
<h1>{user.name}</h1>
<h2>{user.age}</h2>
<button onClick={onClick}>Click</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
当点击按钮
用...拷贝之前所有的属性,然后再覆盖属性。
import React, {useState} from "react";
import ReactDOM from "react-dom";
function App() {
const [user,setUser] = useState({name:'Frank', age: 18})
const onClick = ()=>{
setUser({
...user, //拷贝user的所有属性
name: 'Jack' //覆盖name
})
}
return (
<div className="App">
<h1>{user.name}</h1>
<h2>{user.age}</h2>
<button onClick={onClick}>Click</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
题外话:useReducer也不会合并属性,React新版的所有东西都不会帮你合并,它认为这是你自己要做的事。
2.地址要变
setState(obj) ,如果obj地址不变,那么 React 就认为数据没有变化。
useState 可接受函数
当初始值比较复杂时,可采用。
const [state,setState] = useState(()=>{
return initialState
})
该函数返回初始 state ,且只执行一次。
setState 接受函数
点击button后你会发现n=1而不是2,因为当你setN(n+1)时,n不会变。 不管你做多少次计算,只有最后一次有用。
解决方法: 改成函数
function App() {
const [n, setN] = useState(0)
const onClick = ()=>{
//setN(n+1) 第1次计算
//setN(n+1) 第2次计算,也是最后1次计算
setN(n => n + 1) //形式化的操作
setN(n => n + 1)// 你会发现 n 不能加 2
// setN(i=>i+1)
// setN(i=>i+1)
}
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>+2</button>
</div>
);
}
JS语法有问题:对象必须加()。(JS的bug)
总结:对state进行多次操作时,优先使用函数。
useReducer
useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法
useReducer4步走:
1.创建初始值initicalState
const initical = { n:0 }
2.创建所有操作reducer(state,action)
reducer接受2个参数:旧的状态state和操作的类型action(一般是类型),最后返回新的state。
怎么得到新的state?
看下动作的的类型是什么
规则和useState一样,必须返回新的对象。 (不能直接操作n)
const reducer=(state,action)=>{
if(action.type==='add'){
return { n:state.n+1 } //return新对象
}else if(action.type==='mult'){
return { n:state.n*2 }
}else{
console.log("unknown type")
}
}
3.传给useReducer,得到读和写API
(1)需要导入useReducer或者直接使用全称React.useReducer
(2)useReducer接收2个参数:所有操作reducer和初始状态initical
(3)你将得到读API、写API写API一般叫dispatch,因为你必须通过reducer才能setState,所以叫dispatch。
import React,{useReducer} from "react"
function App(){
const [state,dispatch]=useReducer(reducer,initical)
}
拿出属性n的2种方法:
1' {state.n} 2'const {n}=state然后{n}
4.调用 写({type:'操作类型'})
const onClick=()=>{
dispatch({
type:'add' //调用reducer的add操作
})
}
相当于useState,只不过把所有操作聚拢在一个函数里,这样的好处是:调用的代码简短了。
调用传参: +2时传了参数number:2,那么reducer里的1就可以变成一个参数。因为dispatch()里传的对象就是action。
if (action.type === "add") {
//return { n: state.n + 1 };
return { n: state.n + action.number };
}
...
const onClick2 = () => {
//dispatch({type:'add'})
dispatch({type:'add',number:2}) //里面的对象就是action
}
这就是useReducer对useState的升级操作,总的来说useReducer是useState的复杂版。好处是用来践行React社区一直推崇的flux/Redux思想。随着hooks的流行这个思想会退化。 完整代码
import React, { useState, useReducer } from "react";
import ReactDOM from "react-dom";
const initial = { n: 0};
const reducer = (state, action) => {
if (action.type === "add") {
return { n: state.n + action.number };
} else if (action.type === "multi") {
return { n: state.n * 2 };
} else {
throw new Error("unknown type");
}
};
function App() {
const [state, dispatch] = useReducer(reducer, initial);
const { n } = state;
const onClick = () => {
dispatch({ type: "add", number: 1 });
};
const onClick2 = () => {
dispatch({ type: "add", number: 2 });
};
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>+1</button>
<button onClick={onClick2}>+2</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
如何选择 使用useReducer还是useState?
事不过三原则
如果你发现有几个变量应该放一起(对象里)这时候就用useReducer对对象进行整体的操作。 useReducer的常用例子
const initFormData = {
name: "",
age: 18,
nationality: "汉族"
};
function reducer(state, action) {
switch (action.type) {
case "patch": //更新
//把第1个对象的所有属性和第2个对象的所有属性全部放到第3个空对象里,这就是更新
return { ...state, ...action.formData };
case "reset": //重置,返回最开始的对象
return initFormData;
default:
throw new Error("你传的啥 type 呀");
}
}
function App() {
const [formData, dispatch] = useReducer(reducer, initFormData);
// const patch = (key, value)=>{
// dispatch({ type: "patch", formData: { [key]: value } })
// }
const onSubmit = () => {};
const onReset = () => {
dispatch({ type: "reset" });
};
return (
<form onSubmit={onSubmit} onReset={onReset}>
<div>
<label>
姓名
<input value={formData.name} onChange={e => dispatch(
{type:"patch", formData:{ name: e.target.value }})
}
/>
</label>
</div>
<div>
<label>
年龄
<input value={formData.age} onChange={e =>dispatch(
{type:"patch",formData: { age: e.target.value }})
}
/>
</label>
</div>
<div>
<label>
民族
<input value={formData.nationality}
onChange={e => dispatch({type:"patch",
formData:{nationality: e.target.value}})
}
/>
</label>
</div>
<div>
<button type="submit">提交</button>
<button type="reset">重置</button>
</div>
<hr />
{JSON.stringify(formData)}
</form>
);
}
用户一旦输入就会触发onChange事件。用户输入即更新,因为内容不一样了嘛。 每次更新,App都会render遍。
如何用useReducer代替Redux ?
前提:你得知道Redux是什么 用React的
reducer+context即可代替Redux。
useContext(常用)
概念
上下文就是你运行一个程序所需要知道的所有其它变量(全局变量)。
全局变量是全局的上下文,所有变量都可以访问它。
上下文是局部的全局变量,context只在<C.Provider>内有用,出了这个范围的组件是用不到这个contextde。
使用方法:
一.使用C = createContext(initical)创建上下文
二.使用<C.provider value={}>初始化并圈定作用域
三.在作用域内的组件里使用useContext(C)来获取上下文
import React, { createContext } from "react";
const C = createContext(null)
<C.Provider value={}>
...
</C.Provider>
value的初始值可以是任何值,一般我们会给一个读写接口.
<C.Provider>内的所有组件都可以用上下文C
例子
+1操作的不是本身的state,而是从App那里得到的读、写接口。
App也可以不用state,用reducer: const [n, setN] = useState(0);,context不管你用啥,它只是告诉你n、setN可以共享给你的子代的任何组件的,范围就是由<C.Provider>圈定的。
useContext注意事项
不是响应式的
你在一个模块将C里面的值改变,另一个模块不会感知到这个变化。
更新的机制并不是响应式的,而是重新渲染的过程。
比如,当我们点击+1时:setN去通知useState,useState重新渲染App,发现n变了,于是问里面的组件<Baba />有没有用到n?没有,就继续问<Child />有没有用到n?用到了,这时候儿子就知道要刷新了,是一个从上而下逐级通知的过程,并不是响应式的过程。
Vue3是你改n时,它就知道n变了,于是它就找谁用到了n,它就把谁直接改变了。它不会从上而下整体过一遍,没有这么复杂,因为它是一个响应式的过程。
总结: useContext的更新机制式是自顶向下,逐级更新数据。 而不是监听这个数据变化,直接通知对应的组件。
useEffect & useLayoutEffect
useEffect
副作用
- 对环境的改变即为副作用,如修改 document.title
- 不一定非要把副作用放在 useEffect 里
- 实际上叫做 afterRender 可能更好,每次 render 后执行
用途
- 作为componentDidMount 使用,[]作第二个参数
- 作为 componentDidUpdate 使用, 可指定依赖
- 作为componentWillUnmount 使用,通过 return
- 以上三种用途可以同时存在
特点
如果同时存在多个 useEffect ,会按照出现次序执行。
useLayoutEffect
useEffect 在浏览器渲染完成后执行
useLayoutEffect 在浏览器渲染前执行
特点
- useLayoutEffect 总是比 useEffect 先执行
- useLayoutEff 里的任务最好影响了 Layout
为了用户体验,优先使用 useEffect
useEffect和useLayoutEffect的本质区别:
useEffect在浏览器渲染完成后执行,useLayoutEffect在浏览器渲染完成前执行。
特点
1.useLayoutEffect总是比useEffect先执行。
下面的代码打印2和3,再打印1。
useEffect(()=>{
if(time.current){ console.log("1") },[])
}
useLayoutEffect(()=>{
if(time.current){ console.log("2") },[])
}
useLayoutEffect(()=>{
if(time.current){ console.log("3") },[])
}
2.useLayoutEffect里的任务最好影响了Layout
如果没有改变屏幕外观Layout,就没必要放浏览器渲染前,占时间。
经验: 为了用户体验,优先使用useEffect(优先渲染)
useMemo
要理解
React.useMemo需要先了解React.memo。
useCallback是useMemo的语法糖 React.memo
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () => {
setN(n + 1);
};
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>
<Child data={m}/>
{/* <Child2 data={m}/> */}
</div>
);
}
function Child(props) {
console.log("child 执行了");
console.log('假设这里有大量代码')
return <div>child: {props.data}</div>;
}
const Child2 = React.memo(Child);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
React默认有多余的render,点击按钮,Child() 执行了,但Child 依赖数据并没有改变,此时,可使用React.memo(Child)代替 Child。
如果props不变,就没必要再执行一个函数组件。
但是,React.memo有个 bug
const onClickChild = ()=>{}
把一个监听函数传给这个组件时,即使监听函数什么也不做,每次当外部组件数据改变重新渲染时,这个组件也会执行。
这是因为每次重新执行 App() ,都会生成一个新的监听函数,和之前的监听函数地址不同,所以会导致这个组件也执行。
使用useMemo可以解决这个问题
const onClickChild = useMemo(()=>{ return console.log(m) },[m])
useMemo特点
- 第一个参数是 ()=> value
- 第二个参数是依赖[m,n]
- 只有当依赖变化时,才会计算出新的 value,如果依赖不变,那么就重用之前的value
注意
如果你的 value 是个函数,那么就要写成 useMemo(()=> ()=> console.log(x))
这是一个返回函数的函数,很难用,于是有了 useCallback
useCallback
用法
useCallback(x=>log(x),[m]) 等价于 useMemo(()=>x=>log(x),[m])
useRef
目的
- 如果需要一个值,在组件不断 render 时保持不变
- 初始化: const count=useRef(0)
- 读取:count.current
- 为什么需要current ,为了保证两次useRef是同一个值(只有引用能做到)
forwardRef
props无法传递ref属性。
import React, { useRef } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const buttonRef = useRef(null);
return (
<div className="App">
<Button3 ref={buttonRef}>按钮</Button3>
</div>
);
}
const Button3 = React.forwardRef((props, ref) => {
return <button className="red" ref={ref} {...props} />;
});
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
useImperativeHandle
用于自定义 ref 的属性
const Button2 = React.forwardRef((props, ref) => {
const realButton = createRef(null);
const setRef = useImperativeHandle;
setRef(ref, () => {
return {
x: () => {
realButton.current.remove();
},
realButton: realButton
};
});
return <button ref={realButton} {...props} />;
});
自定义 Hook
封装数据操作 新建目录hooks,新建文件useList.js
import { useState, useEffect } from "react";
const useList = () => {
const [list, setList] = useState(null);
useEffect(() => {
ajax("/list").then(list => {
setList(list);
});
}, []); // [] 确保只在第一次运行
return {
list: list,
setList: setList
};
};
export default useList;
function ajax() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{ id: 1, name: "Frank" },
{ id: 2, name: "Jack" },
{ id: 3, name: "Alice" },
{ id: 4, name: "Bob" }
]);
}, 2000);
});
}
index.js
import React, { useRef, useState, useEffect } from "react";
import ReactDOM from "react-dom";
import useList from "./hooks/useList";
function App() {
const { list, setList } = useList();
return (
<div className="App">
<h1>List</h1>
{list ? (
<ol>
{list.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ol>
) : (
"加载中..."
)}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
总结
1.useState状态
2.useEffect(副作用)就是afterRender
3.useLayoutEffect就是比useEffect提前一点点。
但是很少用,因为会影响渲染的效率,除非特殊情况才会用。
4.useContext上下文,用来把一个读、写接口给整个页面用。
5.useReducer专门给Redux的用户设计的(能代替Redux的使用),我们甚至可以不用useReducer。
6.useMemo(记忆)需要与React.Memo配合使用,useMemo不好用我们可以升级为更好用的useCallback(回调)
7.useRef(引用)就是保持一个量不变,关于引用还有个forwardRef,forwardRef并不是一个Hook,还有个useImperativeHandle就是setRef。
就是我支持ref时,可以自定义ref长什么样子,那就使用useImperativeHandle。
8.自定义Hook
示例中的useList就是自定义Hook,非常好用。
有个默认的自定义HookuseDebugValue就是你在debugger时,可以给你的组件加上名字,很少用。