不一样团队编写出来的 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 逻辑。参考: