1. 程式人生 > 實用技巧 >React 使用中值得優化的 7 個點

React 使用中值得優化的 7 個點

自從使用React後,見過越來越多可值得優化的點,比如:

  • 大量的props

  • props的不相容性

  • props複製為state

  • 返回JSX的函式

  • state的多個狀態

  • useState過多

  • 複雜的useEffect

大量的 props

如果需要把大量的props傳遞到一個元件中,那麼很有可能 該元件可再進一步拆分。

問題來了,“大量” 具體是多少呢?答案是 看情況。

假設你正在開發 一個包含 20 個或更多props的元件時,你想再新增一些props完善其他功能,這時有兩點可以參考 是否應拆分元件:

該元件是否做了多件事?

像函式一樣,一個元件應該只做好一件事,所以考慮下 將元件拆分成多個小元件是否會更好。

例如,該元件存在props的不相容性或返回JSX的函式。

該元件是否可被合成?

開發中,組合是一種很好的模式但經常被忽視。

如果你的元件中存在將不相干邏輯塞到一起的情況,是時候考慮使用組合了。

假設我們有一個表單元件來處理某組織的使用者資訊:

<ApplicationForm
  user={userData}
  organization={organizationData}
  categories={categoriesData}
  locations={locationsData}
  onSubmit={handleSubmit}
  onCancel={handleCancel}
  ...
/>

通過該元件的props,我們可看到它們都與元件提供的功能密切相關。

該元件看起來並無大礙,但如果將其中的一些props分擔到子元件,那麼資料流就會更清晰。

<ApplicationForm onSubmit={handleSubmit} onCancel={handleCancel}>
  <ApplicationUserForm user={userData} />
  <ApplicationOrganizationForm organization={organizationData} />
  <ApplicationCategoryForm categories={categoriesData} />
  <ApplicationLocationsForm locations={locationsData} />
</ApplicationForm>

現在,我們已經看到該表單元件只處理提交和取消動作,其他範圍內的事情,都交給了對應的子元件。

是否傳遞了很多有關配置的props

在某些情況下,將多個有關配置的props組合成一個options是個不錯的實踐。

假設我們有一個可顯示某種表格的元件:

<Grid
  data={gridData}
  pagination={false}
  autoSize={true}
  enableSort={true}
  sortOrder="desc"
  disableSelection={true}
  infiniteScroll={true}
  ...
/>

我們可以很清楚地看出,該元件除了data外其餘的props都是與配置有關的。

如果將多個配置props合成為一個options,就可更好地控制組件的選項,規範性也得到提升。

const options = {
  pagination: false,
  autoSize: true,
  enableSort: true,
  sortOrder: 'desc',
  disableSelection: true,
  infiniteScroll: true,
  ...
}
<Grid
  data={gridData}
  options={options}
/>

props 的不相容性

避免元件之間傳遞不相容的props

假設你的元件庫中有一個<Input />元件,而該元件開始時僅用於處理文字,但過了一段時間後,你將它用於電話號碼處理。

你的實現可能如下:

function Input({ value, isPhoneNumberInput, autoCapitalize }) {
  if (autoCapitalize) capitalize(value)
  return <input value={value} type={isPhoneNumberInput ? 'tel' : 'text'} />
}

問題在於,isPhoneNumberInputautoCapitalize之間並不存在關聯,將一個手機號首字母大寫是沒有任何意義的。

在這種情況下,我們可以將其分割成多個小元件,來明確具體的職責,如果有共享邏輯,可以將其放到hooks中。

function TextInput({ value, autoCapitalize }) {
  if (autoCapitalize) capitalize(value)
  useSharedInputLogic()
  return <input value={value} type="text" />
}
function PhoneNumberInput({ value }) {
  useSharedInputLogic()
  return <input value={value} type="tel" />
}

雖然上面例子有點勉強,可當發現元件的props存在不相容性時,是時候考慮拆分元件了。

props 複製為 state

如何更好地將props作為state的初始值。

有如下元件:

function Button({ text }) {
  const [buttonText] = useState(text)
  return <button>{buttonText}</button>
}

該元件將text作為useState的初始值,可能會導致意想不到的行為。

實際上該元件已經關掉了props的更新通知,如果text在上層被更新,它將仍呈現 接受到text的第一次值,這更容易使元件出錯。

一個更實際場景是,我們想基於props通過大量計算來得到新的state

在下面的例子中,slowlyFormatText函式用於格式化text,注意 需要很長時間才能完成。

function Button({ text }) {
  const [formattedText] = useState(() => slowlyFormatText(text))
  return <button>{formattedText}</button>
}

解決此問題 最好的方案是 使用useMemo代替useState

function Button({ text }) {
  const formattedText = useMemo(() => slowlyFormatText(text), [text])
  return <button>{formattedText}</button>
}

現在slowFormatFormat僅在text更改時執行,並且沒有阻斷 上層元件更新。

進一步閱讀:Writing resilient components by Dan Abramov。

返回 JSX 的函式

不要從元件內部的函式中返回JSX

這種模式雖然很少出現,但我還是時不時碰到。

僅舉一個例子來說明:

function Component() {
  const topSection = () => {
    return (
      <header>
        <h1>Component header</h1>
      </header>
    )
  }
  const middleSection = () => {
    return (
      <main>
        <p>Some text</p>
      </main>
    )
  }
  const bottomSection = () => {
    return (
      <footer>
        <p>Some footer text</p>
      </footer>
    )
  }
  return (
    <div>
      {topSection()}
      {middleSection()}
      {bottomSection()}
    </div>
  )
}

該例子雖然看起來沒什麼問題,但其實這會破壞程式碼的整體性,使維護變得困難。

要麼把函式返回的JSX直接內聯到元件內,要麼將其拆分成一個元件。

有一點需要注意,如果你建立了一個新元件,不必將其移動到新檔案中的。

如果多個元件緊密耦合,將它們儲存在同一個檔案中是有意義的。

state 的多個狀態

避免使用多個布林值來表示元件狀態。

當編寫一個元件並多次迭代後,很容易出現這樣一種情況,即內部有多個布林值來表示 該元件處於哪種狀態。

比如下面的例子:

function Component() {
  const [isLoading, setIsLoading] = useState(false)
  const [isFinished, setIsFinished] = useState(false)
  const [hasError, setHasError] = useState(false)
  const fetchSomething = () => {
    setIsLoading(true)
    fetch(url)
      .then(() => {
        setIsLoading(false)
        setIsFinished(true)
      })
      .catch(() => {
        setHasError(true)
      })
  }
  if (isLoading) return <Loader />
  if (hasError) return <Error />
  if (isFinished) return <Success />
  return <button onClick={fetchSomething} />
}

當按鈕被點選時,我們將isLoading設定為true,並通過fetch執行網路請求。

如果請求成功,我們將isLoading設定為falseisFinished設定為true,如果有錯誤,將hasError設定為true

雖然這在技術上是可行的,但很難推斷出元件處於什麼狀態,而且不容易維護。

並且有可能最終處於“不可能的狀態”,比如我們不小心同時將isLoadingisFinished設定為true

解決此問題一勞永逸的方案是 使用列舉來管理狀態。

在其他語言中,列舉是一種定義變數的方式,該變數只允許設定為預定義的常量值集合,雖然在JavaScript中不存在列舉,但我們可以使用字串作為列舉:

function Component() {
  const [state, setState] = useState('idle')
  const fetchSomething = () => {
    setState('loading')
    fetch(url)
      .then(() => {
        setState('finished')
      })
      .catch(() => {
        setState('error')
      })
  }
  if (state === 'loading') return <Loader />
  if (state === 'error') return <Error />
  if (state === 'finished') return <Success />
  return <button onClick={fetchSomething} />
}

通過這種方式,完全杜絕了出現 不可能狀態的情況,並更利用擴充套件。

如果你使用TypeScript開發的話,則可以從定義時就實現列舉:

const [state, setState] = useState<'idle' | 'loading' | 'error' | 'finished'>('idle')

useState 過多

避免在同一個元件中使用太多的useState

一個包含許多useState的元件可能會做多件事情,可以考慮是否要拆分它。

當然也存在一些複雜的場景,我們需要在元件中管理一些複雜的狀態。

下面是自動輸入元件的例子:

function AutocompleteInput() {
  const [isOpen, setIsOpen] = useState(false)
  const [inputValue, setInputValue] = useState('')
  const [items, setItems] = useState([])
  const [selectedItem, setSelectedItem] = useState(null)
  const [activeIndex, setActiveIndex] = useState(-1)
  const reset = () => {
    setIsOpen(false)
    setInputValue('')
    setItems([])
    setSelectedItem(null)
    setActiveIndex(-1)
  }
  const selectItem = (item) => {
    setIsOpen(false)
    setInputValue(item.name)
    setSelectedItem(item)
  }
  ...
}

我們有一個reset函式,可以重置所有狀態,還有一個selectItem函式,可更新一些狀態。

這些函式都離不開useState定義的狀態。如果功能繼續迭代,那麼函式就會越來越多,狀態也會隨之增加,資料流就會變得模糊不清。

在這種情況下,使用useReducer來代替 過多的useState是一個不錯的選擇。

const initialState = {
  isOpen: false,
  inputValue: "",
  items: [],
  selectedItem: null,
  activeIndex: -1
}
function reducer(state, action) {
  switch (action.type) {
    case "reset":
      return {
        ...initialState
      }
    case "selectItem":
      return {
        ...state,
        isOpen: false,
        inputValue: action.payload.name,
        selectedItem: action.payload
      }
    default:
      throw Error()
  }
}
function AutocompleteInput() {
  const [state, dispatch] = useReducer(reducer, initialState)
  const reset = () => {
    dispatch({ type: 'reset' })
  }
  const selectItem = (item) => {
    dispatch({ type: 'selectItem', payload: item })
  }
  ...
}

通過使用reducer,我們封裝了管理狀態的邏輯,並將複雜的邏輯移出了元件,這使得元件更容易維護。

進一步閱讀:state reducer pattern by Kent C. Dodds。

複雜的 useEffect

避免在useEffect中做太多事情,它們使程式碼易於出錯,並且難以推理。

下面的例子中 犯了一個很大的錯誤:

function Post({ id, unlisted }) {
  ...
  useEffect(() => {
    fetch(`/posts/${id}`).then(/* do something */)
    setVisibility(unlisted)
  }, [id, unlisted])
  ...
}

unlisted改變時,即使id沒有變,也會呼叫fetch

正確的寫法應該是 將多個依賴分離:

function Post({ id, unlisted }) {
  ...
  useEffect(() => { // when id changes fetch the post
    fetch(`/posts/${id}`).then(/* ... */)
  }, [id])
  useEffect(() => { // when unlisted changes update visibility
    setVisibility(unlisted)
  }, [unlisted])
  ...
}