[譯]如何編寫好的React

原文: How to write great React
How to write great React

寫這篇文章源於一個問題:如果我可以提出一條建議,來幫助新開發人員編寫好的React,那會是什麼?

我的答案是:用編寫整潔函數(clean functions)的規範來編寫整潔的組件(clean components)。

爲什麼要專注於編寫組件(Why focus on writing components)?

我們的目標是編寫易於閱讀,易於維護和易於擴展的React應用程序。

這涉及很多因素:體系架構(architecture),狀態管理(state management),文件結構(file structure),代碼格式(code formatting)等。

但是,我們應用程序的大部分(我們團隊使用的大部分代碼)都會是組件

如果你的組件都是乾淨簡潔的,那麼你的團隊就可以更快地向前推進。

這樣就可以保證得到一個很好的應用嗎? 並不能,因爲應用體系架構的其餘部分可能是一團糟。

但是用好的組件(乾淨簡潔的組件)更不容易搭建糟糕的架構。

那麼我們該如何編寫好的組件? 第一步:始終將它們視爲函數(always treat them as functions)

組件作爲函數(Components as functions)

一些React組件是函數:

const Button = ({ text, onClick }) => (
  <button onClick={onClick}>{text}</button>
)

其他組件不是函數,而是帶有render方法的類:

class Button extends Component {
  render() {
    const { text, onClick } = this.props;
    return <button onClick={onClick}>{text}</button>;
  }
}

即使在第一種場景(函數組件)中,也很容易讓人忽視組件作爲函數這一點(it’s easy to stop thinking of components as functions)。我們開始將組件概念化爲自己的實體(its own entity),接受不同於函數規則的約束。

對於類組件,人們甚至更容易忘記該組件的核心部分是render方法:一個返回UI片段(a segment of the UI)的函數。

當我們忘記將組件視爲函數時,我們會創建龐大且難以推理(hard to reason about)的組件。這些組件做了過多的事情,接收過多的props,或者具有過多的條件,很難使用或是對其進行改進。這類組件總是會讓人感到頭疼。

始終將組件視爲函數(無論它們是基於函數的還是基於類的)是編寫好的React的第一步。

這就是爲什麼。

編寫好的函數(Writing great functions)

讓我們先放下React,問一個問題:什麼纔是好的函數?

Robert Martin 經典的《Clean Code》強調了五個因素:

  1. 小(Small)
  2. 只做一件事(Does one thing)
  3. 一個抽象層級(One level of abstraction)
  4. 少於三個參數(Less than three arguments)
  5. 描述性名稱(Descriptive name)

我們依次討論上述每一個規則,以及它們對我們的React組件意味着什麼。

組件應該足夠小(Your component should be small)

The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that. — Clean Code
函數的第一規則是要短小。第二條規則是還要更短小。— Clean Code

小的函數更容易閱讀。 沒有人願意使用一個500行代碼的函數。 羅伯特·馬丁(Robert Martin)認爲函數基本不應該超過20行。

對於React組件,規則有一些不同,因爲即使是一個簡單的element,JSX也會佔用更多行數。

對於你的組件主體(對於類組件,即render方法)50行代碼是一個好的規則。

50行是您的組件主體的良好經驗法則(對於類組件,即render方法)。 如果查看文件的總行數比較容易,那麼絕大多數組件文件都不應超過250行。 低於100行是最理想的。

保證你的組件足夠小。

Your component should be small

組件應該只做一件事(Your component should do one thing)

關於這個主題,我在我的文章Tiny Components: What could go wrong?中講了很多。

簡而言之,組件應當只做一件事情:基於一個理由而改變(one reason to change)

如果你決定切換菜單項的順序而需要更改MenuList.jsx,那是一個好的行爲。 但是,如果當調整了邊欄的打開方式,卻還需要更改MenuList.jsx,那就不好的行爲方式。

將你的UI拆分成多個只處理一件事的小代碼塊(tiny chunks)。

Your component should do one thing

組件應當只有一個抽象層級(Your component should have one level of abstraction)

這是一個具有多個抽象層級(僞代碼)的函數:

const loadThings = async () => {
    setIsLoading(true);
    const response = await fetchThings();
    setIsLoading(false);
    const { error, data } = response;
    if (error) {
        if (error.status === 404) {
            redirectTo('/404');
        } else if (error.status === 500) {
            redirectTo('/error');
        }
    } else {
        const thingsToUpdate = data.ids.reduce((map, id) => {
            map[id] = data.things[id];
            return map;
        }, {});
        updateThingsInState(thingsToUpdate);
    }
};

我們注意到,loadTings函數中,一部分功能被抽象爲其他函數,包括設置加載狀態以及從服務器獲取響應。而另一些功能則沒有被抽象出來,包括錯誤時重定向和更新狀態。

Note that some things are abstracted away to other functions: setting the loading state and fetching the response from the server. Others are not: redirecting on error, and updating the things in state.

這裏有一種更整潔的方法:

const handleResponse = (response) => {
    const { error, data } = response;
    if (error) {
        handleError(error);
    } else {
        updateThingsInState(data);
    }
};
const loadThings = async () => {
    setIsLoading(true);
    const response = await fetchThings();
    setIsLoading(false);
    handleResponse(response);
};

現在的loadThings函數通過逐行調用其他函數來處理與加載數據有關的任務,很容易閱讀。 我們的新函數handleResponse同樣很簡單,只包含一個條件(containing a single condition)。這樣整個函數就只有一個抽象層級了。

這是一個混合抽象(mixed-abstraction)的React組件:

const Dashboard = () => {
    return (
        <div className="Dashboard">
            <header>
                <h1>Too Little Abstraction Corp.</h1>
                <nav>
                    <a href="/about">About</a>
                    <a href="/mission">Mission</a>
                    <a href="/faq">FAQ</a>
                    <a href="/contact">Contact</a>
                </nav>
            </header>
            <ProductDescription />
            <EmailSubscriptionForm />
            <footer>
              <h2>Thanks for visiting!</h2>
            </footer>
        </div>
    )
}

一些標記(markup)被抽象爲子組件(<ProductDescription />, <EmailSubscriptionForm />),但headerfooter卻不沒有。

這也是一個非常簡單的例子:在沒有規範的情況下,你會遇到將數十行(或數百行)HTML標籤與React子組件混合在一起的組件。

Dashboard組件做了太多事情,有太多的理由來更改這個文件,而且由於缺乏抽象,代碼變得很難閱讀。

解決方案:

const Dashboard = () => {
    return (
        <div className="Dashboard">
            <Header />
            <ProductDescription />
            <EmailSubscriptionForm />
            <Footer />
        </div>
    )
}

這樣就非常容易閱讀了。除非需要再Dashboard組件中在添加子組件,否則你幾乎再也不需要去修改這個文件。

每個子組件也可以根據需要共享和修改。當你修改<Header />時,也沒有破壞<Footer />的風險。

混合抽象(Mixed abstraction)是一個容易陷入的陷阱,因爲在當下它是有意義的(“我只是添加一點標記,它不需要被抽象爲自己的組件!”)。但是隨着時間的流逝,它會導致難以解析的複雜組件。

如果你嘗試將組件大致保持在同一抽象層級上(除了一些不重要的例外,例如包裝div,這是可以接受的),那這些組件將更加易於維護。

將組件限制爲同一抽象層級。

Your component should have one level of abstraction

組件的參數(props)要儘量少(Your component should have only a few arguments (props))

The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification — and then shouldn’t be used anyway… — Clean Code
最理想的參數數量是零(零參數函數),其次是一(單參數函數),再次是二(雙參數函數),應儘量避免三(三參數函數)。有足夠特殊的理由才能用三個以上參數(多參數函數)——所以無論如何也不要這麼做。— Clean Code

是的,嚴格來說,React組件僅接收兩個參數,即props和context。 但是props本質上是函數的參數,也應按上面的原則同樣處理。

實際上,編寫只帶有一個或兩個props的組件確實非常困難,特別是一些組件使用props是爲了將props傳遞給子組件。

對於組件有一個更加寬鬆的規範。三個props會很好(fine),五個props是有代碼異味的(code smell,指代碼中可能導致深層次問題的症狀,譯者注),超過七個props會導致嚴重的危機(crisis).

恰當的構成(Proper composition)可以幫你避免通過多個組件傳遞props,儘可能嘗試在組件樹(component tree)中的最低點處對事件進行處理。

附帶說明,boolean類型的props會增加不必要的複雜性。 Filip Danić關於這個主題寫了一篇優秀的文章

將組件的props限制在三個以下。

Your component should have only a few arguments (props)

組件應具有描述性名稱(Your component should have a descriptive name)

這一點似乎是最簡單的,而且應該是!

實際上,你的組件很難命名,說明它做了太多事情。 回答**“此組件的作用是什麼?” 應該很簡單**,並用這個答案作爲描述性名稱。

如果開發人員在瀏覽你的app的組件樹(component tree),那麼他應該對每個組件的功能都有一個完整而清晰的瞭解。這一點沒什麼好驚喜的。

這是一個更好的規則:捫心自問,“如果我告訴用戶這個組件的名稱,她能在UI中找到和/或猜測出組件的功能嗎?”

組件不應該有技術的、抽象的名稱<TodoListItem>?很容易理解。<PortfolioLoader>?更抽象,但仍然直觀。<UserViewModelInterface>?呃…

保證組件名稱的具體和描述性。

Your component should have a descriptive name

最後的想法(Final thoughts)

你在編寫組件時是否遵循了這些規範? 爲什麼遵循或者爲什麼沒有遵循? 你還遵循了哪些其他規則?

如果你有任何想法,問題或建議,請在評論中告訴我。

謝謝閱讀。

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