Hooks 逐一解析
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时,可以给你的组件加上名字,很少用。