React 條件渲染的最佳實踐

前言

React條件渲染寫得好不好,代碼的易讀性真的差非常多,React 很靈活,但是相對的,也很容易寫出很糟糕的代碼,天花板很高,但是樓地板也很低,尤其是維護一些 legacy code,有時都看到想捶桌子。Vue因為用了模板,某種程度也墊高了樓地板的高度。

falsy value

Javascript 中的 falsy value 有六種:

  • false
  • undefined
  • null
  • ''
  • 0
  • NaN

React 中的 falsy value

falsy value 在 React 中的行為,廢話不多說,直接上代碼。

falsy value

可以看到,除了0NaN,其他 4 種都不會被渲染出來,這很重要,尤其是0,很常遇到,而且會衍生另外一個問題.

0 的問題

Javascript 的邏輯運算符(Logical Operators),共有三種:

  • &&
  • ||
  • !

PS:假有只有一個&或是|,他就變成位元邏輯運算符(Bitwise operators),一般後端有時會拿來設計權限,或是多個狀態,很方便,運算很快,但是相對不好懂。

在寫條件渲染的時候,很常拿&&來使用,使用方法如下圖。

  • &&左邊是truthy的時候,顯示右邊
  • &&左邊是falsy的時候,顯示左邊
const isLogin = true
console.log(isLogin && '已登入') // 已登入

const isEditing = false
console.log(isEditing && '編輯中') // false

但是有個場景,判斷某個 Array 的長度,然後顯示 List。

因為 lengh 為0(falsy)時,他會顯示&&的左邊0,而剛好 React 會把0顯示出來,通常這結果不是我們要的。

export function App(props) {
    const arr = []
    function List() {
        return arr.map((item, index) => <div key={index}>{item}</div>)
    }

    return <div className="App">{arr.length && <List />}</div> //畫面顯示0
}

0 問題的解法

問題解法有三種:

  • 條件三元運算(ternary)的寫法
  • !!兩次取反,所得結果會把原本的truthy/falsy轉成true/false
  • 直接就寫 arr.length > 0

條件三元運算

arr.length0(falsy)的時候,回傳nullnull在 React 中不會顯示出來

export function App(props) {
    const arr = []
    function List() {
        return arr.map((item, index) => <div key={index}>{item}</div>)
    }

    return <div className="App">{arr.length ? <List /> : null}</div> //畫面空白
}

!!兩次取反

!!兩次取反,負負得正,原本falsy就會變成falsefalse不會被 React 顯示出來

export function App(props) {
    const arr = []
    function List() {
        return arr.map((item, index) => <div key={index}>{item}</div>)
    }

    return <div className="App">{!!arr.length && <List />}</div> //畫面空白
}

兩次取反會比較好用,因為在 Javascript 中使用:還蠻多的,而且:挺容易被忽略或是沒看到,兩次取反也算是 javacript 的 trick。

React 條件渲染的正確姿勢

做了一些研究,官方文件也有一篇專門在講 conditional render,但是我覺得LogRocket 這篇最完整

我覺得比較好用的方式如下,沒那麼好用的就不提了,有興趣的自己點進去看文章

  • sub component
  • enum object
  • 精簡版的&&或是三元

sub component

文中示範了如以下的寫法:

const If = (props) => {
    const condition = props.condition || false
    const positive = props.then || null
    const negative = props.else || null

    return condition ? positive : negative
}

const app = () => {
    const view = this.state.mode === 'view'
    const editComponent = <EditComponent handleEdit={this.handleEdit} />
    const saveComponent = <SaveComponent handleChange={this.handleChange} />

    return (
        <div>
            <p>Text: {this.state.text}</p>
            <If condition={view} then={editComponent} else={saveComponent} />
        </div>
    )
}

有提到了另外一個套件:jsx-control-statementsjsx-control-statements的樣子我更喜歡

<If condition={true}>
    <span>IfBlock</span>
</If>

他沒有 else,不過也很好解,就是把 condition 加!取反就好了

比較喜歡這種邏輯判斷的跟要顯示view的分開來,很清楚,他有一些類似 switch case 的之類的判斷,但是我沒有很喜歡,而且可以寫成 enum object 的方式

可以自己封裝 If component 來使用

export default function App() {
    return (
        <div>
            <If condition={false}>
                <div className="App">Hello World</div>
            </If>
        </div>
    )
}

function If({ children, condition }) {
    return condition && children
}

enum object

他文中的案例如下

const Components = Object.freeze({
  view: <EditComponent handleEdit={this.handleEdit} />,
  edit: <SaveComponent
          handleChange={this.handleChange}
          handleSave={this.handleSave}
          text={this.state.inputText}
        />
});

...
const key = this.state.mode;
return (
  <div>
    { Components[key] }
  </div>
);

我寫的話,會更進階封裝如下

const A = ({ text }) => <div>我是A,{text}</div>
const B = ({ text }) => <div>我是B,{text}</div>
export default function App({ type }) {
    const options = {
        A: {
            ContentComponent: A,
            attr: { text: 'hello A' }
        },
        B: {
            ContentComponent: B,
            attr: { text: 'hello B' }
        }
    }
    const { ContentComponent, attr } = options[type]

    return (
        <div>
            <ContentComponent {...attr} />
        </div>
    )
}

這樣寫可以根據不同的type,顯示對應的 component,還可以有對應的 attribute,非常好用,代碼又乾淨。

有一點值得提的就是,假如要在 return 裡面寫成<ContentComponent {...attr}/>的形式,那 ContentComponent 對應的值只能是function或是string

options.A.ContentComponent = <A />,這時<A />已經被轉成 Object 了,會報錯。

精簡版的&&或是三元

為何強調是精簡版,因為假如沒把 jsx 抽出來,寫在三元的()裡面,邏輯一多,完全就是災難,直接看代碼。

export default function App() {
    const isLogin = false
    const isEditing = true

    return (
        <div>
            {isLogin ? <A text="hello A" /> : <B text="hello B" />}
            {isEditing && <B text="hello B" />}
        </div>
    )
}

function A({ text }) {
    return <div>我是A,{text}</div>
}
function B({ text }) {
    return <div>我是B,{text}</div>
}

用邏輯運算符&&或是三元運算符,假如後面是只有接 component,那代碼依舊是非常的乾淨清楚,甚至好的 component命名,會讓人一眼就知道他是什麼東西,就跟好的變數命名一樣,代碼本身就是註解,變數命名本身也像是註解,那是寫clean code非常有用的技巧。

像上面的 component A 跟 B,透過 hoisting,用 function 宣告,把它寫在主要內容的下面,有點類似備註的感覺。

代碼由上往下看,一下子就看到主要內容,了解裡面有什麼,想要看 component A 有什麼內容,往下拉去查看,尤其是這些 component 並沒有被其他 component 共用,就不用在額外抽出到另外一個檔案裡面