React Hook的使用
前言
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。 在早期使用react的时候,会发现使用class创建的组件十分的臃肿,而且复用性差。在后面副作用和状态越来越多的时候会变得越来越难以改动。因此,学习hooks有利于我们构建更加精简且复用性更好的代码。
一、useState
1、基本使用方法
- 使用useState会返回两个东西,一个是你创建的状态state,一个是更新state的函数setstate。
- 下文中的[count, setCount]是数组解构,当然你也可以使用别的名字
setState中更新的方法有两种,一种是直接更新,不依赖于之前的状态;另一种是函数式更新,依赖于之前的状态。
import { useState } from "react";
const Test = () => {
const [count, setCount] = useState(0);
return (
<>
<div>{count}</div>
<!-- 方式一,直接更新 -->
<button
onClick={() => {
setCount(count + 1);
}}
>
+
</button>
<!-- 方法二,函数式更新 -->
<button
onClick={() => {
setCount((prevCount) => prevCount - 1);
}}
>
-
</button>
</>
);
};
export default Test;
2、有多个状态就用多个useState
- 多个state之间并不共享状态
import { useState } from "react";
const Test = () => {
const [count, setCount] = useState(0);
const [counter, setCounter] = useState(0);
return (
<>
<div>{count}</div>
<button onClick={() => {setCount(count + 1);}} >
+
</button>
<div>{counter}</div>
<button onClick={() => {setCounter(counter + 10);}} >
+
</button>
</>
);
};
export default Test;
3、当状态为引用类型时
要注意useState中的setstate并不会执行浅合并,而是直接替换
import { useState } from "react";
const Test = () => {
const [customer, setCustomer] = useState({ id: 0, name: "hello", age: 18 });
return (
<>
<div>
{customer.id} - {customer.name} - {customer.age}
</div>
<!-- 错误的修改方法,直接修改age -->
<button
onClick={() => {
setCustomer({ age: customer.age + 1 });
}}
>
error-change-age
</button>
<!-- 正确的修改方法,将之前的state展开,然后将需要修改的属性直接覆盖 -->
<button
onClick={() => {
setCustomer({ ...customer, age: customer.age + 1 });
}}
>
true-change-age
</button>
</>
);
};
export default Test;
4、惰性初始化state
initialState参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
二、useEffect
useEffect有种类似类组件中的componentDidMount,componentDidUpdate和 componentWillUnmount三者结合的作用。其主要作用是处理副作用函数,清除副作用函数和对某些状态进行监听。
1、基本使用方法
基本形式如下
- effect为副作用函数,一般填写挂载成功后以及依赖参数
input变动时执行的动作 - cleanup为返回函数,一般填写组件被卸载前执行的动作以及配套effect的清除函数
- input为依赖参数,可传入空数组
useEffect(() => {
effect
return () => {
cleanup
}
}, [input])
执行的顺序
- 挂载阶段,从上到下读取各个不同的useEffect,然后按先后顺序将effect推入一个队列中(先进先出)。在组件挂载完成后会执行队列中的effect,并获取返回函数,将返回函数推入另一个队中。
- 更新阶段,从上到下读取各个不同的useEffect,然后先后顺序将effect推入一个队列中(先进先出)。在组件完成更新后会先执行队列中的返回函数cleanup,然后再执行队列中的effect。执行时获取新的返回函数队列。
- 卸载阶段,只执行返回函数队列
现在我们来验证一下
import { useState, useEffect } from "react";
const App = () => {
const [show, setShow] = useState(true);
return (
<>
<!-- 负责控制Counter的卸载与挂载 -->
<button
onClick={() => {
setShow(!show);
}}
>
toggle
</button>
{show && <Counter />}
</>
);
};
const Counter = () => {
const [count, setCount] = useState(0);
// 挂载完成后和更新完成后才执行
useEffect(() => {
console.log("useEffect - 1 ");
return () => {
console.log("useEffect - 1 - 返回");
};
});
useEffect(() => {
console.log("useEffect - 2 ");
return () => {
console.log("useEffect - 2 - 返回");
};
});
useEffect(() => {
console.log("useEffect - 3 ");
return () => {
console.log("useEffect - 3 - 返回");
};
});
// 这句是挂载前执行
console.log("挂载");
return (
<>
<div>{count}</div>
<button
onClick={() => {
setCount(count + 1);
}}
>
+
</button>
</>
);
};
export default App;
关于依赖参数
- 组件更新时,副作用函数effect和返回函数cleanup在执行前会判断对应的useEffect中的依赖参数是否发生变化,没有的话就不执行。但是要注意无论有没有依赖参数,effect默认会在组件挂载完成后执行。
- 如果没有依赖参数,那么挂载完成后以及任意的更新后都会执行effect
- 如果依赖参数为空数组[],那么该effect只会执行一次
ok,我们知道了这些注意点,就能模拟类组件中的生命周期函数。
2、模拟类组件声明周期函数以及监听器
1)、componentDidMount
首先来明确一下,componentDidMount中,在组件挂载完成后执行,且只执行一次
useEffect(() => {
console.log("模仿componentDidMount");
}, []);
2)、componentWillUnmount
在组件即将卸载的时候执行
useEffect(() => {
return () => {
console.log("模仿componentWillUnmount");
};
}, []);
3)、componentDidUpdate
在这里我会使用一个hooks叫useRef,我们会用它来记录是否是挂载阶段,来让useEffect不要在挂载完成后就执行,只让函数在任意更新完成后执行。
const Counter = () => {
const [count, setCount] = useState(0);
const [numb, setNumb] = useState(10); // 添加了新的state
const fisrtRend = useRef(true); // 使用它来记录是否是挂载阶段
useEffect(() => {
if (fisrtRend.current) {
fisrtRend.current = false;
return;
}
console.log("模拟componentDidUpdate");
});
return (
<>
<div>count - {count}</div>
<button
onClick={() => {
setCount(count + 1);
}}
>
+
</button>
<div>numb - {numb}</div>
<button
onClick={() => {
setNumb(numb + 10);
}}
>
+
</button>
</>
);
};
4)、监听器
作用类似vue3的watchEffect吧,只监听特定的数据,在数据变动时做出一些操作
const Counter = () => {
const [count, setCount] = useState(0);
const fisrtRend = useRef(true);
useEffect(() => {
if (fisrtRend.current) {
fisrtRend.current = false;
return;
}
console.log("现在的count是" + count);
}, [count]);
return (
<>
<div>count - {count}</div>
<button
onClick={() => {
setCount(count + 1);
}}
>
+
</button>
</>
);
};
3、小总结
- 可以把
useEffect Hook看做componentDidMount,componentDidUpdate和componentWillUnmount这三个函数的组合 - 与 componentDidMount 或 componentDidUpdate 不同,使用
useEffect调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。
三、useRef
- 在类组件的时候,我们使用ref都是使用
React.createRef来创建。 - 但是,在hooks中,我们通过
useRef来创建,利用它来关联实例或者记录组件更新前的一些状态。 - useRef 返回一个可变的 ref 对象,其
.current属性被初始化为传入的参数(initialValue)。返回的ref 对象在组件的整个生命周期内保持不变。 - 当 ref 对象内容发生变化时,useRef 并不会通知你。变更
.current属性不会引发组件重新渲染。
1、获取dom节点
你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以
<div ref={myRef} />形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。
import { useState, useRef } from "react";
const Test = () => {
const [count, setcount] = useState(0);
const refEl = useRef();
return (
<>
<p ref={refEl}>{count}</p>
<button
onClick={() => {
console.log(refEl.current);
}}
>
获取p标签
</button>
<br />
<button
onClick={() => {
setcount(count + 1);
}}
>
+
</button>
</>
);
};
export default Test;
2、记录更新前的一些状态
为什么可以做到? 因为它创建的是一个普通 Javascript 对象,而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。
验证一下
import { useState, useEffect, useRef } from "react";
const Test = () => {
const [count, setcount] = useState(0);
const prevCount = useRef(count);
// 即使count改变了,但是prevCount.current不会变
useEffect(() => {
console.log(`count: ${count}, prevCount: ${prevCount.current}`);
}, [count]);
return (
<>
<p>{count}</p>
<br />
<button
onClick={() => {
setcount(count + 1);
}}
>
+
</button>
</>
);
};
export default Test;
唯一可以改变的方法,就是手动去修改prevCount.current的值
useEffect(() => {
console.log(`count: ${count}, prevCount: ${prevCount.current}`);
// 手动修改, 每次变更之后将prevCount.current的值修改为现在count的值
prevCount.current = count;
}, [count]);
四、useMemo
- useMemo有点类似vue中的计算属性,根据某个依赖参数来返回某些属性,只在依赖参数改变时才重新计算memoized值
- 如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值
- 传入 useMemo 的函数会在渲染期间执行,请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴
1、为什么要使用
我们知道父组件的更新,会导致子组件的更新。在类组件的时候,我们有PureComponent和shouldComponentUpdate这两种手段可以进行优化,但是在函数组件中,我们无法使用这两个api。取而代之的,是useMemo。
没有使用useMemo
import { useState } from "react";
const Test = () => {
const [count, setcount] = useState([0]);
return (
<>
<button
onClick={() => {
setcount([...count, count.length]);
}}
>
add
</button>
{count.map((item) => (
<Child data={item} key={item} />
))}
</>
);
};
const Child = ({ data }) => {
function updater() {
console.log(`${data} - 渲染`);
return "Child" + data;
}
return (
<>
<div>{updater()}</div>
</>
);
};
export default Test;
2、如何使用
api与useEffect完全相同,但是注意不要和useEffect搞混
import { useState, useMemo } from "react";
const Test = () => {
const [count, setcount] = useState([0]);
return (
<>
<button
onClick={() => {
setcount([...count, count.length]);
}}
>
add
</button>
{count.map((item) => (
<Child data={item} key={item} />
))}
</>
);
};
const Child = ({ data }) => {
const updater = useMemo(() => {
console.log(`${data} - 渲染`);
return "Child" + data;
}, [data]);
return (
<>
<!-- 注意到这里并不是updater(), 因为它会自动执行 -->
<div>{updater}</div>
</>
);
};
export default Test;
3、最终真的实现了PureComponent的效果了吗?
我们在挂载的时候进行打印来看看子组件究竟重新渲染了没
import { useState, useEffect, useMemo } from "react";
const Test = () => {
const [count, setcount] = useState([0]);
return (
<>
<button
onClick={() => {
setcount([...count, count.length]);
}}
>
add
</button>
{count.map((item) => (
<Child data={item} key={item} />
))}
</>
);
};
const Child = ({ data }) => {
const updater = useMemo(() => {
console.log(`${data} ------ useMemo `);
return "Child" + data;
}, [data]);
console.log(` ---- ${data} - 挂载 `);
return (
<>
<div>{updater}</div>
</>
);
};
export default Test;
4、结论
可以看到,子组件还是更新了。那么在这里useMemo究竟有什么用?答案是虽然子组件还是重新加载了,但是会产生新的虚拟dom,新旧虚拟dom之间会进行对比,发现两者并没有变化,所以就不更新dom了。
五、Memo
- React.memo 为高阶组件
- 如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果
- 默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现
- 比较函数的参数为(prevProps, nextProps)
1、对上面的例子使用memo进行优化
import { useState, useMemo, memo } from "react";
const Test = () => {
const [count, setcount] = useState([0]);
return (
<>
<button
onClick={() => {
setcount([...count, count.length]);
}}
>
add
</button>
{count.map((item) => (
<MemoChild data={item} key={item} />
))}
</>
);
};
const Child = ({ data }) => {
const updater = useMemo(() => {
console.log(`${data} ------ useMemo `);
return "Child" + data;
}, [data]);
console.log(` ---- ${data} - 挂载 `);
return (
<>
<div>{updater}</div>
</>
);
};
// 注意高阶组件的命名要遵守pascal命名
const MemoChild = memo(Child);
现在能看到使用memo优化后符合了我们的预期。
2、关于第二参数
这里和shouldComponentUpdate有点不同,如果返回true则不执行更新,如果返回false则执行更新
import { useState, memo } from "react";
const Test = () => {
const [count, setcount] = useState(0);
return (
<>
<button
onClick={() => {
setcount(count + 1);
}}
>
add
</button>
<MemoChild1 data={count} />
<MemoChild2 data={count} />
</>
);
};
const Child = ({ data }) => {
return (
<>
<div>{data}</div>
</>
);
};
const MemoChild1 = memo(Child, (prevProps, props) => {
// 不执行更新
console.log("memo1", prevProps, props);
return true;
});
const MemoChild2 = memo(Child, (prevProps, props) => {
// 执行更新
console.log("memo2", prevProps, props);
return false;
});
export default Test;
六、useContext
- 接收一个
context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value 决定 - 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用
React.memo或shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染
使用useContext实现counter
import { useState, createContext, useContext } from "react";
const MyCustomContext = createContext();
const Test = () => {
const [count, setCount] = useState(0);
return (
<>
<MyCustomContext.Provider value={{ count }}>
<Child />
</MyCustomContext.Provider>
<button
onClick={() => {
setCount(count + 1);
}}
>
+
</button>
</>
);
};
const Child = () => {
return (
<>
<GrandChild />
</>
);
};
const GrandChild = () => {
// 不需要使用consumer就可以直接拿到{count: 0}
const { count } = useContext(MyCustomContext);
return (
<>
<p>GrandChild - {count}</p>
</>
);
};
export default Test;
七、useReducer
- useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法
- 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。
- 并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数
1、基本使用
import { useReducer} from "react";
function reducer(state, action) {
switch(action.type) {
case "INCREMENT":
return {count: state.count + 1}
case "DECREMENT": {
return {count: state.count - 1}
}
default :{
return state
}
}
}
const initialState = {count: 0};
const Test = () => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<>
<div>Count: {state.count}</div>
<button onClick={()=>{dispatch({type: "INCREMENT"})}}>+</button>
<button onClick={()=>{dispatch({type: "DECREMENT"})}}>-</button>
</>
);
};
export default Test;
2、惰性初始化
- 你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)
- 这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利
import { useReducer } from "react";
// initialState
const count = 0;
// 第三参数 init
function init(initialState) {
return { count: initialState };
}
function reducer(state, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT": {
return { count: state.count - 1 };
}
case "RESET": {
return { count };
}
default: {
return state;
}
}
}
const Test = () => {
const [state, dispatch] = useReducer(reducer, count, init);
return (
<>
<div>Count: {state.count}</div>
<button
onClick={() => {
dispatch({ type: "INCREMENT" });
}}
>
+
</button>
<button
onClick={() => {
dispatch({ type: "DECREMENT" });
}}
>
-
</button>
<button
onClick={() => {
dispatch({ type: "RESET" });
}}
>
RESET
</button>
</>
);
};
export default Test;
八、自定义hooks
- 自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook
- 与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)
- 在两个组件中使用相同的 Hook 会共享 state 吗?不会。
- 自定义 Hook 如何获取独立的 state?每次调用 Hook,它都会获取独立的 state。
我们来自己实现一个简单的hook,形式类似useState,作用是设置一个div标签显示当前屏幕的scrollY以及设置点击时跳到顶部。
import { useEffect, useState } from "react";
const useCustom = () => {
const [y, setY] = useState(window.scrollY);
useEffect(() => {
window.onscroll = () => {
setY(window.scrollY);
};
return () => {
window.onscroll = null;
};
}, []);
return [
y.toFixed(3),
(newY) => {
window.scrollTo(0, newY);
setY(newY);
},
];
};
const Test = () => {
const arr = new Array(20).fill(0);
const style = {
width: "100px",
height: "100px",
background: "red",
margin: "5px",
};
const style2 = {
position: "fixed",
left: 0,
top: "500px",
width: "100px",
background: "black",
color: "white",
};
const [y, setY] = useCustom();
return (
<>
<div
style={style2}
onClick={() => {
setY();
}}
>
{y}
</div>
{arr.map((item, index) => (
<div key={index} style={style}></div>
))}
</>
);
};
export default Test;