代碼簡潔之道:編寫乾淨的 React Components & JSX

不一樣團隊編寫出來的 React 代碼也不盡相同,水平有個有高低,就像十個讀者就有十種哈姆雷特,可是如下八點多是你編寫 React 代碼的基本準則。

這篇 性能優化小冊 - React 搜索優化:防抖、緩存、LRU 文章提到,最近要作 React 項目的一些重構和優化等相關工做,過了這麼久來總結一下(借鑑網上的一些文章和本身的實踐,提取一些 React 代碼優化上的共性)。html

1、 可選的 props 和空對象 {}

遵循單一目的組件哲學,避免過於複雜的多行組件,並儘量地將組件分解。前端

假設咱們有一個簡單的 <userCard/> 組件,此組件的惟一用途是接收一個 user 對象並顯示相應的用戶數據。react

代碼以下:git

import React from 'react';
import propsTypes from 'props-types';

const UserCard = ({ user }) => {
  return (
    <ul>
      <li>{user.name}</li>
      <li>{user.age}</li>
      <li>{user.email}</li>
    </ul>
  )
}

UserCard.propsTypes = {
  user: propsTypes.object
}
UserCard.defaultTypes = {
  user: {}
}

這個組件須要一個 user props 而且它是組件的惟一數據源,可是這個 props 不是必須的(沒有設置 isRequired),因此設置了一個默認值爲 {},以免 Can not access property‘ name’ of... errors 等錯誤。github

那麼,若是沒有爲 <UserCard> 組件在等待渲染時提供一個回退值 (fallback),而且 UserCard 沒有任何數據也沒有任何邏輯運行,就沒有理由呈現這個組件。編程

那麼,props 何時是必需的,何時不是必需的呢?segmentfault

有時候你只須要問問本身,怎樣實現纔是最合理的。api

假設咱們有一個轉換貨幣的組件 <CurrencyConverter/>,它有三個 props數組

  • value - 咱們要轉換的數值。
  • givenCurrency - 咱們正在轉換的貨幣。
  • targetCurrency - 咱們要轉換成的貨幣。

然而,若是咱們的 value 值不足,那麼進行任何轉換都毫無心義,那時根本不須要呈現組件。緩存

所以,value props 確定是必需的。

2、 條件在父組件中呈現

咱們有時候在一個子組件中常常會看到相似以下代碼:

import React, { useState } from "react";
import PropTypes from "prop-types";
// 子組件
export const UserCard = ({ user }) => {
  const keys = Object.keys(user)
  return (
    keys.length ?
      <ul>
        <li>{user.name}</li>
        <li>{user.age}</li>
        <li>{user.email}</li>
      </ul>
    : "No Data"
  );
};

咱們看到一個組件帶有一些邏輯,卻徒勞地執行,只是爲了顯示一個 spinner 或一條信息。

針對這種狀況,請記住,在父組件內部完成此操做老是比在組件自己內部完成更爲乾淨。

按着這個原則,子組件和父組件應該像這樣:

import React, { useState } from "react";
import PropTypes from "prop-types";

// 子組件
export const UserCard = ({ user }) => {
  return (
    <ul>
      <li>{user.name}</li>
      <li>{user.age}</li>
      <li>{user.email}</li>
    </ul>
  );
};

UserCard.propTypes = {
  user: PropTypes.object.isRequired
};

// 父組件
export const UserContainer = () => {
  const [user, setUser] = useState(null);
  // do some apiCall here
  return (
    <div>
      {user && <UserCard user={user} />}
    </div>
  );
};

經過這種方式,咱們將 user 初始化爲 null,而後簡單的運行一個 falsy 檢測,若是 user 不存在,!user 將返回 true

若是設置爲 {} 則否則,咱們必須經過 Object.keys() 檢查對象 key 的長度,經過不建立新的對象引用,咱們節省了一些內存空間,而且只有在得到了所需的數據以後,咱們才渲染子組件 <UserCard/>

若是沒有數據,顯示一個 spinner 也會很容易作到。

export const UserContainer = () => {
  const [user, setUser] = useState(null); 
  // do some apiCall here
  return (
    <div>
      {user ? <UserCard user={user} /> : 'No data available'}
    </div>
  );
};

子組件 <UserCard/> 只負責顯示用戶數據,父組件 <UserContainer/> 是用來獲取數據並決定呈現什麼的。這就是爲何父組件是顯示回退值(fallback)最佳位置的緣由。

3、不知足時,及時 return

即便咱們使用的是正常的編程語言,嵌套也是一團糟,更不用說 JSX(它是 JavaScript、 HTML 的混合體)了。

你可能常常會看到使用 JSX 編寫的相似的代碼:

const NestedComponent = () => {
  // ...
  return (
    <>
      {!isLoading ? (
        <>
          <h2>Some heading</h2>
          <p>Some description</p>
        </>
      ) : <Spinner />}
    </>
  )
}

該怎麼作纔是更合理的呢?

const NestedComponent = () => {
  // ...
  if (isLoading) return <Spinner />
  return (
    <>
      <h2>Some heading</h2>
      <p>Some description</p>
    </>
  )
}

咱們處理 render 邏輯時,在處理是否有可用的數據,頁面是否正在加載,咱們均可以選擇提早 return

這樣咱們就能夠避免嵌套,不會把 HTML 和 JavaScript 混合在一塊兒,並且代碼對於不一樣技術水平或沒有技術背景的人來講也是可讀的。

4、在 JSX 中儘可能少寫 JavaScript

JSX 是一種混合語言,能夠寫JS代碼,能夠寫表達式,能夠寫 HTML,當三者混合起來後,使用 JSX 編寫的代碼可讀性就會差不少。

雖然有經驗的人能夠理解組件內部發生了什麼,但並非每一個人都能理解。

const CustomInput = ({ onChange }) => {
  return (
    <Input onChange={e => {
      const newValue = getParsedValue(e.target.value);
      onChange(newValue);
    }} />
  )
}

咱們正在處理一些外部對 input 的一些輸入,使用自定義處理程序解析該輸入 e.target.value,而後將解析後的值傳給 <CustomInput/> 組件接收的 onChange prop。雖然這個示例能夠正常工做,可是會讓代碼變得很難理解。

在實際項目中會有更多的元素和更復雜的 JS 邏輯,因此咱們將邏輯從組件中抽離出來,會使 return() 更清晰。

const CustomInput = ({ onChange }) => {
  const handleChange = (e) => {
    const newValue = getParsedValue(e.target.value);
    onChange(newValue);
  };
  return (
    <Input onChange={handleChange} />
  )
}

當在 render 中返回 JSX 時,不要使用內聯的 JavaScript 邏輯。

5、userCallback & userEffect

隨着 v16.8 Hooks 問世後,人們開始大量使用函數組件,當使用函數進行編寫組件時,若是須要在內部執行 API 接口的調用,須要用到 useEffect 生命週期鉤子。

useEffect 用於處理組件中的 effect,一般用於請求數據,事件處理,訂閱等相關操做。

在最第一版本的文檔指出,防止 useEffect 出現無限循環,須要提供空數組 [] 做爲 useEffect 依賴項,將使鉤子只能在組件的掛載和卸載階段運行。所以,咱們會看到在不少使用 useEffect 的地方將 [] 做爲依賴項傳入。

使用 useEffect 出現無限循環的緣由是,useEffect 在組件 mount 時執行,但也會在組件更新時執行。由於咱們在每次請求數據以後基本上都會設置本地的狀態,因此組件會更新,所以 useEffect 會再次執行,所以出現了無限循環的狀況。

然而,這種處理方式就會出現 react-hooks/exhaustive-deps 規則的警告,所以代碼中經常會經過註釋忽略此警告。

// eslint-disable-next-line react-hooks/exhaustive-deps
import React, { useState, useEffect } from 'react'

import { fetchUserAction } from '../api/actions.js'

const UserContainer = () => {
  const [user, setUser] = useState(null);
  
  const handleUserFetch = async () => {
    const result = await fetchUserAction();
    setUser(result);
  };
  
  useEffect(() => {
    handleUserFetch();
    // 忽略警告
    // eslint-disable-next-line react-hooks/exhaustive-deps 
  }, []);
  
  if (!user) return <p>No data available.</p>
  
  return <UserCard data={user} />
};

最初,不少人認爲這個警告毫無心義,從而選擇進行忽略,而不去試圖探索它是如何產生的。

其實,有些人沒有意識到,handleUserFetch() 方法在組件每次渲染的時候都會從新建立(組件有多少次更新就會建立多少次)。

關於 react-hooks/exhaustive-deps 詳細的討論,能夠看下這個 issue

useCallback 的做用在於利用 memoize 減小無效的 re-render,來達到性能優化的做用。

這就是爲何咱們須要在 useEffect 中調用的方法上使用 useCallback的緣由。經過這種方式,咱們能夠防止 handleUserFetch() 方法從新建立(除非其依賴項發生變化) ,所以這個方法能夠用做 useEffect 鉤子的依賴項,而不會致使無限循環。

上邊的例子應該這樣重寫:

import React, { useState, useEffect, useCalllback } from 'react'

import { fetchUserAction } from '../api/actions.js'

const UserContainer = () => {
  const [user, setUser] = useState(null);
  
  // 使用 useCallback 包裹
  const handleUserFetch = useCalllback(async () => {
    const result = await fetchUserAction();
    setUser(result);
  }, []);
  
  useEffect(() => {
    handleUserFetch();
  }, [handleUserFetch]); /* 將 handleUserFetch 做爲依賴項傳入 */
  
  if (!user) return <p>No data available.</p>
  
  return <UserCard data={user} />
};

咱們將 handleUserFetch 做爲 useEffect 的依賴項,並將它包裹在 useCallback 中。若是此方法使用外部參數,例如 userId (在實際開發中,可能但願獲取特定的用戶) ,則此參數能夠做爲 useCallback 的依賴項傳入。只有 userId 發生變化時,依賴它的 handleUserFetch 才重寫改變。

6、抽離獨立邏輯

假設咱們在組件中有一個方法,它能夠處理組件的一些變量,併爲咱們返回一個輸出。

例如:

const UserCard = ({ user }) => {
  const getUserRole = () => {
    const { roles } = user;
    if (roles.includes('admin')) return 'Admin';
    if (roles.includes('maintainer')) return 'Maintainer';
    return 'Developer';
  }
  
  return (
    <ul>
      <li>{user.name}</li>
      <li>{user.age}</li>
      <li>{user.email}</li>
      <li>{getUserRole()}</li>
    </ul>
  );
}

這個方法和前一個例子中的方法同樣,在組件每次渲染時都會從新建立,(可是不必使用 useCallback 進行包裹,由於它沒有被做爲一個依賴項傳入)。

組件內部定義的許多邏輯能夠從組件中抽離,由於它的實現並不真正與組件相關。

改進後的代碼:

const getUserRole = (roles) => {
  if (roles.includes('admin')) return 'Admin';
  if (roles.includes('maintainer')) return 'Maintainer';
  return 'Developer';
}

const UserCard = ({ user }) => {
  return (
    <ul>
      <li>{user.name}</li>
      <li>{user.age}</li>
      <li>{user.email}</li>
      <li>{getUserRole(user.roles)}</li>
    </ul>
  );
}

經過這種方式,能夠在一個單獨的文件中定義函數,並在須要時導入,進而可能會達到複用的目的。

早期將邏輯從組件中抽象出來,可讓咱們擁有更簡潔的組件和易於重用的實用函數。

7、不要使用內聯樣式

CSS 做用域在 React 中是經過 CSS-in-JS 的方案實現的,這引入了一個新的面向組件的樣式範例,它和普通的 CSS 撰寫過程是有區別的。另外,雖然在構建時將 CSS 提取到一個單獨的樣式表是支持的,但 bundle 裏一般仍是須要一個運行時程序來讓這些樣式生效。當你可以利用 JavaScript 靈活處理樣式的同時,也須要權衡 bundle 的尺寸和運行時的開銷。 -- 來自 Vue 官網

之前,網頁開發有一個原則,叫作「關注點分離」,主要是如下三種技術分離:

  • HTML 語言:負責網頁的結構,又稱語義層。
  • CSS 語言:負責網頁的樣式,又稱
  • JavaScript 語言:負責網頁的邏輯和交互,又稱邏輯層或交互層。

對 CSS 來講,就是不要寫內聯樣式(inline style),以下:

<div style="width: 100%; height: 20px;">

可是組件化(Vue、React)流行之後,打破了這個原則,它要求把 HTML、CSS、JavaScript 寫在一塊兒。

使用 React 編寫樣式能夠這麼作:

const style = {
  fontSize: "14px"
}
const UserCard = ({ user }) => {
  return (
    <ul style={style}>
      <li>{user.name}</li>
      <li>{user.age}</li>
      <li>{user.email}</li>
      <li>{getUserRole(user.roles)}</li>
    </ul>
  );
}

React 這麼作有利於組件的隔離,每一個組件包含了全部須要用到的代碼,不依賴外部,組件之間沒有耦合,很方便複用。

這裏,本文不建議在 React 中使用內聯樣式基於兩點:

  1. 他會讓你的 HTML 結構變得臃腫。

  1. 若是樣式過多,維護起來很麻煩,沒法經過外部修改 CSS。
const style1 = {
  fontSize: "14px"
}
const style2 = {
  fontSize: "12px",
  color: "red"
}
const style = {...}
const UserCard = ({ user }) => {
  return (
    <ul style={style}>
      <li style={style2}>{user.name}</li>
      <li style={color: "#333"}>{user.age}</li>
      <li style={color: "#333"}>{user.email}</li>
      <li style={color: "#333"}>{getUserRole(user.roles)}</li>
    </ul>
  );
}

看到這裏,有人可能會反駁:「你可使用 props 有條件地對 CSS 內嵌樣式進行樣式化」,這是可行的,然而,你的組件不該該只有 10 個處理 CSS 的 props,而不作其餘事情。

若是非要在組件中編寫 CSS,建議使用 style-components CSS-in-JS 庫。

styled-components 編寫的組件樣式存在於 style 標籤內,並且選擇器名字是一串隨機的哈希字符串,實現了局部 CSS 做用域的效果(scoping styles),各個組件的樣式不會發生衝突。

若是不借助管理 CSS 的類庫,把 CSS 和 JS 混合在一塊兒,若是作的好,能夠有效的作到組件隔離。若是作的很差,這個組件不只會變得臃腫難以理解,你的 CSS 也會變得愈來愈難以維護。

8、編寫有效的 HTML

不少人對 HTML 技術的關注度都是不夠的,可是編寫 HTML 和 CSS 仍然是咱們前端工程師的必備工做。

React 是有趣的,Hooks 也是有趣的,但咱們最終關心的是渲染 HTML 和使它看起來更友好。

對於 HTML 元素來講,若是它是一個按鈕,它應該是一個 <button>,而不是一個可點擊的 div;若是它不進行表單提交,那麼它應該是 type="button";若是它應該基於文本大小自適應,它不該該有一個固定的寬度,等等。

對一些人來講,這是最基本的,但對於一部分人來講,狀況並不是如此。

咱們常常會看到相似的表單提交代碼:

import React, { useState } from 'react';

const Form = () => {
  const [name, setName] = useState('');  
  const handleChange = e => {
    setName(e.target.value);
  }
  const handleSubmit = () => {
    // api call here
  } 
  return (
    <div>
      <input type="text" onChange={handleChange} value={name} />
      <button type="button" onClick={handleSubmit}>
        Submit
      </button>
    </div>
  )
}

這個示例所作的事是在 <input/> 上更新 name 值,而且在 <button/> 上綁定 click 事件,經過點擊調用 handleSubmit 來提交數據。

這個功能對於經過使用按鈕進行提交的用戶是能夠的,可是對於使用 Enter 鍵提交表單的用戶來講就不行了。

對於 form 表單支持 Enter 提交,能夠這麼作,無需對 Enter 進行監聽:

<form onsubmit="myFunction()">
  Enter name: <input type="text">
  <input type="submit">
</form>

詳細 https://www.w3schools.com/jsref/event_onsubmit.asp

在 React 中 使用 onSubmit 是等效的:

import React, { useState } from 'react';

const Form = () => {
  const [name, setName] = useState('');
  
  const handleChange = e => {
    setName(e.target.value);
  }
  
  const handleSubmit = e => {
    e.preventDefault();
    // api call here
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input type="text" onChange={handleChange} value={name} />
      <button type="submit">
        Submit
      </button>
    </form>
  )
}

如今,這個表單提交適用於任何觸發場景,而不只僅是一個只支持經過按鈕點擊的表單。

總結

  1. 遵循單一目的組件哲學,避免過於複雜的多行組件,並儘量地將組件分解。
  2. 請記住,在父組件內部完成此操做老是比在組件自己內部完成更爲乾淨。
  3. 當在 render 中返回 JSX 時,不要使用內聯的 JavaScript 邏輯。
  4. 組件內部定義的許多邏輯能夠從組件中抽離,由於它的實現並不真正與組件相關。
  5. 早期將邏輯從組件中抽象出來,可讓咱們擁有更簡潔的組件和易於重用的實用函數。
  6. 若是不借助管理 CSS 的類庫,把 CSS 和 JS 混合在一塊兒,若是作的好,能夠有效的作到組件隔離。若是作的很差,這個組件不只會變得臃腫難以理解,你的 CSS 也會變得愈來愈難以維護。
  7. React 是有趣的,Hooks 也是有趣的,但最終咱們關心的是渲染 HTML 和使它看起來更友好。

參考:

  1. https://itnext.io/write-clean...
  2. https://zhuanlan.zhihu.com/p/...(介紹 useCallback)
  3. https://stackoverflow.com/que...(react-hooks-exhaustive-deps)
  4. https://zhuanlan.zhihu.com/p/...(介紹 CSS-in-Js)
相關文章
相關標籤/搜索