约半年前 React 大力推广其新出的 Hooks API,而最近沉迷 TS 不能自拔,因此很多地方我直接 Hooks + TS 来写了。
# useState ↵
利用 useState 可以用来声明变化的状态,比如一个
点击计数
:00import * as React from "react"; 01 02export function Counter() { 03 const [count, setCount] = React.useState(0); 04 const inc = () => setCount(count + 1); 05 06 return ( 07 <div onClick={ inc }>Click Me: { count }</div> 08 ) 09}
当调用 setCount 的时候, Counter 会重新渲染,此时的 count 是调用 setCount 的那个参数,用来设置新的 count 以得到最新的状态对应的视图。
React 通过 React Hooks 调用的顺序来推断状态的值及其 dispatcher (即上面的 setCount),比如在下面代码中,程序根据 count 的状态来生成
test
test2
:00import * as React from "react"; 01 02export function Counter() { 03 const [count, setCount] = React.useState(0); 04 05 let temp1: [number, (n: number) => void]; 06 let temp2: [number, (n: number) => void]; 07 08 if (count >= 3) { 09 temp1 = React.useState(0); 10 temp2 = React.useState(6); 11 } else { 12 temp2 = React.useState(6); 13 temp1 = React.useState(0); 14 } 15 16 return ( 17 <div> 18 <div onClick={() => { 19 setCount(count + 1); 20 }}>Click Me: { count }</div> 21 22 {/* test1 counter */} 23 <div onClick={() => { 24 temp1[1](temp1[0] + 1); 25 }}>test1: { temp1[0] }</div> 26 27 {/* test2 counter */} 28 <div onClick={() => { 29 temp2[1](temp2[0] + 1); 30 }}>test2: { temp2[0] }</div> 31 </div> 32 ); 33}
上述程序,一开始显示:
00Click Me: 0 01test1: 0 02test2: 6
而后点击
Click Me
直到 3 的时候,结果会变成这样:00Click Me: 3 01test1: 6 02test2: 0
说明 React 里用 useState 的调用序来作为其状态的 key。(在传统类组件里是通过 state 上的键名来唯一确定某个状态)
# useEffect ↵
useEffect 用于监听各种副作用,比如在 counter 中 count 初始值延迟初始化为一个随机数:
00function Hello() { 01 const [count, setCount] = React.useState(0); 02 const inc = () => setCount(count + 1); 03 04 // 副作用函数 05 React.useEffect(() => { 06 console.log('init'); 07 const r = Math.round(Math.random() * 10); 08 setTimeout(() => { 09 setCount(r); 10 }, 500); 11 }); // 注意这里没有写 deps 依赖 12 13 return ( 14 <div onClick={ inc }> 15 Click Me: { count } 16 </div> 17 ); 18}
这个初始值取决于一个随机数,但是实际运行中,却是每 0.5s 触发一次副作用函数,其原因在于我们没有指定其副作用的依赖,这需要设置 useEffect 来告诉副作用应该何时运行:
00function Hello() { 01 const [count, setCount] = React.useState(0); 02 const inc = () => setCount(count + 1); 03 04 // 副作用函数 05 React.useEffect(() => { 06 console.log('init'); 07 const r = Math.round(Math.random() * 10); 08 setTimeout(() => { 09 setCount(r); 10 }, 500); 11 }, [] /* 12 注意这个空数组,它表明这个副作用没有任何依赖 13 此时这个副作用退化为类组件中的 componentDidMount 生命周期钩子 14 */); 15 16 return ( 17 <div onClick={ inc }> 18 Click Me: { count } 19 </div> 20 ); 21}
useEffect 的第二个参数叫做
依赖列表
它用于指定副作用的依赖,当列表中的项变化的时候会触发副作用重新刷新。上面这种情况意味着副作用不依赖任何对象,此时副作用函数退化为类组件中的 componentDidMount
生命周期钩子。在下面的例子里,我们以 count
本身作为依赖对象:00function Hello() { 01 const [count, setCount] = React.useState(0); 02 const inc = () => setCount(count + 1); 03 04 // 副作用函数 05 React.useEffect(() => { 06 console.log('Count Changed'); 07 ajax('/set-new-count', { count }); 08 }, [ count ]); 09 10 return ( 11 <div onClick={ inc }> 12 Click Me: { count } { text } 13 </div> 14 ); 15}
在上面的例子里,指定 count 为依赖对象使得该副作用域变成
count watcher
了,在这个副作用里面我们可以吧 count 传到服务器,但是,这会出现另外一种情况,如果用户疯狂连续触发 inc
呢?这样可能会疯狂对服务器发起请求,此时需要利用副作用函数的返回值来消除作用域,可以这样处理副作用函数,用于消除其对组件、对程序的影响:00function Hello() { 01 const [count, setCount] = React.useState(0); 02 const inc = () => setCount(count + 1); 03 04 // 副作用函数 05 React.useEffect(() => { 06 console.log('Count Changed'); 07 08 const t = setTimeout(() => { 09 ajax('/set-new-count', { count }); 10 }); 11 12 return () => clearTimeout(t); 13 }, [ count ]); 14 15 return ( 16 <div onClick={ inc }> 17 Click Me: { count } { text } 18 </div> 19 ); 20}
顺便提一下,组件的 props 也可以作为依赖对象:
00function Test(props: any) { 01 React.useEffect(() => { 02 console.log('props changed'); 03 }, [ props ]); 04 05 return <div>...</div> 06}
至此,我们已可以使用
useState
和 useEffect
来完成类组件做的事了。# Context ↵
利用 useContext 纯函数组件里也可以使用 React Context:
00import * as React from "react"; 01 02type MsgCtx = { 03 msg: string; 04 setMsg: (msg: string) => void 05} 06 07const msgCtx = React.createContext({} as MsgCtx); 08 09export function App() { 10 return ( 11 <MsgCtxProvider> 12 <MyInnerComponent /> 13 </MsgCtxProvider> 14 ) 15} 16 17function MsgCtxProvider(props: React.PropsWithChildren<{}>) { 18 const [msg, setMsg] = React.useState(''); 19 20 const inner = ( 21 <div> 22 <div>{ msg }</div> 23 { props.children } 24 </div> 25 ); 26 27 return ( 28 <msgCtx.Provider value={{ msg, setMsg }}> 29 { inner } 30 </msgCtx.Provider> 31 ); 32} 33 34function MyInnerComponent() { 35 const ctx = React.useContext(msgCtx); 36 37 return ( 38 <div onClick={ () => { 39 ctx.setMsg('hello, msg'); 40 } }> 41 click here set msg: 42 </div> 43 ) 44}
Context 的意义在于将另外一个组件里的 hooks 给其他组件用,从而达到 lift state up 的效果(上述代码中将 msg setMsg 这两个 hooks 写在了 MsgCtxProvider 里,MyInnerComponent 可以通过 useContext 来使用这两个 hooks (注:context 也可以作为依赖列表)。
# Todo App Example ↵
以下是利用 Hooks API 去实现
TODO
应用的全部代码,可以参考一下。