不一樣團隊編寫出來的 React 代碼也不盡相同,水平有個有高低,就像十個讀者就有十種哈姆雷特,可是如下八點多是你編寫 React 代碼的基本準則。
這篇 性能優化小冊 - React 搜索優化:防抖、緩存、LRU 文章提到,最近要作 React 項目的一些重構和優化等相關工做,過了這麼久來總結一下(借鑑網上的一些文章和本身的實踐,提取一些 React 代碼優化上的共性)。html
遵循單一目的組件哲學,避免過於複雜的多行組件,並儘量地將組件分解。前端
假設咱們有一個簡單的 <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
確定是必需的。
咱們有時候在一個子組件中常常會看到相似以下代碼:
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
)最佳位置的緣由。
即便咱們使用的是正常的編程語言,嵌套也是一團糟,更不用說 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 混合在一塊兒,並且代碼對於不一樣技術水平或沒有技術背景的人來講也是可讀的。
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 邏輯。
隨着 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
才重寫改變。
假設咱們在組件中有一個方法,它能夠處理組件的一些變量,併爲咱們返回一個輸出。
例如:
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> ); }
經過這種方式,能夠在一個單獨的文件中定義函數,並在須要時導入,進而可能會達到複用的目的。
早期將邏輯從組件中抽象出來,可讓咱們擁有更簡潔的組件和易於重用的實用函數。
CSS 做用域在 React 中是經過 CSS-in-JS 的方案實現的,這引入了一個新的面向組件的樣式範例,它和普通的 CSS 撰寫過程是有區別的。另外,雖然在構建時將 CSS 提取到一個單獨的樣式表是支持的,但 bundle 裏一般仍是須要一個運行時程序來讓這些樣式生效。當你可以利用 JavaScript 靈活處理樣式的同時,也須要權衡 bundle 的尺寸和運行時的開銷。 -- 來自 Vue 官網
之前,網頁開發有一個原則,叫作「關注點分離」,主要是如下三種技術分離:
對 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 中使用內聯樣式基於兩點:
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 也會變得愈來愈難以維護。
不少人對 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> ) }
如今,這個表單提交適用於任何觸發場景,而不只僅是一個只支持經過按鈕點擊的表單。
render
中返回 JSX 時,不要使用內聯的 JavaScript 邏輯。參考: