React Hooks简单用法了解
hooks数据更新驱动
useState
const [state, dispatch] = useState(initData);
-
state是一个值,可以交给render来渲染。
-
dispatch是改变state的函数。
-
initData是state值得初始化值,可以是一个值也可以是一个函数,如果是一个函数则state的初始化值就是函数的返回值。
Demo
import React, { useState } from 'react';
const App = ()=>{
const [num, setNum] = useState(0);
return <div onClick={()=>{
setNum(num+1);
console.log(num);//这里的num是不能够及时改变的
}}>
{num}
</div>
}
export default App;
注意点:
-
在函数组件的一次上下文中,state的值是固定不变的,及时你使用dispatch对state进行修改,在视图中state改变,但是打印的state还是没有改变。
-
如果dispatch传入的值state的值不变或地址值不变,则组件不会更新。
import React, { useState } from 'react';
const App = ()=>{
const [num, setNum] = useState({name: 'name'});
return <div onClick={()=>{
num.name = 'name2';
setNum(num);//因为num的地址值没有改变,所以组件不会更新,视图也不会改变
}}>
{num.name}
</div>
}
export default App;
-
dispatch在当前执行上下文中获取不到最新的state,只有在下一组件render中才能获取到。
useReducer
const [state, dispatch] = useReducer(reducer);
-
state
-
dispatch和useState中的dispatch是一样的。
-
reducer是一个函数,接收两个参数,state和action,state是之前的state值,action是传入的值,如果state和action相同,则组件不会更新。
Demo
import React, { useReducer } from 'react';
const App = ()=>{
const [num, setNum] = useReducer((state:number, action:{name:'add'|'sub'|'reset'; payload:number})=>{
const {name, payload} = action;
switch(name){
case 'add':
return state + 1;
case 'sub':
return state - 1;
case 'reset':
return payload;
}
},0)
return <div>
当前值:{num}
{/* 派发更新 */}
<button onClick={()=>setNum({name:'add', payload:0})}>增加</button>
<button onClick={()=>setNum({name:'sub', payload:0})}>减少</button>
<button onClick={()=>setNum({name:'reset', payload: 666})}>赋值</button>
</div>
}
export default App;
useSyncExternalStore
useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot
)
-
subscribe为订阅函数,当数据改变的时候会触发subscribe,useSyncExternalStore会通过带有记忆性的getSnapshot来判断数据是否发生变化,如果发生变化则会强制更新数据
-
getSnapshot 可以理解成一个带有记忆功能的选择器。当 store 变化的时候,会通过 getSnapshot 生成新的状态值,这个状态值可提供给组件作为数据源使用,getSnapshot 可以检查订阅的值是否改变,改变的话那么会触发更新。
-
getServerSnapshot 用于 hydration 模式下的 getSnapshot。
import React, { useReducer, useState, useSyncExternalStore } from 'react';
import {combineReducers, createStore} from 'redux'
function numberReducer(state=1,action:any){
switch (action.type){
case 'ADD':
return state + 1
case 'DEL':
return state - 1
default:
return state
}
}
const rootReducer = combineReducers({number: numberReducer});
const store = createStore(rootReducer,{number:1});
const App = ()=>{
const state = useSyncExternalStore(store.subscribe,()=>store.getState().number);
return <div>
{state}
<button onClick={()=>store.dispatch({type: 'ADD'})}>点击</button>
</div>
}
export default App;
useTransition
const [ isPending , startTransition ] = useTransition ()
-
isPending是出于过度状态的标志。
-
startTransition可以把更新任务变成过渡任务。
Demo
import React, { useState, useTransition } from 'react';
/* 模拟数据 */
const mockList1 = new Array(10000).fill('tab1').map((item,index)=>item+'--'+index )
const mockList2 = new Array(10000).fill('tab2').map((item,index)=>item+'--'+index )
const mockList3 = new Array(10000).fill('tab3').map((item,index)=>item+'--'+index )
const tab = {
tab1: mockList1,
tab2: mockList2,
tab3: mockList3
}
const App = ()=>{
const [active, setActive] = useState('tab1');
const [renderData, setRenderData] = useState(tab[active]);
const [isPending, startTransition] = useTransition();
function handleChangeTab(activeItem:string){
setActive(activeItem);//立即更新
startTransition(()=>{//startTransition里面的任务优先级低
setRenderData(tab[activeItem]);
})
}
return <div>
<div className='tab'>
{Object.keys(tab).map((item,idx)=><span key={idx} className={active === item ? 'active':''} onClick={()=>handleChangeTab(item)}>{item}</span>)}
</div>
<ul className='content'>
{isPending&&<div>loading...</div>}
{renderData.map((item:string)=><li key={item}>{item}</li>)}
</ul>
</div>
}
export default App;
useDeferredValue
useDeferredValue和useTransition类似。它两个本质上和内部实现一样都是标记成了过渡更新任务。
useTransition是把其内部的更新任务变成了过渡任务transtion,而useDeferredValue是把原值通过过渡任务得到新的值,这个值作为延时状态。一个是处理一段逻辑,一个是产生一个新的状态。
const deferrredValue = React.useDeferredValue(value)
-
value一般为可变的state
-
deferrredValue是返回的延时状态
Demo 将useTransition的例子用useDeferredValue实现
import React, { useDeferredValue, useState } from 'react';
import './index.css'
/* 模拟数据 */
const mockList1 = new Array(10000).fill('tab1').map((item,index)=>item+'--'+index )
const mockList2 = new Array(10000).fill('tab2').map((item,index)=>item+'--'+index )
const mockList3 = new Array(10000).fill('tab3').map((item,index)=>item+'--'+index )
const tab = {
tab1: mockList1,
tab2: mockList2,
tab3: mockList3
}
const App = ()=>{
const [active, setActive] = useState('tab1');
const deferActive = useDeferredValue(active);
function handleChangeTab(activeItem:string){
setActive(activeItem);//立即更新
}
const renderData = tab[deferActive]//滞后状态
return <div>
<div className='tab'>
{Object.keys(tab).map((item,idx)=><span key={idx} className={active === item ? 'active':''} onClick={()=>handleChangeTab(item)}>{item}</span>)}
</div>
<ul className='content'>
{renderData.map((item:string)=><li key={item}>{item}</li>)}
</ul>
</div>
}
export default App;
hooks执行副作用
useEffect
useEffect、useLayoutEffect、useInsertionEffect弥补了函数组件没有生命周期的缺陷。其中最常用的就是 useEffect。
useEffect(()=>{
return destory
},dep)
-
第一个参数-callbacb,返回的destory作为下一次callback执行之前调用,可以用来清除上一次callback产生的副作用,可以代替卸载组件的声明周期。
-
dep是依赖项,是一个数组,可以有多个依赖项。当dep发生改变,执行上一次callback返回的destory,和执行新的effect第一个参数callback。对于 useEffect 执行, React 处理逻辑是采用异步调用 ,对于每一个 effect 的 callback, React 会向 setTimeout回调函数一样,放入任务队列,等到主线程任务完成,DOM 更新,js 执行完成,视图绘制完毕,才执行。所以 effect 回调函数不会阻塞浏览器绘制视图。
Demo
import React, { useEffect } from "react";
//模仿请求数据
function msg(){
return new Promise((resolve)=>{
setTimeout(()=>{
resolve({
name: 'name1',
age: 16
})
},500)
})
}
const App = ()=>{
//因为useEffect第二个参数为空数组,说明这个useEffect只执行一次,相当于mouted生命周期函数
useEffect(()=>{
msg().then(val=>{
console.log(val);
})
},[]);
return <div></div>
}
export default App;
useEffect 中做的功能如下:
-
请求数据。
-
设置定时器,延时器等。
-
操作 dom , 在 React Native 中可以通过 ref 获取元素位置信息等内容。
-
注册事件监听器, 事件绑定,在 React Native 中可以注册 NativeEventEmitter 。
-
还可以清除定时器,延时器,解绑事件监听器等。
useLayoutEffect
useLayoutEffect和useEffect的区别
-
useLayoutEffect是同步的,useEffect是异步的。
-
useLayoutEffect是在DOM更新之后,浏览器绘制之前。useEffect是在DOM更新之后,浏览器绘制之后。所以如果要修改DOM可以在useLayoutEffect中,这样避免了在useEffect中,绘制过后再次绘制。
-
useLayoutEffect callback 中代码执行会阻塞浏览器绘制。因为useLayoutEffect是在DOM结构更新后,渲染前执行,所以相当于有一个防抖效果。
import React, { useEffect, useState } from "react";
const App = ()=>{
const [value, setValue] = useState(0);
useEffect(() => {
if (value === 0) {
setValue(10 + Math.random() * 200);
}
}, [value]);
console.log("render", value);
return (
<div onClick={() => setValue(0)}>value: {value}</div>
);
}
export default App;
上面的代码执行的顺序是:
-
点击
-
虚拟DOM设置到真实DOM上
-
渲染
-
执行useEffect回调
-
调用setVlaue更新value
-
虚拟DOM设置到真实DOM上
-
渲染因为在很短的时间内渲染了两次,所以导致页面会出现闪动。我们可以用useLayoutEffect改进。
import React, { useLayoutEffect, useState } from "react";
const App = ()=>{
const [value, setValue] = useState(0);
useLayoutEffect(() => {
if (value === 0) {
setValue(10 + Math.random() * 200);
}
}, [value]);
console.log("render", value);
return (
<div onClick={() => setValue(0)}>value: {value}</div>
);
}
export default App;
上面的代码执行的顺序是:
-
点击
-
虚拟DOM设置到真实DOM上
-
执行useLayoutEffect回调
-
调用setVlaue更新value
-
虚拟DOM设置到真实DOM上
-
渲染上面的代码只渲染了一次,不会出现页面闪动。
useInsertionEffect(一般不使用)
-
useInsertionEffect比useLayoutEffect和useEffect的执行都要靠前。
-
useInsertionEffect执行的时候,DOM还没有更新。
-
useInsertionEffect主要解决css-in-js在渲染中注入样式的性能问题。
Demo
import React, { useInsertionEffect } from "react";
const App = ()=>{
useInsertionEffect(()=>{
const style = document.createElement('style');
style.innerHTML = `
.css-in-js{
color: red;
font-size: 20px;
}
`
document.head.appendChild(style);
},[]);
return <div className="css-in-js">123</div>
}
export default App;
hooks状态获取与传递
useContext
可以使用useContext接收父组件传过来的参数,也可以使用Context.Consumer接收父组件传过来的参数。
Demo
//App.tsx文件
import React, { createContext, useContext, useState } from "react";
import { Children2 } from "./components/children";
export const CountContext = createContext({num:0,count:0});
const App = ()=>{
const [num, setNum] = useState(666);
const [count, setCount] = useState(888);
return <div>
<p>父组件num和count:{num},{count}</p>
<button onClick={()=>setNum(num+1)}>点击</button>
<button onClick={()=>setCount(count-1)}>点击</button>
<CountContext.Provider value={{num,count}}>
<Children/>
<Children2/>
</CountContext.Provider>
</div>
}
export const Children = ()=>{
const {num, count} = useContext(CountContext);
return <div>子组件num和count:{num},{count}</div>
}
export default App;
//子组件文件
import React from 'react';
import { CountContext } from '../../App';
export const Children2 = ()=>{
return <CountContext.Consumer>
{
({num, count})=><div>子组件num和count:{num},{count}</div>
}
</CountContext.Consumer>
}
useRef
useRef可以用来获取DOM元素,缓存状态,在React Native中虽然没有 DOM 元素,但是也能够获取组件的节点信息。
const cur = React.useRef(initState)
console.log(cur.current)
-
cur上有一个current就是ref对象需要获取的内容。
-
initState是初始化值。
Demo
import React, { useRef } from "react";
const App = ()=>{
const dom = useRef(null);
const click = ()=>{
console.log(dom.current);
}
return <div>
<div ref={dom}>DOM</div>
<button onClick={click}>点击</button>
</div>
}
export default App;
useRef 保存状态, 可以利用 useRef 返回的 ref 对象来保存状态,只要当前组件不被销毁,那么状态就会一直存在。
Demo
import React, { useRef } from "react";
const App = ()=>{
const dom = useRef(false);
const click = ()=>{
dom.current = !dom.current;
console.log(dom.current);
}
return <div>
<button onClick={click}>点击</button>
</div>
}
export default App;
useImperativeHandle
对于子组件,如果是 class 类组件,我们可以通过 ref 获取类组件的实例,但是在子组件是函数组件的情况,如果我们就需要useImperativeHandle和forwardRef配合来实现。
useImperativeHandle(ref,createHandle,deps)
-
第一个参数ref是接收forWardRef传过来的ref。
-
第二个参数createHandle是处理函数,返回值作为暴露给父组件的 ref 对象。
-
第三个参数deps是依赖项,依赖项更改形成新的 ref 对象。
hooks状态派生与保存
useMemo
useMemo 可以在函数组件 render 上下文中同步执行一个函数逻辑,这个函数的返回值可以作为一个新的状态缓存起来。
const cacheSomething = useMemo(create,deps)
-
create是一个函数,函数的返回值作为缓存值,缓存起来。
-
deps是一个数组,存放useMemo的依赖项。再下一次函数组件执行时,对比deps依赖里面的状态,如果改变重新执行 create ,得到新的缓存值。
-
cacheSomething执行create的返回值。如果deps中有依赖项改变,返回的重新执行create产生的值,否则取上一次缓存值。
Demo
当持续点击一个按钮,只会输出“组件更新了”,当两个按钮切换着点击才会显示useMemo改变了。
import React, { useEffect, useMemo, useState } from "react";
const App = ()=>{
const [num, setNum] = useState({name: 'name', age:1});
function handerClick(num:number){
setNum({name: 'name'+Math.random(), age:num});
}
return <div>
<button onClick={()=>handerClick(1)}>点击</button>
<button onClick={()=>handerClick(2)}>点击</button>
<Son msg={num}/>
</div>
}
export const Son = (props:{msg:{name:string,age:number}})=>{
const memo = useMemo(()=>{
console.log('useMemo改变了');
return props.msg.age;
}, [props.msg.age])
useEffect(()=>{
console.log('组件更新了');
},[props.msg])
return <div></div>
}
export default App;
useCallback
useMemo 和 useCallback 接收的参数都是一样,区别在于 useMemo 返回的是函数运行的结果,useCallback 返回的是函数。
Demo
//子组件会有不必要渲染的例子
interface ChildProps {
name: { name: string; color: string };
onClick: Function;
}
const Child = ({ name, onClick}: ChildProps): JSX.Element => {
console.log('子组件?')
return(
<>
<div style={{ color: name.color }}>我是一个子组件,父级传过来的数据:{name.name}</div>
<button onClick={onClick.bind(null, '新的子组件name')}>改变name</button>
</>
);
}
const ChildMemo = memo(Child);
const Page = (props) => {
const [count, setCount] = useState(0);
const [name, setName] = useState('Child组件');
return (
<>
<button onClick={(e) => { setCount(count+1) }}>加1</button>
<p>count:{count}</p>
<ChildMemo
name={{ name, color: name.indexOf('name') !== -1 ? 'red' : 'green'}}
onClick={ useCallback((newName: string) => setName(newName), []) }
/>
</>
)
}
hooks工具hooks
useDebugValue
import React, { useDebugValue, useEffect, useState } from "react";
const App = ()=>{
const id = useFriendStatus(1);
return <div>{id}</div>
}
function useFriendStatus(friendID:number) {
useEffect(()=>{
setIsOnline(friendID);
},[friendID])
const [isOnline, setIsOnline] = useState(0);
// ...
// 在开发者工具中的这个 Hook 旁边显示标签
// e.g. "FriendStatus: Online"
useDebugValue(isOnline!==0 ? 'Online' : 'Offline');
return isOnline;
}
export default App;
在开发者工具中的这个hooks旁边会显示标签。
useId
useId用来生成唯一id。
function Demo (){
const rid = useId() // 生成稳定的 id
return <div id={rid} ></div>
}