useState
使用状态
const[n, setN] = React.useState(0) <====n是读接口,setN是写接口
const[user,setUser] = React.useState({name:'F'})
注意事项1:不可局部更新
如果state是一个对象,能否部分setState?
答案是不行,请看示例代码
https://codesandbox.io/s/jirengudemo-t16xl?file=/src/App.js:0-365
import React, { useState } from "react";
export default 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>
);
}
会出现像图中这种,只copy了name,而没有copy age,因为因为setState不会帮我们合并属性
如何解决?
setUser({
...user, <====...user就是把user所有的属性全部copy过来
name:'Jack'
})
那么useReducer会合并属性吗?妈的,也不会!
注意事项2:地址要变
setState(obj)如果obj地址不变,那么React就认为数据没有变化
const onClick = () => {
user.name = "Jack"
setUser(user);
};
//这段代码在鼠标点击按钮后不会起任何变化
useState续
useState接受函数
const [state, setState] = useState(()=>{
return initialState
})
该函数返回初始state,且只执行一次
setState接受函数
https://codesandbox.io/s/hopeful-dubinsky-m8gnt?file=/src/index.js:290-350
import React, { useState } from "react";
import ReactDOM from "react-dom";
function App() {
const [n, setN] = useState(0);
const onClick = () => {
//setN(n + 1);
//setN(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>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
什么时候用这种方式?
如果你能接受这种方式,应该优先使用这种形式
useReducer
用来践行Flux/Redux的思想
开代码,分4步走
创建所有操作reducer(state,action)
总的来说useReducer是useState的复杂版
示例代码
一个用useReducer的表单例子
用useReducer代替redux
步骤
用Context.Provider将Context提供给所有组件
例子:
如何模块化?其实很简单
useContext
上下文
全局变量是全局的上下文。
上下文是局部的全局变量。
使用方法
之前的例子已经介绍过了。
使用C = createContext(initial)创建上下文。
在作用域内使用useContext(C)来使用上下文。
import { createContext, useContext, useState } from "react";
import "./styles.css";
const C = createContext(null);
export default function App() {
const [n, setN] = useState(0);
return (
<C.Provider value={{ n, setN }}>
<div className="App">
<Parents />
</div>
</C.Provider>
);
}
function Parents() {
return (
<div>
我是爸爸
<Child />
</div>
);
}
function Child() {
const { n, setN } = useContext(C);
const onClick = () => {
setN((i) => i + 1);
};
return (
<div>
我是Child, 我得到的n : {n}
<button onClick={onClick}>+1</button>
</div>
);
}
注意事项
useContext
不是响应式的,你在一个模块将C里面的值改变,另一个模块,不会感知到这个变化。
useEffect
副作用(API名字叫得不好。)
useEffect就是在更新后执行某个操作
对环境的改变记为副作用,如修改document.title
但我们不一定非要把副作用放在useEffect里
实际上叫做afterRender更好,每次render后运行。
用途
作为componentDidMount使用,[]做第二个参数,表示只在第一次更新后执行,其他时候不执行。
作为componentDidUpdate使用,可指定依赖
import "./styles.css";
import { useEffect, useState } from "react";
export default function App() {
const [n, setN] = useState(0);
const onClick = () => {
setN((i) => i + 1);
};
const afterRender = useEffect;
//case 1
afterRender(() => {
console.log("第一次渲染之后执行这句话");
}, []); //只在第一次渲染之后执行,加个空数组,意思是[]里面的变量变化时执行=》不执行
//case 2
afterRender(() => {
console.log("第一二三次执行这句话");//任何一个state变化时都执行,这里没有加第二个参数默认为所有的状态
});
//case 3
afterRender(() =>{
//如果想做到第二次变化才执行
if(n!== 0)
console.log("n变化了")//在某个值变化的时候执行这句话,比如这里是当n变化的时候执行
},[n])
return (
<div className="App">
n:{n}
<button onClick={onClick}> +1</button>
</div>
);
}
作为componentWillUnmount使用,通过return
以上三种用途可同时存在。
componentwillunmount
import "./styles.css";
import { useEffect, useState } from "react";
export default function App() {
const [n, setN] = useState(0);
const onClick = () => {
setN((i) => i + 1);
};
useEffect(()=>{
const id = setInterval(()=>{
console.log('hi')
},1000)
return ()=>{
window.clearInterval(id)
}
})
return (
<div className="App">
n:{n}
<button onClick={onClick}> +1</button>
</div>
);
}
特点
如果同时存在多个useEffect,会按照出现次序执行。
useLayoutEffect
当中途需要改变dom的时候,(中途截胡),但是一般很少用到
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [value,setValue] = useState(0);
useEffect(()=>{
document.querySelector('#x').innerText = `value:1000`
},[value])
return (
<div id="x" onClick={()=>setValue(0)}>
value:{value}
</div>
);
}
import { useLayoutEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [value,setValue] = useState(0);
useLayoutEffect(()=>{
document.querySelector('#x').innerText = `value:1000`
},[value])
return (
<div id="x" onClick={()=>setValue(0)}>
value:{value}
</div>
);
}
布局副作用
useEffect在浏览器 渲染完成后执行。
例子
useLayoutEffect 在浏览器渲染前执行。
通过时间点来侧面证明(此处要画图说明)。
特点
useLayoutEffect总是比useEffect先执行
useLayoutEffect里的任务最好影响了Layout
经验
为了用户体验,优先使用useEffect(优先渲染)
useMemo
一句话总结:就是在相应组件的props变化的时候才执行,否则不执行。
要理解React.useMemo需要先讲React.memo,React默认有多余的render。
讲代码中的Child用React.memo(Child)代替。
如果props不变,就没有必要再次执行一个函数组件。
最终效果:代码。
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [n, setN] = useState(0);
const [m, setM] = useState(0);
const onClick = () => {
setN(n + 1);
};
const onClick2 = ()=>{
setM(m + 1)
}
return (
<div className="App">
<button onClick={onClick}> update n {n}</button>
<button onClick={onClick2}> update m {m}</button>
<Child2 data={m} />
</div>
);
}
function Child(props) {
console.log("child执行了");
console.log("假设这里有大量代码");
return <div>child:{props.data}</div>;
}
const Child2 = React.memo(Child);
memo可以改写成下面这样
const Child = memo((props) => {
console.log("child执行了");
console.log("假设这里有大量代码");
return <div>child:{props.data}</div>;
});
但是这玩意儿有一个bug。
添加了监听函数之后,1秒破功。
因为App 运行时会再次执行第12行,生成新的函数,新旧函数虽然功能一样,但是地址不一样,怎么办,用useMemo
import React, { useState,memo, useMemo } from "react";
import "./styles.css";
export default function App() {
const [n, setN] = useState(0);
const [m, setM] = useState(0);
const onClick = () => {
setN(n + 1);
};
const onClickChild = useMemo(()=>{
return ()=>{
console.log(m)
}
},[m])
const onClickChild = useCallback(()=>{
console.log(m);
},[m])
return (
<div className="App">
<button onClick={onClick}> update n {n}</button>
{/* <button onClick={onClick2}> update m {m}</button> */}
<Child2 data={m} onClick={onClickChild}/>
</div>
);
}
function Child(props) {
console.log("child执行了");
console.log("假设这里有大量代码");
return <div>child:{props.data}</div>;
}
const Child2 = React.memo(Child);
特点
第一个参数是()=>value,见定义
第二个参数是依赖[m,n]。
只有当依赖变化时,才会计算出value
如果依赖不变,那么就重用之前的value
这不就是vue2的computed吗
注意
如果你的value是个函数,那么你就要写成useMemo(()=>(x)=>console.log(x))。
这是一个返回函数的函数。
是不是很难用?于是就有了useCallback。
useCallback
useCallback是useMemo的语法糖
用法
useCallback(x => log(x), [m])等价于useMemo(()=>x =>log(x),[m])
useRef
目的
如果你需要一个值,在组件不断render时保持不变。
初始化: const count = useRef(0)
为了保证两次useRef是同一个值(只有引用能做到)
延伸
初始化: const count = ref(0)
不同点: 当count.value变化时,Vue3会自动render
useRef就是用来控制全局变量
useEffect是每次更新之后需要做的操作
下面这段代码用来记录一共更新了几次。
import { useEffect, useRef, useState } from "react";
import "./styles.css";
export default function App() {
const count = useRef(0);
const [n, setN] = useState(0);
const onClick = () => {
setN(n + 9);
};
useEffect(() => {
count.current += 1;
console.log(count.current);
});
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>
</div>
);
}
.current不会更新UI,如果需要render,要手动添加一个setN,调用这个setN(Math.random) 就可以更新UI。
useRef能做到变化时自动render吗?
不能,为什么不能,因为这不符合React的理念,React的理念是UI=f(data),你如果想要这个功能,完全可以自己加,监听ref,当ref.current变化时,调用setX即可。
不想自己加
那你就用Vue3吧,Vue3帮你加好了。
总结
useState/useReducer(useState的复杂版)n每次都变
useMemo/useCallback [m]的时候fn会变
useRef 永远不变
forwardRef
总结:forwardRef就是让你在函数组件上加了一个参数,然后就可以使用ref,class组件默认就可以用ref。
讲了useRef就不得不讲一下它了。
基本用法:让函数组件Button2支持ref
代码一:props无法传递ref属性
代码二:实现ref的传递
import { forwardRef ,useRef} from "react";
import "./styles.css";
export default function App() {
const buttonRef = useRef(null);
return (
<div className="App">
<Button3 ref={buttonRef}>按钮</Button3>
</div>
);
}
const Button2 = (props,ref) =>{
console.log(props);
console.log(ref)
return <button className="red" ref={ref} {...props} />
}
const Button3 = forwardRef(Button2)
useRef
可以用来引用DOM对象。
也可以用来引用普通对象。
forwardRef
由于props不包含ref,所以需要forwardRef。
为什么props不包含ref呢?因为大部分时候不需要。
useImperativeHandle
名字起得稀烂
应该叫做setRef
。
不用useImperativeHandle的代码
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
createRef
} from "react";
import "./styles.css";
export default function App() {
const buttonRef = useRef(null);
useEffect(() => {
console.log(buttonRef.current);
});
return (
<div className="App">
<Button2 ref={buttonRef}>按钮</Button2>
<button
className="close"
onClick={() => {
console.log(buttonRef);
buttonRef.current.x();
}}
>
{" "}
x
</button>
</div>
);
}
const Button2 = forwardRef((props,ref) =>{
const realButton = createRef(null);
useImperativeHandle(ref,() => ({
x:() => {
realButton.current.remove();
}
}))
})
用了useImperativeHandle的代码。
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
createRef
} from "react";
import "./styles.css";
export default function App() {
const buttonRef = useRef(null);
useEffect(() => {
console.log(buttonRef.current);
});
return (
<div className="App">
<Button2 ref={buttonRef}>按钮</Button2>
<button
className="close"
onClick={() => {
console.log(buttonRef);
buttonRef.current.x();
}}
>
{" "}
x
</button>
</div>
);
}
const Button2 = forwardRef((props, ref) => {
const realButton = createRef(null);
const setRef = useImperativeHandle;
setRef(ref, () => {
return {
x: () => {
realButton.current.remove();
}
};
});
return <button ref={realButton} {...props} />;
});
分析
用于自定义ref的属性。
自定义Hook
react里面最牛逼的功能。可以添加增删改查的功能,类似于把接口暴露给使用者,使用者只要知道怎么用即可,不用管内部怎么运作的。自定义Hooks的时候可以使用Context,reducer,这样都没有必要使用redux。方方建议在自己的项目中尽量使用自定义Hook。
封装数据操作
简单例子
贴心例子
分析
你还可以在自定义Hook里使用Context。
useState
只说了不能在if里,没说不能在函数里运行,只要这个函数在函数组件里运行即可。
stale Closure
总结
React Hooks
useEffect
副作用=======>useLayoutEffect
useLayoutEffect就是比useEffect提前一点点,很少用的原因是会影响渲染效果,特殊情况下会用
Redux
useReducer
useReducer是专门给Redux的用户设计的,可以不用redux
useMemo
记忆=======>useCallback
回调
如果useMemo不好用,可以使用useCallback
useRef
引用 ======>useImperativeHandle
useImperativeHandle
就是setRef,在支持ref的时候可以自定义ref
自定义Hook =====> useDebugValue
方方提到最常用的React Hooks
用得比较少的React Hooks
方方推荐的JS书籍: