React Hooks 用法

剛學了一下 React Hooks 的用法,就寫篇博客記錄一下。因爲學得也比較淺,所以這篇博客只講怎麼用。

useState

普通用法

就是用來管理組件的內部狀態。

const UseState: React.FC = () => {
  const [x, setX] = useState(0)
  const [y, setY] = useState(100)

  const onClickX = () => setX(x + 1)
  const onClickY = () => setY((prevX) => prevX + 1)

  return (
    <div>
      <h1>useState</h1>
      <button onClick={onClickX}>x + 1</button>
      <p>x: {x}</p>

      <button onClick={onClickY}>y + 1</button>
      <p>y: {y}</p>
    </div>
  )
}

這裏注意,x 是直接通過 setX 去改變的,而 y 是傳入一個函數(操作)去改變。傳入函數去改變的一個好處就是可以連續多次改變 y 值,而 setX 只會執行最後一次的 setX。

改變對象

對於改變對象一定不能只改變其中的屬性,而是整個對象都要改。

const [obj, setObj] = useState({name: 'Jack', age: 18})

// 錯誤
obj.age = 19
setObj(obj)

// 正確
setObj({
  ...obj,
  age: 19
})

複雜計算

如果初始值需要複雜計算,則可以在 useState 直接傳入一個 factory 函數,而不是默認值。

const [onlyRunAtFirstTime, change] = useState(() => {
  console.log('只在第一次渲染時執行,這裏可以假設 9 + 9 是一些複雜的操作')

  return {name: 'Jack', age: 9 + 9}
})

useReducer

useReducer 可以看成複雜版的 useState,或者理解成 Redux 裏的 reducer,再或者理解成 Vuex。返回值依然是一個數組,第一項是讀,第二項是寫。

const initState: TState = {
  name: 'Jack',
  age: 18
}

const reducer = (state: TState, action: TAction) => {
  const {type} = action

  switch (type) {
    case 'addAge':
      return {...state, age: state.age + 1}
    case 'minusAge':
      return {...state, age: state.age - 1}
    case 'update':
      return {...state, ...action.user}
    default:
      throw new Error('沒有傳入 type')
  }
}

const LearnUseReducer: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, initState)

  const onAdd = () => dispatch({type: 'addAge'})
  const onMinus = () => dispatch({type: 'minusAge'})
  const onUpdate = () => dispatch({type: 'update', user: {name: 'Tom', age: 20}})

  return (
    <div>
      <h1>useReducer</h1>
      <button onClick={onAdd}>age + 1</button>
      <button onClick={onMinus}>age - 1</button>
      <button onClick={onUpdate}>更新整個user</button>
      <p>{JSON.stringify(state)}</p>
    </div>
  )
}

這裏要注意的是,reducer 是第一個參數,initState 是第二個參數,非常奇怪的傳參邏輯。

useContext

普通用法

這個 Hook 的意義就是造一些局部變量,在這個局部下的組件都能訪問到這些變量。

一般可以配合上面的 useReducer 去代替 Redux,或者在 styled-components 裏可直接使用主題色。

const initTheme = {
  success: 'green'
}

const Context = createContext<TTheme | null>(null)

const LearnUseContext: React.FC = () => {
  const [theme] = useState(initTheme)

  return (
    <Context.Provider value={theme}>
      <div>
        爸爸得到的值: {theme.success}
        <Child/>
      </div>
    </Context.Provider>
  )
}

const Child: React.FC = () => {
  const theme = useContext(Context)

  return (
    <div>
      兒子得到的值: {theme!.success}
      <GrandChild/>
    </div>
  )
}

const GrandChild: React.FC = () => {
  const theme = useContext(Context)

  return (
    <div>孫子得到的值: {theme?.success}</div>
  )
}

上面的例子可以看到,在外面定義好的主題色,通過 Context.Provider 傳遞到下面的所有組件。當然也可以再傳個 setTheme 函數,讓兒子、孫子組件去修改 theme。

代替 Redux

那怎麼去代替 Redux 呢?很簡單,把上面的講的 reducerstate 提到 App,然後,通過 Context.Provider 傳到下面所有組件就好了。

const initState = {/*初始狀態*/}
const reducer = () => {/*你的 reducer*/}

const App: React.FC = () => {
  const [store, dispatch] = useReducer(reducer, initState)

  return (
    <Context.Provider value={{store, dispatch}}>
      <div>各種組件</div>
    </Context.Provider>
  )
}

useEffect

useEffect 一般是去代替 componentDidMount, componentDidUpdate 和 componentWillUnmount。簡單來講,就是活了,變了,死了的時候要幹啥。

下面先給個例子。

const LearnUseEffect: React.FC = () => {
  const [movies, setMovies] = useState<any[]>([])

  useEffect(() => { // 對應 componentDidMount
    console.log('剛開始就執行,要在這裏去獲取 movies')

    ajax('movies', (newMovies: any[]) => setMovies(newMovies))
  }, [])
  useEffect(() => { // 對應 componentDidUpdate
    console.log('只要有東西更新了,我就執行')
  })
  useEffect(() => { // 對應 componentDidUpdate,但是隻有 movies 更新才執行
    console.log('movies 更新了,所以我執行了')
  }, [movies])

  return (
    <div>
      <h1>useEffect</h1>
      {
        movies.length === 0 ?
          <div>加載中</div> :
          <ul>
            {movies.map(m => <li key={m.id}>{m.name}</li>)}
          </ul>
      }
    </div>
  )
}

執行後可以看到

爲什麼會有兩次的 “只要有東西...” 和 “movies 更新了..” 呢?是因爲第一次是使用了 useStatemoviesnull 變成了 [],第二次纔是發 ajax 去請求數據。

關於替換 componentWillUnmount,只需要返回一個函數就可以了。

useEffect(() => { // 對應 componentDidMount
  console.log('剛開始就執行,要在這裏去獲取 movies')

  ajax('movies', (newMovies: any[]) => setMovies(newMovies))
  
  return () => {
    console.log('這個組件就要死翹翹了')
  }
}, [])

注意,這裏可以同時使用多個 useEffect,執行順序就是按代碼的順序。

useLayoutEffect

useEffectuseLayoutEffect 是差不多的東西,區別是執行時機不一樣。這個 Hook 是在渲染之前,先去執行一些東西,然後再去渲染。而 useEffect 就是在渲染之後,纔去執行函數。也就是說:

  1. App() 執行
  2. 生成虛擬 DOM
  3. 變成真實 DOM
  4. 執行 useLayoutEffect 回調
  5. 渲染 render
  6. 執行 useEffect 回調

這樣的好處就是如果你有下面的代碼:

useEffect(() => {
  document.querySelector('#xxx').textContent = Math.random()
})

你會看到頁面的閃爍,本來是 0 變成了 隨機數。

而如果用 useLayoutEffect 就不會出現上面的情況,因爲還渲染,已經改成了隨機數了。

既然不會閃爍,那是不是都用 useLayoutEffect 好呢?其實不是,因爲如果把所有的操作都放在渲染之前,用戶一打開網頁就會是一片白,等數據來了或者計算結束了,頁面才渲染。這會嚴重影響用戶體驗。

大部分情況下應該是使用 useEffect,在獲取數據或者大量計算時候顯示一個 Loading 菊花就好了。

useMemo

在寫 React 的時候,我們經常會有這種場景,App 下有一個 Child 組件:<App> -> <Child>。當改變了 App 組件的變量時,Child 會再次執行。

const LearnUseMemo: React.FC = () => {
  const [x, setX] = useState(0)

  const onClickX = () => setX(x + 1)

  return (
    <div>
      <h1>useMemo</h1>
      <button onClick={onClickX}>x + 1</button>
      <Child/>
    </div>
  )
}

const Child: React.FC = () => {
  console.log('我是兒子,爲什麼要搞我')

  return <div>兒子</div>
}

像這個例子,點一下 x + 1 按鈕,x 的值變了,但是依然會打出“我是兒子,別搞我”。

這裏就有問題了:改變的是 App 的東西,又不是 Child 的東西,Child 怎麼還要執行呢?useMemo 就是來解決這個問題的。

什麼是 memo

這裏有個解決方法就是用 React.memo,將 Child 組件傳入會得到一個組件,這個新的組件只有在 props 變了纔會再次執行。

const Child2 = React.memo(Child)

然後

return (
    <div>
      <h1>useMemo</h1>
      <button onClick={onClickX}>x + 1</button>
      <Child2/>
    </div>
)

這樣就不會打出“我是兒子,爲什麼要搞我”。但是,這裏還是有問題,假如 Child 的 props 要傳入一個函數,那麼 props 就有可能發生變化:

const LearnUseMemo: React.FC = () => {
  const [x, setX] = useState(0)

  const onClickX = () => setX(x + 1)

  const onChild3Click = () => {}

  return (
    <div>
      <button onClick={onClickX}>x + 1</button>
      <Child3 onChild3Click={onChild3Click}/>
    </div>
  )
}

const Child3 = React.memo((props: any) => {
  console.log('我是兒子3,用了 memo 還能搞到我?')

  return <div onClick={props.onChild3Click}>我是兒子3,用了 memo 還能搞到我?</div>
})

這裏點擊了 x + 1 按鈕,還是會打出 “我是兒子3,用了 memo 還能搞到我”,因爲在執行 LearnUseMemo 的時候, onChild3Click 已經換成另一個函數了(雖然函數體是一樣的,但是函數是對象,對象地址變了),所以 props 會變,props 一變,就會執行 Child3。

最後的 useMemo

爲了解決上面的問題,終於引出我們的 useMemo 了。useMemo 可以緩存一切東西,比如上面的函數:

const onMemoClick = useMemo(() => {
  return () => console.log("點擊了")
}, [])

然後再把這個 onMemoClick 傳給 Child3 就OK了!第二個參數是監聽的依賴,只有依賴變了,纔會更新,和 useEffect 是差不多的。

爲了更簡潔,我們還可以使用 useCallback,就不用像上面那樣瘋狂俄羅斯套娃了。

const onMemoClick = useCallback(() => {
  console.log('點擊了')
}, [])

useRef

普通用法

使用 useSate 的時候,值是每次都會變,那我希望每次更新值不要變
useRef 的使用場景是如果你需要一個值,在組件不斷 render 時保持不變。。useRef 會將值存在一個地方,與當前組件一一對應。例子:

const LearnUseRef: React.FC = () => {
  const [x, setX] = useState(0)
  const y = useRef(0)

  useEffect(() => {
    y.current += 1
  }, [])

  const changeX = () => setX(x + 1)
  const changeY = () => y.current += 1

  return (
    <div>
      <h1>useRef</h1>
      <button onClick={changeX}>x + 1</button>
      <button onClick={changeY}>y + 1</button>
      <div>x 的值 {x}</div>
      <div>y 的值 {y.current}</div>
    </div>
  )
}

但是這裏有個問題,就是在 useEffect 的時候改變了 y 的值,頁面顯示還是0.

這是因爲改變了 y.current 的值後,React 並不會幫你更新頁面。只有,如 setX 被執行導致頁面更新纔會更新 y 的值,所以很蛋疼。不過,如果你非要實現點擊然後加一這個功能,那就不如用 useState 好了。

forwardRef

之前用過 React 和 Vue 的同學應該知道,我們可以用 ref 去代替 document.querySelect 來獲取某個元素。forwardRef 是用來傳遞 ref 這個 props 的。

const LearnUseRef: React.FC = () => {
  const myButton = useRef(null)
  return (
    <div>
      ...
      <Button ref={myButton}/>
    </div>
  )
}

const Button = (props: any) => {
  return <button ref={props.ref}>用作 ref 的按鈕</button>
}

這裏我們想獲取 Button 組件裏的 button 元素,所以我們想從 LearnUseRef 組件傳一個 ref 到 Button,好讓 Button 去引用到 button 元素。但是報錯:

因爲 ref 本來就用作引用某個元素的,但是你是想傳一個叫 "ref" 的 props,這不就衝突了嘛,所以報錯。這裏已經提示我們要用 forwardRef 了,所以應該是這麼用的:

const LearnUseRef: React.FC = () => {
  const myButton = useRef(null)
  return (
    <div>
      ...
      <OKButton ref={myButton}/>
    </div>
  )
}

const Button = (props: any) => {
  return <button ref={props.ref}>用作 ref 的按鈕</button>
}

const OKButton = React.forwardRef(Button)

useImperativeHandle

這個 Hook 其實的意思就是 setRef,怎麼理解呢?回看上面的例子,我們傳了一個 ref 給 OKButton 組件,那萬一 OKButton 組件想修改這個 ref 怎麼辦呢?這就需要 useImperativeHandle 了。

const LearnUseImperativeHandle: React.FC = () => {
  const myButton = useRef(null)

  useEffect(() => {
    console.log('有人改了我的 ref', myButton)
  })

  return (
    <div>
      <OKButton ref={myButton}/>
    </div>
  )
}

const OKButton = React.forwardRef((props: any, ref: any) => {
  useImperativeHandle(ref, () => {
    return {
      x: 'hello'
    }
  })
  return <button>按鈕</button>
})

打開控制檯,可以看到

這裏的 current 就變成了 {x: 'hello'} 了。用處嘛,我也沒想到有什麼用。。。

自定義 Hook

這個其實說到底就是一種封裝的方法。比如,我們有 books 這個資源,RESTful API 有

  1. get /books
  2. post /books?id=xxx
  3. delete /books?id=xxx
  4. put /books?id=xxx

那麼我們代碼可能就有4個函數去發這4個請求:

const getBooks = () => {/* get /books */}
const addBooks = () => {/* post /books?id=xxx */}
const editBooks = () => {/* put /books?id=xxx */}
const deleteBooks = () => {/* delete /books?id=xxx */}

最好我們有個東西可以把上面的東西放在一起,然後在對應的組件調用一下就好了,這其實就是自定義 Hooks。

const useBooks = (initBooks: TBook[]) => {
  const [books, setBooks] = useState(initBooks)

  useEffect(() => {
    getBooks()
  }, [])

  const getBooks = () => {
    setTimeout(() => {
      setBooks([
        {id: '1', name: '一體'},
        {id: '2', name: '二體'},
        {id: '3', name: '三體'},
        {id: '4', name: '裸體'},
      ])
    }, 2000)
  }
  const addBooks = (newBook: TBook) => {
    setBooks([...books, newBook])
  }
  const editBooks = () => {/* put /books?id=xxx */}
  const deleteBooks = () => {/* delete /books?id=xxx */}

  return {
    books,
    getBooks,
    addBooks,
    editBooks,
    deleteBooks
  }
}

const LearnCustomizeHook: React.FC = () => {
  const {books} = useBooks([])

  return (
    <div>
      <h1>useBooks</h1>
      {
        books.length === 0?
          <div>加載中</div> :
          <ul>
            {
              books.map(b => <li key={b.id}>{b.name}</li>)
            }
          </ul>
      }
    </div>
  )
}

可以看到,用了自定義 Hooks 會讓代碼變得清爽很多。

(完)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章