首頁>技術>

推薦閱讀:

看人家 Typescript 和 React hooks 耍的溜的飛起,好羨慕啊~ 那來吧,這篇爽文從腦殼到jio乾地教你如何使用這兩大利器開始閃亮開發!✨

課前預知

我覺得比較好的學習方式就是跟著所講的內容自行實現一遍,所以先啟個專案唄~

npx create-react-app hook-ts-demo --template typescript

在 src/App.tsx 內引用我們的案例元件,在 src/example.tsx 寫我們的案例元件。

函式式元件的使用~ 我們可以通過以下方式使用有型別約束的函式式元件:

import React from 'react'type UserInfo = {  name: string,  age: number,}export const User = ({ name, age }: UserInfo) => {  return (    <div className="App">      <p>{ name }</p>      <p>{ age }</p>    </div>  )}const user = <User name='vortesnail' age={25} />

也可以通過以下方式使用有型別約束的函式式元件:

import React from 'react'type UserInfo = {  name: string,  age: number,}export const User:React.FC<UserInfo> = ({ name, age }) => {  return (    <div className="User">      <p>{ name }</p>      <p>{ age }</p>    </div>  )}const user = <User name='vortesnail' age={25} />

上述程式碼中不同之處在於:

export const User = ({ name, age }: UserInfo)  => {}export const User:React.FC<UserInfo> = ({ name, age }) => {}

使用函式式元件時需要將元件申明為React.FC型別,也就是 Functional Component 的意思,另外props需要申明各個引數的型別,然後通過泛型傳遞給React.FC。

雖然兩種方式都差不多,但我個人更喜歡使用 React.FC 的方式來建立我的有型別約束的函式式元件,它還支援 children 的傳入,即使在我們的型別中並沒有定義它:

export const User:React.FC<UserInfo> = ({ name, age, children }) => {  return (    <div className="User">      <p>{ name }</p>      <p>{ age }</p>      <div>        { children }      </div>    </div>  )}const user = <User name='vortesnail' age={25}>I am children text!</User>

我們也並不需要把所有引數都顯示地解構:

export const User:React.FC<UserInfo> = (props) => {  return (    <div className="User">      <p>{ props.name }</p>      <p>{ props.age }</p>      <div>        { /* 仍可以拿到 children */ }        { props.children }      </div>    </div>  )}const user = <User name='vortesnail' age={25}>I am children text!</User>

好了,我們暫時知道上面這麼多,就可以開始使用我們的 hooks 了~

我將從三個點闡述如何結合 typescript 使用我們的 hooks :

為啥使用❓怎麼使用場景例舉useState為啥使用useState?

可以讓函式式元件擁有狀態管理特性,類似 class 元件中的 this.state 和 this.setState ,但是更加簡潔,不用頻繁的使用 this 。

怎麼使用useState?
const [count, setCount] = useState<number>(0)
場景舉例1.引數為基本型別時的常規使用:
import React, { useState } from 'react'const Counter:React.FC<{ initial: number }> = ({ initial = 0 }) => {  const [count, setCount] = useState<number>(initial)  return (    <div>      <p>Count: {count}</p>      <button onClick={() => setCount(count+1)}>加</button>      <button onClick={() => setCount(count-1)}>減</button>    </div>  )}export default Counter
2.引數為物件型別時的使用:
import React, { useState } from 'react'type ArticleInfo = {  title: string,  content: string}const Article:React.FC<ArticleInfo> = ({ title, content }) => {  const [article, setArticle] = useState<ArticleInfo>({ title, content })  return (    <div>      <p>Title: { article.title }</p>      <section>{ article.content }</section>      <button onClick={() => setArticle({        title: '下一篇',        content: '下一篇的內容',      })}>        下一篇      </button>    </div>  )}export default Article

在我們的引數為物件型別時,需要特別注意的是, setXxx 並不會像 this.setState 合併舊的狀態,它是完全替代了舊的狀態,所以我們要實現合併,可以這樣寫(雖然我們以上例子不需要):

setArticle({  title: '下一篇',  content: '下一篇的內容',  ...article})
useEffect為啥使用useEffect?

你可以把 useEffect 看做 componentDidMount , componentDidUpdate 和 componentWillUnmount 這三個函式的組合。

怎麼使用useEffect?
useEffect(() => {  ...  return () => {...}},[...])
場景舉例1.每當狀態改變時,都要重新執行 useEffect 的邏輯:
import React, { useState, useEffect } from 'react'let switchCount: number = 0const User = () => {  const [name, setName] = useState<string>('')  useEffect(() => {    switchCount += 1  })  return (    <div>      <p>Current Name: { name }</p>      <p>switchCount: { switchCount }</p>      <button onClick={() => setName('Jack')}>Jack</button>      <button onClick={() => setName('Marry')}>Marry</button>    </div>  )}export default User
2.即使每次狀態都改變,也只執行第一次 useEffect 的邏輯:
useEffect(() => {  switchCount += 1}, [])
3.根據某個狀態是否變化來決定要不要重新執行:
const [value, setValue] = useState<string>('I never change')useEffect(() => {  switchCount += 1}, [value])

因為 value 我們不會去任何地方改變它的值,所以在末尾加了 [value] 後, useEffect 內的邏輯也只會執行第一次,相當於在 class 元件中執行了 componentDidMount ,後續的 shouldComponentUpdate 返回全部是 false 。

4.元件解除安裝時處理一些記憶體問題,比如清除定時器、清除事件監聽:
useEffect(() => {  const handler = () => {    document.title = Math.random().toString()  }  window.addEventListener('resize', handler)  return () => {    window.removeEventListener('resize', handler)  }}, [])
useRef為啥使用useRef?

它不僅僅是用來管理 DOM ref 的,它還相當於 this , 可以存放任何變數,很好的解決閉包帶來的不方便性。

怎麼使用useRef?
const [count, setCount] = useState<number>(0)const countRef = useRef<number>(count)
場景舉例1.閉包問題:

想想看,我們先點選 加 按鈕 3 次,再點 彈框顯示 1次,再點 加 按鈕 2 次,最終 alert 會是什麼結果?

import React, { useState, useEffect, useRef } from 'react'const Counter = () => {  const [count, setCount] = useState<number>(0)  const handleCount = () => {    setTimeout(() => {      alert('current count: ' + count)    }, 3000);  }  return (    <div>      <p>current count: { count }</p>      <button onClick={() => setCount(count + 1)}>加</button>      <button onClick={() => handleCount()}>彈框顯示</button>    </div>  )}export default Counter

結果是彈框內容為 current count: 3 ,為什麼?

當我們更新狀態的時候, React 會重新渲染元件, 每一次渲染都會拿到獨立的 count 狀態, 並重新渲染一個 handleCount 函式. 每一個 handleCount 裡面都有它自己的 count 。

** 那如何顯示最新的當前 count 呢?

const Counter = () => {  const [count, setCount] = useState<number>(0)  const countRef = useRef<number>(count)  useEffect(() => {    countRef.current = count  })  const handleCount = () => {    setTimeout(() => {      alert('current count: ' + countRef.current)    }, 3000);  }  //...}export default Counter
2.因為變更 .current 屬性不會引發元件重新渲染,根據這個特性可以獲取狀態的前一個值:
const Counter = () => {  const [count, setCount] = useState<number>(0)  const preCountRef = useRef<number>(count)  useEffect(() => {    preCountRef.current = count  })  return (    <div>      <p>pre count: { preCountRef.current }</p>      <p>current count: { count }</p>      <button onClick={() => setCount(count + 1)}>加</button>    </div>  )}

我們可以看到,顯示的總是狀態的前一個值:

3.操作 Dom 節點,類似 createRef():
import React, { useRef } from 'react'const TextInput = () => {  const inputEl = useRef<HTMLInputElement>(null)  const onFocusClick = () => {    if(inputEl && inputEl.current) {      inputEl.current.focus()    }   }  return (    <div>      <input type="text" ref={inputEl}/>      <button onClick={onFocusClick}>Focus the input</button>    </div>  )}export default TextInput
useMemo為啥使用useMemo?

從 useEffect 可以知道,可以通過向其傳遞一些引數來影響某些函式的執行。 React 檢查這些引數是否已更改,並且只有在存在差異的情況下才會執行此。

useMemo 做類似的事情,假設有大量方法,並且只想在其引數更改時執行它們,而不是每次元件更新時都執行它們,那就可以使用 useMemo 來進行效能優化。

記住,傳入 useMemo 的函式會在渲染期間執行。請不要在這個函式內部執行與渲染無關的操作,諸如副作用這類的操作屬於 useEffect 的適用範疇,而不是 useMemo 。

怎麼使用useMemo?
function changeName(name) {  return name + '給name做點操作返回新name'}const newName = useMemo(() => {\treturn changeName(name)}, [name])
場景舉例1.常規使用,避免重複執行沒必要的方法:

我們先來看一個很簡單的例子,以下是還未使用 useMemo 的程式碼:

import React, { useState, useMemo } from 'react'// 父元件const Example = () => {  const [time, setTime] = useState<number>(0)  const [random, setRandom] = useState<number>(0)  return (    <div>      <button onClick={() => setTime(new Date().getTime())}>獲取當前時間</button>      <button onClick={() => setRandom(Math.random())}>獲取當前隨機數</button>      <Show time={time}>{random}</Show>    </div>  )}type Data = {  time: number}// 子元件const Show:React.FC<Data> = ({ time, children }) => {  function changeTime(time: number): string {    console.log('changeTime excuted...')    return new Date(time).toISOString()  }  return (    <div>      <p>Time is: { changeTime(time) }</p>      <p>Random is: { children }</p>    </div>  )}export default Example

在這個例子中,無論你點選的是 獲取當前時間 按鈕還是 獲取當前隨機數 按鈕, <Show /> 這個元件中的方法 changeTime 都會執行。

但事實上,點選 獲取當前隨機數 按鈕改變的只會是 children 這個引數,但我們的 changeTime 也會因為子元件的重新渲染而重新執行,這個操作是很沒必要的,消耗了無關的效能。

使用 useMemo 改造我們的 <Show /> 子元件:

const Show:React.FC<Data> = ({ time, children }) => {  function changeTime(time: number): string {    console.log('changeTime excuted...')    return new Date(time).toISOString()  }  const newTime: string = useMemo(() => {    return changeTime(time)  }, [time])  return (    <div>      <p>Time is: { newTime }</p>      <p>Random is: { children }</p>    </div>  )}

這個時候只有點選 獲取當前時間 才會執行 changeTime 這個函式,而點選 獲取當前隨機數 已經不會觸發該函式執行了。

2.你可能會好奇, useMemo 能做的難道不能用 useEffect 來做嗎?

答案是否定的!如果你在子元件中加入以下程式碼:

const Show:React.FC<Data> = ({ time, children }) => {\t//...    useEffect(() => {    console.log('effect function here...')  }, [time])  const newTime: string = useMemo(() => {    return changeTime(time)  }, [time])  \t//...}

你會發現,控制檯會列印如下資訊:

> changeTime excuted...> effect function here...

正如我們一開始說的:傳入 useMemo 的函式會在渲染期間執行。 在此不得不提 React.memo ,它的作用是實現整個元件的 Pure 功能:

const Show:React.FC<Data> = React.memo(({ time, children }) => {...}

所以簡單用一句話來概括 useMemo 和 React.memo 的區別就是:前者在某些情況下不希望元件對所有 props 做淺比較,只想實現區域性 Pure 功能,即只想對特定的 props 做比較,並決定是否區域性更新。

useCallback為啥使用useCallback?

useMemo 和 useCallback 接收的引數都是一樣,都是在其依賴項發生變化後才執行,都是返回快取的值,區別在於 useMemo 返回的是函式執行的結果, useCallback 返回的是函式。

useCallback(fn, deps) 相當於 useMemo(() => fn, deps)

怎麼使用useCallback?
function changeName(name) {  return name + '給name做點操作返回新name'}const getNewName = useMemo(() => {  return changeName(name)}, [name])
場景舉例

將之前 useMemo 的例子,改一下子元件以下地方就OK了:

const Show:React.FC<Data> = ({ time, children }) => {  //...  const getNewTime = useCallback(() => {    return changeTime(time)  }, [time])  return (    <div>      <p>Time is: { getNewTime() }</p>      <p>Random is: { children }</p>    </div>  )}
useReducer為什麼使用useReducer?

有沒有想過你在某個元件裡寫了很多很多的 useState 是什麼觀感?比如以下:

const [name, setName] = useState<string>('')const [islogin, setIsLogin] = useState<boolean>(false)const [avatar, setAvatar] = useState<string>('')const [age, setAge] = useState<number>(0)//...複製程式碼
怎麼使用useReducer?
import React, { useState, useReducer } from 'react'type StateType = {  count: number}type ActionType = {  type: 'reset' | 'decrement' | 'increment'}const initialState = { count: 0 }function reducer(state: StateType, action: ActionType) {  switch (action.type) {    case 'reset':      return initialState    case 'increment':      return { count: state.count + 1 }    case 'decrement':      return { count: state.count - 1 }    default:      return state  }}function Counter({ initialCount = 0}) {  const [state, dispatch] = useReducer(reducer, { count: initialCount })  return (    <div>      Count: {state.count}      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>      <button onClick={() => dispatch({ type: 'increment' })}>+</button>      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>    </div>  )}export default Counter
場景舉例:

與 useContext 結合代替 Redux 方案,往下閱讀。

useContext為啥使用useContext?

簡單來說 Context 的作用就是對它所包含的元件樹提供全域性共享資料的一種技術。

怎麼使用useContext?
export const ColorContext = React.createContext({ color: '#1890ff' })const { color } = useContext(ColorContext)// 或export const ColorContext = React.createContext(null)<ColorContext.Provider value='#1890ff'>  <App /></ColorContext.Provider>// App 或以下的所有子元件都可拿到 valueconst color = useContext(ColorContext) // '#1890ff'
場景舉例1.根元件註冊,所有子元件都可拿到註冊的值:
import React, { useContext } from 'react'const ColorContext = React.createContext<string>('')const App = () => {  return (    <ColorContext.Provider value='#1890ff'>      <Father />    </ColorContext.Provider>  )}const Father = () => {  return (    <Child />  )}const Child = () => {  const color = useContext(ColorContext)  return (    <div style={{ backgroundColor: color }}>Background color is: { color }</div>  )}export default App
2.配合 useReducer 實現 Redux 的代替方案:
import React, { useReducer, useContext } from 'react'const UPDATE_COLOR = 'UPDATE_COLOR'type StateType = {  color: string}type ActionType = {  type: string,  color: string}type MixStateAndDispatch = {  state: StateType,  dispatch?: React.Dispatch<ActionType>}const reducer = (state: StateType, action: ActionType) => {  switch(action.type) {    case UPDATE_COLOR:      return { color: action.color }    default:      return state    }}const ColorContext = React.createContext<MixStateAndDispatch>({  state: { color: 'black' },})const Show = () => {  const { state, dispatch } = useContext(ColorContext)  return (    <div style={{ color: state.color }}>      當前字型顏色為: {state.color}      <button onClick={() => dispatch && dispatch({type: UPDATE_COLOR, color: 'red'})}>紅色</button>      <button onClick={() => dispatch && dispatch({type: UPDATE_COLOR, color: 'green'})}>綠色</button>    </div>  )}const Example = ({ initialColor = '#000000' }) => {  const [state, dispatch] = useReducer(reducer, { color: initialColor })  return (    <ColorContext.Provider value={{state, dispatch}}>      <div>        <Show />        <button onClick={() => dispatch && dispatch({type: UPDATE_COLOR, color: 'blue'})}>藍色</button>        <button onClick={() => dispatch && dispatch({type: UPDATE_COLOR, color: 'lightblue'})}>輕綠色</button>      </div>    </ColorContext.Provider>  )}export default Example

以上此方案是值得好好思索的,特別是因為 TypeScript 而導致的型別約束! 當然,如果有更好的解決方案,希望有大佬提出來,我也可以多學習學習~

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Spring Boot Idea中熱部署(自動重新整理)