<input id="0qass"><u id="0qass"></u></input>
  • <input id="0qass"><u id="0qass"></u></input>
  • <menu id="0qass"><u id="0qass"></u></menu>

    React 組件中的七種代碼壞味道

    React 組件中的七種代碼壞味道

    原文鏈接 https://dev.to/awnton/7-code-smells-in-react-components-5f66 譯文中的我是指代原文作者,譯者的話會以譯者注的形式出現。

    以下是我目前收集到的 React 組件中的7種壞味道

    ?組件使用過多的屬性?組件的屬性之間不兼容?拷貝組件的屬性到組件的狀態?組件的中又定義函數式組件?多個布爾類型的組件狀態?組件中使用過多的 useState?useEffect 中實現太多功能

    組件使用過多的屬性

    如果一個組件有太多的屬性,說明這個組件應該拆分下。較真的程序員會問,那多少個屬性算多呢?嗯。。??辞闆r。如果一個組件有 20 多個屬性,但是這個組件依然滿足“只做一件事情”的原則,那這個 20 個屬性也不算多。(譯者:20個屬性,我覺得基本不可能是只做一件事情)但是如果當你遇到了一個有很多屬性的組件,或者要給這樣的組件再添加一個屬性的時候,從下面幾個點看看。

    這個組件是不是做了多件事情?

    和函數一樣,組件最好只做好一件事,所以還是看看能否把這個組件拆分成多個較小的組件。關于拆分可以參考后面提到的“不相關的組件屬性”和“組件的中又定義函數式組件”的處理方式。

    能不能使用組合

    組合是一個經常被忽略的很好的設計模式,采用組合的方式可以避免在一個組件里面處理所有的邏輯。假設我們有一個處理用來處理用戶提交申請的組件。

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

    雖然這個組件接收的屬性都是它需要的,但是還是有改進的空間,比如把一些功能拆分到子組件里面。

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

    這樣我們就保證了 ApplicationForm 組件處理了更加內聚的職責; 只負責提交或者撤銷這個表單的功能。而那些子組件則負責處理了他們在整個功能中自己的小功能;這樣的模塊劃分也非常適合使用 React 的 Context ,讓父子組件通信,實現數據共享。

    把過多的配置屬性可以組合到一起

    把一些配置屬性放到一個配置對象里面是一個好主意,這樣做可以很容易的切換另外一套配置??聪旅娴睦?/p>

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

    除了 data 屬性其他的屬性都是配置屬性。在種情況下,把這些配置屬性組成一個配置對象,把配置對象給到組件是一個很好的方案。

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

    這樣方法也可以讓我們需要使用不同的配置的時候,很方便的切換不同的配置。

    組件的屬性之間不兼容

    避免把一些不相兼容的屬性放到一個組件里面。

    比如,我們一開始寫了個處理文本輸入的 Input 組件,沒過多久我們可能就還需要一個處理電話號碼的組件,我們的實現可能變成這樣。

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

    這個組件的問題再于,如果屬性 isPhoneNumberInput (是否是電話號碼) 和 autoCapitalize (自動大寫)同時為 true 時是沒有意義的,因為我們從來沒有大寫電話號碼的需要(譯者注:這就是這兩個屬性不兼容的意思)。

    在這個問題上,最好的解決方法就是拆分成兩個更小的組件。如果我們想在兩個組件里面共享一些相同的邏輯,那我們可以自定義一些 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" />
    }
    

    雖然這個例子看起來很生硬 ,但是當發現組件的屬性之間有不兼容的情況,這是一個信號提醒你可以嘗試把這個組件拆分一下。

    拷貝屬性到組件狀態

    拷貝屬性到組件的狀態里面的話就會中斷組件的數據流,我們來看看下面的例子:

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

    text 屬性作為初始值傳給了useState ,這樣一來 Button 組件就忽略了 text 的更新。如果 text 屬性更新, Button 組件渲染的還是一開始的初始值。大多數情況這樣的行為不是期望的,而且這樣的做法也會帶來 bug。

    一個更加常見的例子是,我們希望從屬性繼承一些值進行復雜耗時的計算。比如下面的例子,我們需要調用非常耗時的文本格式化函數 slowlyFormatText 。

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

    把 text 放到 state 確實避免了重新渲染帶來的不必要的對slowlyFormatText的調用,但是一樣還是有 text 本身變化,而導致組件文本不更新的問題。一個更好的方法來解決這個問題就是使用useMemo 這個 hook,來緩存復雜的計算結果。

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

    這個 slowlyFormatText 值會在 text 屬性變化了才會被調用,同樣組件顯示的文本也會隨著 text 的更新而更新。

    在某些情況下我們也確實需要對某些屬性的更新進行忽略,比如一個顏色選擇器組件,我們只需要屬性來設置它的初始的選擇的顏色,同時用戶如果選擇了新的顏色,我們應該使用用戶選擇的顏色(而不是初始值),在這樣的需求下把屬性傳給狀態就非常的適合。但是最好還是通過在屬性名前面加上一些前綴(initialColor/defaultColor)來強調你這么做的意圖。

    擴展閱讀 Redux 作者 Dan Abramov 的博客《編寫有彈性的組件》(https://overreacted.io/zh-hans/writing-resilient-components/), 里面也談到了把屬性賦值給狀態的問題。

    組件的中又定義函數式組件

    不要在組件的里面定義組件了。

    自從函數組件流行起來以后,這種做法已經很少出現了。但是隔三差五我還是會碰到,先舉個例子給你們看看。

    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 內聯到 return 中去,但是更多的情況下應該把那些 JSX 提取到一個單獨的組件里面去。

    注意!當你創建一個新的組件你沒有必要一定要新建一個文件;如果組件之間有很強的耦合,把它們都放在一個文件里面也無可厚非。

    多個布爾類型的組件狀態

    避免在組件中使用過多的 boolean 類型的狀態。

    在開發一個組件的時,需要擴展這個組件的時候很容易就會通過用布爾類型的值來表示組件的不同狀態。

    假設一個這樣的場景,組件中的按鈕點擊后,組件就發起一個 web 網絡請求,那你的代碼大致是這樣的。

    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 設置為 false,同時標記 isFinished 為 true;如果出錯了設置 hasError 為 true。

    這樣方法是好用,但是這樣的方法很難確定當前組件的狀態,而且相對其他方案非常容易引入 bug(上面例子里面,進入錯誤狀態時候,代碼依然是按 loading 狀態再處理,因為 isLoading 為 true)。在上面的例子里面,我們同時設置 isLoading 和 isFinished 為 true,那組件就進入到了一種不可能的狀態(譯者注:這是狀態之間不兼容。)。

    處理這類問題一個更推薦的做法是:使用枚舉來管理狀態。枚舉在其他語言中的定義是那些只能預先賦值成常量的變量,雖然在 javascript 中是沒有枚舉,但我們可以用字符串來代替,而且同樣能獲得枚舉的好處(譯者注:大家有空可以看看 xstate 這個庫 https://github.com/davidkpiano/xstate,在狀態機方面是專業的,也支持 Reac)。

    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?函數,用來更新組件的狀態。這兩個函數都用到了一些狀態的設置函數來完成他們的任務。

    試想下,如果我們有更多對狀態的更新操作,那這樣的做法勢必是越來越難維護,而且長期來看很容易引入 bug。在這樣的情況下使用 useReducer 來管理狀態的就會好很多。

    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 我們把狀態的復雜邏輯封裝起來,移到組件外。把組件的 UI 和組件的邏輯分離讓組件變得易于理解。使用 useState 和 useReducer 在不同的場景下各有利弊。關于 reducer 使用方法,個人最推薦這篇文章由著名 React 布道師、培訓師 Kent C. Dodds 的文章 《The State Reducer Pattern with React Hooks 》 https://kentcdodds.com/blog/the-state-reducer-pattern-with-react-hooks/。

    useEffect 中實現太多功能

    避免在 useEffect 的回調用中實現大量的邏輯;這樣會讓你的代碼會更加容易引入 bug ,并且降低可讀性。

    當 react hooks 特性剛發布的使用,我在使用的時候經常犯的錯誤就是在一個 useEffect 中實現過多的邏輯。舉個例子,下面的組件的一個 useEffect 中做了兩件事情。

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

    雖然例子中的副總用(effect)并不是特別的龐大,但是當只有?unlisted?屬性變化了之后會額外的觸發一次網絡請求,即使 id 沒有發生變化。

    為了發現這種類似錯誤,我這樣來自己檢查?useEffect?!叭绻鹸x 或者 yy 發生變化,那就執行 zz”。應用到這個例子中,“當?id?或者?unlisted?屬性發生變化,那么我們就獲取新的帖子?并且?更新它是否可見”。

    如果一個句子里面同時出現了或者和并且,通常它是有問題的。那把這個 effect 拆成兩個就好很多。

    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])
    }
    

    通過這樣的拆分我們,我們降低了組件的復雜度,讓它更加容易理解,同時降低引入bug的風險。

    總結

    好了,終于講完了。但是一定要記住這7個點只是代碼外在的可疑的表現,并不是一桿子說代碼一定有錯。因為在實際開發的過程中還是會碰到需要這么做的特殊情況。

    如果你覺得我的文章寫的有問題,或者你想提議一個其他的代碼壞味道你,歡迎在文章下面留言,或者在 twitter(@awnton)上聯系我。

    如果你覺得本文對你有幫助歡迎點贊,轉發和贊賞。

    已標記關鍵詞 清除標記
    ??2020 CSDN 皮膚主題: 大白 設計師:CSDN官方博客 返回首頁
    多乐彩