React Hooks
React Hooks 要解决的问题是状态共享,或者称为状态逻辑复用更为恰当,因为只共享数据处理漏计,不会共享数据本身。
React Hooks API
- useState: setState
- useReducer: setState
- useRef: ref
- useContext: context
- useCallback
- useMemo
- useEffect
- hooksDemo:codesandbox.io/s/shy-hooks…
参考链接
- 官网
- 中文网
- hooks的正确用法
- 一文看懂 react hooks
- React Hooks的几个问题
- React Hooks 你真的用对了吗?
- 精读《React Hooks》可以更好的理解官网中案例
- 精读《Function Component 入门》
2019.11.12 最近用到hooks,只是将class组件变成hooks写法,只是换了一种写法,没有实质性的优化。 所以leader提出了这个问题,并且举证,git到了hooks的新用法。
hooks 的动机(添加 Hook 的具体原因):
- Hook 使你在无需修改组件结构的情况下复用状态逻辑。
- Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。
- Hook 使你在非 class 的情况下可以使用更多的 React 特性。
2019.10.15
精读Hooks
随着React 16.8的发布,React Hooks已经发布!
Hooks增加了无需编写JavaScript类即可访问状态等功能的功能。Hooks承诺将大大简化React组件所需的代码,并且当前在React alpha版本中可用。
React Hook让无狀态组件拥有了许多只有有狀态组件的能力,如自更新能力(setState,使用useState),访问ref(使用useRef或useImperativeMethods)
访问context(使用useContext),使用更高级的setState设置(useReducer),及进行类似生命周期的阶段性方法(useEffect或useLayoutEffect)。
useState: setState
useReducer: setState
useRef: ref
useImperativeMethods: ref
useContext: context
useCallback: 可以对setState的优化
useMemo: useCallback的变形
useLayoutEffect: 类似componentDidMount/Update, componentWillUnmount
useEffect: 类似于setState(state, cb)中的cb,总是在整个更新周期的最后才执行
useState
useState是react自带的一个hook函数,它的作用是用来声明状态变量。
useState这个函数接收的参数是状态的初始值(Initialstate),它返回一个数组,这个数组的第0位是当前的状态值,第1位是可以改变状态值的方法函数。
useState是react自带的一个hook函数,它的作用是用来声明状态变量。
useState这个函数接收的参数是状态的初始值(Initialstate),它返回一个数组,这个数组的第0位是当前的状态值,第1位是可以改变状态值的方法函数。
多状态声明的注意事项:
React是根据useState出现的顺序来确定的
useState不能在if...else...这样的条件语句中进行调用,必须要按照相同的顺序进行渲染。如果你还是不理解,你可以记住这样一句话就可以了:
就是React Hooks不能出现在条件判断语句中,因为它必须有完全一样的渲染顺序。
function Table(props) {
// ✅ createRows() is only called once
const [rows, setRows] = useState(() => createRows(props.count));
// ...
}
function createRows(props) {
return props
}
useEffect
利用useEffect函数代替一些生命周期;
useEffect类似于setState(state,cb)中的cb,总是在整个更新周期的最后才执行;
useEffect需要定义第二个参数
dependences,dependences 这个参数定义了 useEffect的依赖;
useEffect注意点
- React首次渲染和之后的每次渲染都会调用一遍useEffect函数。
- 而之前我们要用两个生命周期函数分别表示首次渲染(componentDidMount)和更新导致的重新渲染(componentDidUpdate)。
- useEffect中定义的函数的执行不会阻碍浏览器更新视图,也就是说这些函数是异步执行的。
- 而componentDidMount和componentDidUpdate中的代码都是同步执行的
- useEffect的第二个参数:dependences
- dependences这个参数定义了useEffect的依赖,在新的渲染中,只要所有依赖项的引用都不发生变化,useEffect就不会被执行,且当依赖项为[]时,useEffect仅在初始化执行一次,后续的Rerender永远也不会被执行。
- useEffect 实现 componentWillUnmount生命周期函数
- 当第二个参数传空数组[]时,就是当组件将被销毁时才进行解绑,这也就实现了componentWillUnmount的生命周期函数
- 开发者通过 useEffect 的第二个参数告诉 React 用到了哪些外部变量。
/**
useEffect可以看做是componentDidMount,componentDidUpdate,componentWillUnmount 这三个生命周期函数的组合。
*/
useEffect(()=>{
console.log('start useEffect')
document.addEventListener('click', props.closeRobotList)
return()=>{
console.log('end useEffect')
document.removeEventListener('click', props.closeRobotList)
}
},[])
function onShow(state) {
console.log(state)
}
useLayoutEffect(()=>{
console.log(prevCountRef,'useLayoutEffect')
document.title = `You clicked ${state.count} times`;
return()=>{
console.log(prevCountRef,'end useLayoutEffect')
document.title += `!!!`;
}
},[state.count])
console.log('更新Example',state.count)
//更新Example 2
// ReducerCount.js:37 {current: input} "end useEffect"
// ReducerCount.js:34 {current: input} "useEffect"
20191113
使用 Effect Hook
-
利用 useEffect 代替一些生命周期。
-
无需清除的effect
-
useEffect会在每次渲染后都执行,在第一次渲染之后和每次更新之后都会执行
-
需要清除的effect
(1)为什么要在 effect 中返回一个函数?这是effect可选的清除机制。每个 effect 都可以返回一个清除函数。 (2)React 何时清除 effect?在组件卸载的时候执行清除操作。因为effect在每次渲染的时候都会执行,所以React会在执行当前effect之前对上一个effect进行清除。
-
useEffect可以在组件渲染后实现各种不同的副作用,有些副作用需要清除,所以要返回一个函数;有的副作用不必清除,所以不需要返回。
-
effect可以像使用多个state 一样,使用多个effect
createContext和useContext 让组件之间传值更简单
详见/components/TestDemo/HooksTodo
useContext,它可以帮助我们跨越组件层级直接传递变量,实现共享。
需要注意的是useContext和redux的作用是不同的,一个解决的是组件之间值传递的问题,一个是应用中统一管理状态的问题,但通过和useReducer的配合使用,可以实现类似Redux的作用。
可以直接在js中创建,也可以在公共js中创建
//======3======
const AgeContext = createContext()
<AgeContext.Provider value={age} >
<ChildAge/>
</AgeContext.Provider>
function ChildAge() {
const age = useContext(AgeContext)
return(<h3>通过createContext和useContext实现父子组件的传递:{age}</h3>)
}
{color: "blue", dispatch:f} // "useContext(ColorContext)"
useReducer
reducer其实就是一个函数,这个函数接收两个参数,一个是状态,一个用来控制业务逻辑的判断参数。
useReducer 返回的结构与useState很像,只是数组第二项是dispatch,而接收的参数也有两个,初始值放在第二位,第一位就是reducer。
const [state, dispatch] = useReducer(reducer, initialArg, init);
第一个参数:reducer纯函数 第二个参数:state的默认值
const initialState = {count: 0};
第三个参数:state的重置
function init(initialCount) {
return {count: initialCount};
}
可以写在一个js中,在reducer中做数据处理
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<div>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<div/>
);
}
//======4:useReducer========
const [count,setCount] = useReducer((state,action)=>{
switch (action.type) {
case 'add':
return state + 1;
case 'sub':
return state - 1;
default:
break;
}
},0)
<div>
<Button onClick={()=>setCount({type:'add'})}>+</Button>
<span style={{margin:'0 20px'}}> 数字:{count}</span>
<Button onClick={()=>setCount({type: 'sub'})}>-</Button>
</div>
2019.11.29
useEffect中同时使用两个以上变量时?
如果同时需要对 count 与 step 两个变量做累加,那 useEffect 的依赖必然要写上一种某一个值,频繁实例化的问题就又出现了:
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
return <h1>{count}</h1>;
}
用useReducer解决此类问题
在useEffect的函数中,绕过了count、step这两个变量。所以useReducer 也被称为解决此类问题的 “黑魔法”。
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: "tick" });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
return <h1>{count}</h1>;
}
function reducer(state, action) {
switch (action.type) {
case "tick":
return {
...state,
count: state.count + state.step
};
}
}
2019.11.01
useMemo
useMemo 会「记住」一些值,同时在后续 render 时,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回「记住」的值。
应该使用 useMemo 的场景
- 保持引用相等
- 对于组件内部用到的 object、array、函数等,如果用在了其他 Hook 的依赖数组中,或者作为 props 传递给了下游组件,应该使用 useMemo。
- 自定义 Hook 中暴露出来的object、array、函数等,都应该使用useMemo。以确保当值相同时,引用不发生变化。
- 使用 Context 时,如果Provider的value中定义的值(第一层)发生了变化,即便用了Pure Component或者React.memo,仍然会导致子组件re-render。这种情况下,仍然建议使用 useMemo 保持引用的一致性。
- 成本很高的计算
- 比如 cloneDeep 一个很大并且层级很深的数据
无需使用 useMemo 的场景
- 如果返回的值是原始值: string, boolean, null, undefined, number, symbol(不包括动态声明的 Symbol),一般不需要使用 useMemo。
- 仅在组件内部用到的 object、array、函数等(没有作为 props 传递给子组件),且没有用到其他 Hook 的依赖数组中,一般不需要使用 useMemo。
2019.11.29
useMemo好处:更细粒度的优化渲染。
所谓更细粒度的优化渲染,是指函数 Child 整体可能用到了 A、B 两个props,而渲染仅用到了B,那么使用memo方案时,A的变化会导致重渲染,而使用 useMemo 的方案则不会。
useMemo 配合 useContext
const Count = () => {
const { state, dispatch } = useContext(Store);
return useMemo(
() => (
<button onClick={() => dispatch("incCount")}>
incCount {state.count}
</button>
),
[state.count, dispatch]
);
};
const Step = () => {
const { state, dispatch } = useContext(Store);
return useMemo(
() => (
<button onClick={() => dispatch("incStep")}>incStep {state.step}</button>
),
[state.step, dispatch]
);
};
2019.10.16
useReducer代替Redux
- useContext:可访问全局状态,避免一层层的传递状态。这符合Redux其中的一项规则,就是状态全局化,并能统一管理。
- useReducer:通过action的传递,更新复杂逻辑的状态,主要是可以实现类似Redux中的Reducer部分,实现业务逻辑的可行性。
useMemo优化React Hooks程序性能
- useMemo主要用来解决使用React hooks产生的无用渲染的性能问题。useMemo返回缓存的变量。
- useMemo 优化性能:
- 只要使用useMemo,然后给她传递第二个参数,参数匹配成功,才会执行。
- 使用function的形式来声明组件,失去了shouldCompnentUpdate(在组件更新之前)这个生命周期,也就是说我们没有办法通过组件更新前条件来决定组件是否更新。
- 而且在函数组件中,也不再区分mount和update两个状态,这意味着函数组件的每一次调用都会执行内部的所有逻辑,就带来了非常大的性能损耗。
useMemo和useCallback都是解决上述性能问题的
另一种说法:
- 基于class的形式创建的组件,性能优化会通过在shouldComponentUpdate中判断前后的props和state,如果没有变化,则返回false来阻止更新。
- 基于hooks创建的函数组件中,react不在区分mount和update两个状态,这意味着函数组件的每一次调用都会执行其内部的所有逻辑, 那么会带来较大的性能损耗。因此useMemo和useCallback就是解决性能问题的杀手锏。
- useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数。
const funcConst = useMemo(()=> handleFunc(name),[name])
useEffect(()=>{
// MumSort()
return()=>{
}
},[])
const [values,setValues] = useState([0,3,2,7,4,8,1])
const MumSort = ()=>{
values.sort((a,b)=>{
return a - b
})
// setValues(values)
}
console.log(values,'old values') //[0,3,2,7,4,8,1]
useMemo(()=>MumSort(),[values]) //更新render,重新渲染
console.log(values,'new values')//[0, 1, 2, 3, 4, 7, 8]
//打印values的
//[0, 3, 2, 7, 4, 8, 1] "old values"
//[0, 1, 2, 3, 4, 7, 8] "new values"
//[0, 1, 2, 3, 4, 7, 8] "===render"
//应用useMemo之后重新渲染,不需要写在useEffect()中
//[0, 1, 2, 3, 4, 7, 8] "old values"
//[0, 1, 2, 3, 4, 7, 8] "new values"
// [0, 1, 2, 3, 4, 7, 8] "===render"
return (
<div>
{
console.log(values,'===render')
}
{values.map(v=><span key={v}>{v}</span>)}
</div>
)
useCallback
useCallback返回缓存的函数
用法:
const fnA = useCallback(fnB, [a])
应用场景:
所有依赖本地状态或props来创建函数,需要使用到缓存函数的地方,都是useCallback的应用场景。
- 由于函数也具有 Capture Value 特性,经过 useCallback 包装过的函数可以当作普通变量作为 useEffect 的依赖。
- useCallback 做的事情,就是在其依赖变化时,返回一个新的函数引用,触发 useEffect 的依赖变化,并激活其重新执行。 Function Component 中利用 useCallback 封装的取数函数,可以直接作为依赖传入 useEffect,useEffect 只要关心取数函数是否变化,
- 而取数参数的变化在 useCallback 时关心,再配合 eslint 插件的扫描,能做到 依赖不丢、逻辑内聚,从而容易维护。
useCallback 也有第二个参数 - 依赖项.
如何将函数抽到 useEffect 外部?
function Counter() {
const [count, setCount] = useState(0);
const getFetchUrl = useCallback(() => {
return "https://v?query=" + count;
}, [count]);
useEffect(() => {
getFetchUrl();
}, [getFetchUrl]);
return <h1>{count}</h1>;
}
useEffect 只需要依赖 getFetchUrl 这个函数,就实现了对 count 的间接依赖。
为什么 useCallback 比 componentDidUpdate 更好用
Class Component 的模式:
class Parent extends Component {
state = {
count: 0,
step: 0
};
fetchData = () => {
const url =
"https://v?query=" + this.state.count + "&step=" + this.state.step;
};
render() {
return <Child fetchData={this.fetchData} count={count} step={step} />;
}
}
class Child extends Component {
state = {
data: null
};
componentDidMount() {
this.props.fetchData();
}
componentDidUpdate(prevProps) {
if (
this.props.count !== prevProps.count &&
this.props.step !== prevProps.step // 别漏了!
) {
this.props.fetchData();
}
}
render() {
// ...
}
}
Function Component模式
function Parent() {
const [ count, setCount ] = useState(0);
const [ step, setStep ] = useState(0);
const fetchData = useCallback(() => {
const url = 'https://v/search?query=' + count + "&step=" + step;
}, [count, step])
return (
<Child fetchData={fetchData} />
)
}
function Child(props) {
useEffect(() => {
props.fetchData()
}, [props.fetchData])
return (
// ...
)
}
useRef
通过 useRef 创建的对象,其值只有一份,而且在所有 Rerender 之间共享。
- 用useRef获取React JSX中的DOM元素,获取后你就可以控制DOM的任何东西了。但是一般不建议这样来作,React界面的变化可以通过状态来控制。
- 用useRef来保存变量,这个在工作中也很少能用到,我们有了useContext这样的保存其实意义不大,但是这是学习,也要把这个特性讲一下。
避免重新创建useRef()初始值
function Image(props) {
const ref = useRef(null);
// ✅ IntersectionObserver is created lazily once
function getObserver() {
if (ref.current === null) {
ref.current = new IntersectionObserver(onIntersect);
}
return ref.current;
}
// When you need it, call getObserver()
// ...
}
2019.10.18
useImperativeHandle
不懂
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用。
useLayoutEffect
官方解释,这两个hook基本相同,调用时机不同,请全部使用useEffect,除非遇到bug或者不可解决的问题,再考虑使用useLayoutEffect。
还举了个例子,譬如你想测量DOM元素时候,使用useLayoutEffect。
个人感觉举例不恰当,测试DOM我也完全可以在useEffect中测量啊。说如果需要在paint前改变DOM,更合适。
我做过测试,譬如一个div尺寸是200 * 200,我想改成100 * 100,如果写在useEffect中,确实会造成页面抖动,写在useLayoutEffect中可以避免。
const a = useRef()
useEffect(()=>{
console.log(a,'useEffect')
document.title = `You clicked ${state.count} times`;
return()=>{
console.log(a,'end useEffect')
document.title = `remove`;
}
})
//=====8:useLayoutEffect====
useLayoutEffect(()=>{
console.log(a,'useLayoutEffect')
document.title = `You clicked ${state.count} times`;
return()=>{
console.log(a,'end useLayoutEffect')
document.title += `!!!`;
}
})
console.log('更新Example',state.count)
//====打印结果===
//更新Example 2
// ReducerCount.js:46 {current: input} "end useLayoutEffect"
// ReducerCount.js:43 {current: input} "useLayoutEffect"
// ReducerCount.js:37 {current: input} "end useEffect"
// ReducerCount.js:34 {current: input} "useEffect"
//点击+或者-或者向input输入内容,会发现每次都会先进行 useEffect与useLayout的清理函数,再执行他们的初始函数。
// 并且发现useEffect的函数会在最后才执行,它会晚于包含它的父函数。
useDebugValue
usePrevious
hooks请求处理
How to fetch data with React Hooks? 获取数据
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
// By moving this function inside the effect, we can clearly see the values it uses.
async function fetchProduct() {
const response = await fetch('http://myapi/product' + productId);
const json = await response.json();
setProduct(json);
}
fetchProduct();
}, [productId]); // ✅ Valid because our effect only uses productId
// ...
}
数据请求:请求结束之后才能继续下一次请求
useEffect(() => {
let ignore = false;
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
const json = await response.json();
if (!ignore) setProduct(json);
}
fetchProduct();
return () => { ignore = true };
}, [productId]);
也可以添加函数来实现以来关系,应用useCallback.这样可以确保它不会在每个渲染器上都改变,除非它自己的依赖性也改变了
function ProductPage({ productId }) {
// ✅ Wrap with useCallback to avoid change on every render
const fetchProduct = (() => {
// ... Does something with productId ...
}, [productId]); // ✅ All useCallback dependencies are specified
return <ProductDetails fetchProduct={fetchProduct} />;
}
function ProductDetails({ fetchProduct }) {
useEffect(() => {
fetchProduct();
}, [fetchProduct]); // ✅ All useEffect dependencies are specified
// ...
}
- 在 context 中传递 dispatch ,而不是在 props(属性) 中单独回调
2019/10/18
HooksTodoList
问题:添加的数据不能及时渲染
在添加todolist时,如下代码,添加的数据不能及时渲染
function addList(){
let item = {
id: Math.random().toString().slice(2),
value: inputValue
}
todoLists.push(item)
setTodo(newTodoList)
}
解决: 需要将todolist浅拷贝一下,代码如下
function addList(){
if(inputValue!==''){
const newTodoList = [...todoList]
let item = {
id: Math.random().toString().slice(2),
value: inputValue
}
newTodoList.push(item)
setTodo(newTodoList)
setIptValue('')
}
}