React hooks實踐

陳紀庚發表於2019-01-27

前言

最近要對舊的專案進行重構,統一使用全新的react技術棧。同時,我們也決定嘗試使用React hooks來進行開發,但是,由於React hooks崇尚的是使用(也只能使用)function component的形式來進行開發,而不是class component,因此,整個開發方式也會與之前產生比較大的差異。所以,我這裡就積累了下實際專案中遇到的問題以及思考,看下能不能幫助大家少走彎路。

正文

接下來就直接進入正文。我會將專案中遇到的問題一一列舉出來,並且給出解決方案。

執行初始化操作的時機

當我轉到React hooks的時候,首先就遇到了這個問題:

一般來說,業務元件經常會遇到要通過發起ajax請求來獲取業務資料並且執行初始化操作的場景。在使用class component程式設計的時候,我們就可以在class component提供的生命週期鉤子函式(比如componentDidMount, constructor等)執行這個操作。可是如果轉到React hooks之後,function component裡是沒有這個生命週期鉤子函式的,那這個初始化操作怎麼辦呢?總不能每次遇到這種場景都使用class component來做吧?

解決方案:使用useEffect(想知道useEffect是什麼的話,可以點選這裡)

useEffect,顧名思義,就是執行有副作用的操作,你可以把它當成componentDidMount, componentDidUpdate, and componentWillUnmount 的集合。它的函式宣告如下

useEffect(effect: React.EffectCallback, inputs?: ReadonlyArray<any> | undefined)
複製程式碼

那麼,我們在實際使用中,我們就可以使用這個來執行初始化操作。舉個例子

import React, { useEffect } from 'react'

export function BusinessComponent() {
  const initData = async () => {
    // 發起請求並執行初始化操作
  }
  // 執行初始化操作,需要注意的是,如果你只是想在渲染的時候初始化一次資料,那麼第二個引數必須傳空陣列。
  useEffect(() => {
    initData();
  }, []);

  return (<div></div>);
}
複製程式碼

需要注意的是,這裡的useEffect的第二個引數必須傳空陣列,這樣它就等價於只在componentDidMount的時候執行。如果不傳第二個引數的話,它就等價於componentDidMount和componentDidUpdate

做一些清理操作

由於我們在實際開發過程中,經常會遇到需要做一些副作用的場景,比如輪詢操作(定時器、輪詢請求等)、使用瀏覽器原生的事件監聽機制而不用react的事件機制(這種情況下,元件銷燬的時候,需要使用者主動去取消事件監聽)等。使用class Component程式設計的時候,我們一般都在componentWillUnmount或者componentDidUnmount的時候去做清理操作,可是使用react hooks的時候,我們如何做處理呢?

解決方案:使用useEffect第一個引數的返回值

如果useEffect的第一個引數返回了函式的時候,react會在每一次執行新的effects之前,執行這個函式來做一些清理操作。因此,我們就可以使用它來執行一些清理操作。

例子:比如我們要做一個二維碼元件,我們需要根據傳入的userId不斷輪詢地向後臺發請求查詢掃描二維碼的狀態,這種情況下,我們就需要在元件unmount的時候清理掉輪詢操作。程式碼如下:

import React, { useEffect } from 'react'

export function QRCode(url, userId) {
  // 根據userId查詢掃描狀態
  const pollingQueryingStatus = async () => {
  }
  // 取消輪詢
  const stopPollingQueryStatus = async() => {
  }

  useEffect(() => {
    pollingQueryingStatus();

    return stopPollingQueryStatus;
  }, []);

  // 根據url生成二維碼
  return (<div></div>)
}
複製程式碼

這樣的話,就等價於在componentWillUnmount的時候去執行清理操作。

但是,有時候我們可能需要執行多次清理操作。還是舉上面的例子,我們需要在使用者傳入新的userId的時候,去執行新的查詢的操作,同時我們還需要清除掉舊的輪詢操作。想一下怎麼做比較好。

其實對這種情況,官方也已經給出瞭解決方案了,useEffect的第二個引數是觸發effects的關鍵,如果使用者傳入了第二個引數,那麼只有在第二個引數的值發生變化(以及首次渲染)的時候,才會觸發effects。因此,我們只需要將上面的程式碼改一下:

import React, { useEffect } from 'react'

export function QRCode(url, userId) {
  // 根據userId查詢掃描狀態
  const pollingQueryingStatus = async () => {
  }

  const stopPollingQueryStatus = async() => {
  }
  // 我們只是將useEffect的第二個引數加了個userId
  useEffect(() => {
    pollingQueryingStatus();

    return stopPollingQueryStatus;
  }, [userId]);

  // 根據url生成二維碼
  return (<div></div>)
}
複製程式碼

我們只是在useEffect的第二個引數陣列裡,加入了一個userId。這樣的話,userId的每一次變化都會先觸發stopPollingQueryStatus,之後再執行effects,這樣就可以達到我們的目的。

useState與setState的差異

react hooks使用useState來代替class Component裡的state。可是,在具體開發過程中,我也發現了一些不同點。useState介紹可以點選這裡

在setState的時候,我們可以只修改state中的區域性變數,而不需要將整個修改後的state傳進去,舉個例子

import React, { PureComponent } from 'react';

export class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      name: 'cjg',
      age: 18,
    }
  }

  handleClick = () => {
    const { count } = this.state;
    // 我們只需要傳入修改的區域性變數
    this.setState({
      count: count + 1,
    });
  }

  render() {
    return (
      <button onClick={this.handleClick}></button>
    )
  }
}
複製程式碼

而使用useState後,我們修改state必須將整個修改後的state傳入去,因為它會直接覆蓋之前的state,而不是合併之前state物件。

import React, { useState } from 'react';

export function Count() {
  const [data, setData] = useState({
    count: 0,
    name: 'cjg',
    age: 18,
  });
    
  const handleClick = () => {
    const { count } = data;
    // 這裡必須將完整的state物件傳進去
    setData({
      ...data,
      count: count + 1,
    })
  };

  return (<button onClick={handleClick}></button>)
}
複製程式碼

減少不必要的渲染

在使用class Component進行開發的時候,我們可以使用shouldComponentUpdate來減少不必要的渲染,那麼在使用react hooks後,我們如何實現這樣的功能呢?

解決方案:React.memouseMemo

對於這種情況,react當然也給出了官方的解決方案,就是使用React.memo和useMemo。

React.memo

React.momo其實並不是一個hook,它其實等價於PureComponent,但是它只會對比props。使用方式如下(用上面的例子):

import React, { useState } from 'react';

export const Count = React.memo((props) => {
  const [data, setData] = useState({
    count: 0,
    name: 'cjg',
    age: 18,
  });
  
  const handleClick = () => {
    const { count } = data;
    setData({
      ...data,
      count: count + 1,
    })
  };

  return (<button onClick={handleClick}>count:{data.count}</button>)
});

複製程式碼

useMemo

useMemo它的用法其實跟useEffects有點像,我們直接看官方給的例子

function Parent({ a, b }) {
  // Only re-rendered if `a` changes:
  const child1 = useMemo(() => <Child1 a={a} />, [a]);
  // Only re-rendered if `b` changes:
  const child2 = useMemo(() => <Child2 b={b} />, [b]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}
複製程式碼

從例子可以看出來,它的第二個引數和useEffect的第二個引數是一樣的,只有在第二個引數陣列的值發生變化時,才會觸發子元件的更新。

總結

一開始在從class component轉變到react hooks的時候,確實很不適應。可是當我習慣了這種寫法後,我的心情如下:

React hooks實踐

當然,現在react hooks還是在alpha階段,如果大家覺得不放心的話,可以再等等。反正我就先下手玩玩了哈哈哈。

本文地址在->本人部落格地址, 歡迎給個 start 或 follow

相關文章