useTransition
返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数。它和 CSS 的过渡没有任何关系。useTransition 的主要目的是作用于在复杂的过渡任务时提供一个优先级较低的更新,过渡任务中触发的更新会让更紧急地更新先进行,比如点击。
过渡任务中的更新将不会展示由于再次挂起而导致降级的内容。这个机制允许用户在 React 渲染更新的时候继续与当前内容进行交互。
这里渲染一个长度为 10 万的列表和两个不同的按钮,第一个按钮会增加列表的长度并使第一个状态数值加一。第二个按钮只会增加自己的状态。
如果在这里设置状态时不使用 setTransiton
,那么在整个列表进行渲染时将无法处理其他任何操作。
在使用了 setTransition
后,整个列表进行渲染操作优先级将会比其他更紧急地操作低,从而可以响应第二个按钮点击。
useTransition
返回的元组中包含两个值 [pending, setTransiton]
,分别是 setTransiton
方法和表示正在过渡的状态 pending
。如果需要在过渡时展示特定的 UI 就可以使用 pending
来控制状态。
import Button from './Button' ;
import { useState , useTransition } from 'react' ;
const UseTransition = ( ) => {
const [ value , setValue ] = useState ( 0 ) ;
const [ value2 , setValue2 ] = useState ( -1 ) ;
const [ length , setLength ] = useState ( 30000 ) ;
const [ pending , setTransiton ] = useTransition ( ) ;
const handleClick = ( ) => {
setValue ( ( v ) => v + 1 ) ;
setTransiton ( ( ) => setLength ( ( l ) => l + 1 ) ) ;
} ;
return (
< >
< div className ="my-4" >
< Button onClick ={ handleClick } > { value } </ Button >
< Button onClick ={ ( ) => setValue2 ( ( v ) => v - 1 ) } > { value2 } </ Button >
</ div >
< div
className ={ `wrapper ${ pending && `fade` } ` }
>
{ Array .from ( { length } ) .map ( ( _ , i ) => (
< div className ="p-2 mb-2 mr-2 rounded-md shadow" key ={ length - i } >
{ length - i }
</ div >
) ) }
</ div >
< style > { `
.wrapper {
transition: all 0.3 ease;
}
.fade {
opacity: 0.5;
}` }
</ style >
</ >
) ;
} ;
export default UseTransition ;
useDeferredValue
useDeferredValue
接受一个状态值,并返回该的副本。返回的副本状态值将会在其他紧急更新后更新。它与 useTransition
比较类似,二者可以搭配使用。
因为函数的原子性,在整个组件更新(重新渲染)时,子组件也会随着一起更新。这里通过设置了状态 value
来对整个组件重新渲染,即使下方的列表没有任何变化也会一起重新渲染。而重新渲染 UI 界面对我们来说就是紧急更新。
将原本的状态值 value
与 useDeferredValue
返回的副本相比较就会发现 value
会随着 UI 一起被更新,而被延迟的状态 deferred
会等待 UI 更新结束后再做更新。
import Button from './Button' ;
import { useDeferredValue , useState } from 'react' ;
const UseDeferredValue = ( ) => {
const [ value , setValue ] = useState ( 0 ) ;
const deferred = useDeferredValue ( value ) ;
return (
< >
< div className ="my-4" >
< div >
Deferred:
< Button onClick ={ ( ) => setValue ( ( v ) => v + 1 ) } > { deferred } </ Button >
</ div >
< div >
Primtive:
< Button onClick ={ ( ) => setValue ( ( v ) => v + 1 ) } > { value } </ Button >
</ div >
</ div >
< div >
{ Array .from ( { length : 30000 } ) .map ( ( _ , i ) => (
< div className ="p-2 mb-2 mr-2 rounded-md shadow" key ={ i } >
{ i }
</ div >
) ) }
</ div >
</ >
) ;
} ;
export default UseDeferredValue ;
useId
useId
与其他的都不同,从名字就能看出来它的作用,返回一个在整个 APP 中唯一的 ID。它能够保证每个组件调用它返回的 ID 都唯一,即使是同一个组件被父组件多次调用。
通常的作用有:
为页面中需要唯一 ID 元素提供 ID,例如 <label for="ID">
。
SSR 到客户端注入时需要 ID 避免错误。
import Input from './Input' ;
import { useId } from 'react' ;
const RUAForm = ( ) => {
const id = useId ( ) ;
return (
< >
< label htmlFor ={ ` ${ id } ` } > Label 1: </ label >
< div >
< Input type ="text" id ={ ` ${ id } ` } />
</ div >
</ >
) ;
} ;
const UseId = ( ) => {
return (
< >
< RUAForm />
< RUAForm />
</ >
) ;
} ;
export default UseId ;
useSyncExternalStore
useSyncExternalStore
是目前最适合用来取代全局状态库的 hook,尤其是配合 redux 的思想后,简直就是手写的 mini redux。
首先看下该钩子的前面,其实不是很复杂。它接受一个范型,用于推断返回的状态。剩下的三个参数分别是:
subscribe
: 一个当状态更新后可执行的回调函数。该函数会收到一个回调函数,这个回调函数就是当状态后执行,React 用来对比是否需要重新渲染组件。
getSnapshot
: 返回当前状态的函数。
getServerSnapshot
: 在服务端渲染时返回当前状态的函数,可选。
useSyncExternalStore<State >(subscribe : (onStoreChange: () => void ) => () => void , getSnapshot : () => State , getServerSnapshot?: (() => State ) | undefined ): State
useSyncExternalStore
在组件中使用与 useSelector
很相似:
const { count, info } = useSyncExternalStore (
store.subscribe ,
store.getSnapshot ,
);
但它的重点还是在 store 上:
export type State = {
count : number ;
info : string ;
};
export type Store = {
state : State ;
setState : (
stateOrFn: Partial<State> | ((state: State) => Partial<State>),
) => void ;
subscribe : (listener: () => void ) => () => void ;
listeners : Set <() => void >;
getSnapshot : () => State ;
};
const store : Store = {
state : {
count : 0 ,
info : 'Hello' ,
},
setState (stateOrFn ) {
const newState =
typeof stateOrFn === 'function' ? stateOrFn (store.state ) : stateOrFn;
store.state = {
...store.state ,
...newState,
};
store.listeners .forEach ((listener ) => listener ());
},
listeners : new Set (),
subscribe (listener ) {
store.listeners .add (listener);
return () => {
store.listeners .delete (listener);
};
},
getSnapshot ( ) {
return store.state ;
},
};
export default store;
其中, listeners
用于存放 subscribe
的回调,在我们更新状态后需要通知 React 来更新组件。所以在 setState
中遍历执行。之所以使用 Set()
是因为 subscribe
还需要返回一个函数用于注销 listener
。
import Button from './Button' ;
import Input from './Input' ;
import { useSyncExternalStore } from 'react' ;
import store from './store' ;
const Couter = ( ) => {
const { count , info } = useSyncExternalStore (
store .subscribe ,
store .getSnapshot
) ;
return (
< >
< div >
< div >
Count: < span > { count } </ span >
</ div >
< div >
Info: < span > { info } </ span >
</ div >
< div >
< Button
onClick ={ ( ) => store .setState ( ( d ) => ( { ... d , count : d .count + 1 } ) ) }
>
Add
</ Button >
</ div >
</ div >
</ >
) ;
} ;
const Infor = ( ) => {
const { count , info } = useSyncExternalStore (
store .subscribe ,
store .getSnapshot
) ;
return (
< >
< div >
< div >
Count: < span > { count } </ span >
</ div >
< div >
Info: < span > { info } </ span >
</ div >
< div >
< Input
type ="text"
onChange ={ ( e ) => store .setState ( ( d ) => ( { ... d , info : e .target .value } ) ) }
value ={ info }
/>
</ div >
</ div >
</ >
) ;
} ;
const UseSyncExternalStore = ( ) => {
return (
< >
< Couter />
< hr className ="my-4" />
< Infor />
</ >
) ;
} ;
export default UseSyncExternalStore ;
useInsertionEffect
useInsertionEffect
在通常情况下都用不上,它的唯一目的是对 CSS-in-JS 库很重要。
CSS-in-JS 库通常需要在运行时插入或修改 <style>
标签等。当 CSS 规则被添加或删除时,浏览器必须检查这些规则是否适用于现有的 DOM 树。它必须重新计算所有的样式规则并重新应用它们--而不仅仅是改变了的那些。如果 React 发现另一个组件也产生了一个新的规则,同样的过程会再次发生。
解决这个问题的最好办法就是所有东西呈现给浏览器绘制前就进行改变。没错 useInsertionEffect
与 useEffect
有着同样的签名,但它会同步的在所有 DOM 更改之前触发。比 useLayoutEffect
还要早触发,这样就可以用于在重绘之前注入样式。
import { useEffect , useLayoutEffect , useInsertionEffect } from 'react' ;
const Child = ( ) => {
useEffect ( ( ) => {
console .log ( 'useEffect child is called' ) ;
} ) ;
useLayoutEffect ( ( ) => {
console .log ( 'useLayoutEffect child is called' ) ;
} ) ;
useInsertionEffect ( ( ) => {
console .log ( 'useInsertionEffect child is called' ) ;
} ) ;
return < > </ > ;
} ;
const UseInsertionEffect = ( ) => {
useEffect ( ( ) => {
console .log ( 'useEffect app is called' ) ;
} ) ;
useLayoutEffect ( ( ) => {
console .log ( 'useLayoutEffect app is called' ) ;
} ) ;
useInsertionEffect ( ( ) => {
console .log ( 'useInsertionEffect app is called' ) ;
} ) ;
return (
< >
< Child />
< div > Check console in DevTools</ div >
</ >
) ;
} ;
export default UseInsertionEffect ;