1 声明函数式组件
声明 React 组件的时候采用函数式组件的声明方式,不要使用 Class 组件的声明方式
bad case
// 类组件
class Welcome extends React.Component {
// ...
render() {
return (
// ...
)
}
}
函数组件
const Welcome = props => {
return (
// ...
)
}
2 采用 Hooks
React 在 16.8 引入了 Hooks,令函数式组件具备了生命周期和管理内部 state 的能力。
1)常用 Hooks:
import React, { useState } from 'react';
const Example = () => {
const [count, setCount] = useState(0);
return (
<>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</>
);
}
与 Class 组件常用生命周期对比
Class 组件生命周期函数 | useEffect 写法 |
---|---|
componentDidMount | useEffect(() => { // componentDidMount }, []); |
componentWillUnmount | useEffect(() => { return () => { // componentWillUnmount } }); |
componentDidUpdate | useEffect(() => { // name 变化时会执行 }, [name]); |
shouldComponentUpdate | const MyComponent = React.memo( _MyComponent, (prevProps, nextProps) => nextProps.count !== prevProps.count ) |
forceUpdate | const useForceUpdate = () => { const forceUpdate = useState(0)[1]; return () => forceUpdate(x => x + 1); } export default () => { const forceUpdate = useForceUpdate(); // 先定义好,后面想用就用 // ... return ( <div > <div /> ) } |
componentDidCatch | - |
UNSAFE_componentWillReceiveProps | 该生命周期已经被废弃,如果需要获得上一次渲染的 state 和 prop 参考下面使用 useRef 获得 preState 和preProp |
getSnapshotBeforeUpdate | - |
实现实例
const Timer = () => {
const intervalRef = useRef()
useEffect(() => {
const id = setInterval(() => {
// ...
})
intervalRef.current = id
return () => {
clearInterval(intervalRef.current)
}
})
}
获得 preState
const Counter = () => {
const [count, setcount] = useState(0)
const prevCountRef = useRef()
useEffect(() => {
prevCountRef.current = count
})
const prevCount = prevcountRef.current
return <h1>Current: {count}, Before: {prevCount} </h1>
}
获得 preProp
const ScrollView = ({row}) => {
const [isScollingDown, setIsScrollingDown] = useState(false)
const [prevRow, setPrevRow] = useState(null)
if(row !== preRow) {
setIsScrollingDown(prevRow !== null && row > prevRow)
setPrevRow(row)
}
return <p>Scroll down: {isScrollingDown}</p>
}
3 如何执行异步请求
React 在推荐在componentDidMount和componentDidUpdate中执行异步请求,对应 hooks 可以写成下面的样式:
componentDidMount
useEffect(() => {
async function fetchMyAPI() {
let url = '<http://something>';
let config = {};
const response = await fetch(url);
console.log(response);
}
fetchMyAPI();
}, []);
componentDidUpdate,productId 变化时发起 fetch
useEffect(() => {
async function fetchMyAPI() {
let url = '<http://something/>' + productId;
let config = {};
const response = await fetch(url);
console.log(response);
}
fetchMyAPI();
}, [productId]);
4 优先使用 useEffect
React 提供了两个与 effect 有关的 hook:useEffect 和 useLayoutEffect。两者的区别在于:
与 componentDidMount 和 componentDidUpdate 不同,useEffect 并不会阻塞 dom 渲染,这对性能是有帮助的
而 useLayoutEffect 则会阻塞 dom 渲染。
如果在边缘 case 中(如在 effect 的 callback 中涉及到 dom 操作这样的副作用,为了避免屏幕抖动的问题),可以才需要使用 useLayoutEffect
React 官方推荐:任何场景下优先使用 useEffect,而只有遇到问题了再尝试使用到 useLayoutEffect
5 使用PureComponent 或者 React.memo
Purecomponent 和 React.memo 默认重写了 shouldComponentDidUpdate,在其中进行了浅比较,优化了渲染性能。
如下面的例子:
一个父组件的 state 发生了变化,而其子组件的 props 并没有发生变化。
如果在不使用 Purecomponent 或者 React.memo 情况下,每次父组件的state变化都会引起子组件 render 方法的执行。
使用 Purecomponent 或者 React.memo 后,只有子组件的 prop 真正发生变化,子组件的 render 方法才会执行
6 合理声明 key
根据 React 启发式算法的规则,在循环渲染过程中,需要为子元素或者子组件声明 key,key 能够帮助 React 识别那些元素被更改,删除或者添加
声明 key 时遵循两个原则:稳定和唯一
不要使用随机数或者数组索引作为 key
推荐使用唯一 id 作为 key
bad case
const numbers = \[1, 2, 3, 4, 5];
const listItems = numbers.map((number, index) =>
<li key={index}> // 不要用索引作为 key
{number}
</li>
);
const numbers = \[1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={Math.random()}> // 不要用随机数作为 key
{number}
</li>
);
good case
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number}>
{number}
</li>
);
此外,我们需要在循环体中显示声明 key
bad case
function ListItem(props) {
const value = props.value;
return (
// 不要在此处声明key
<li key={value.toString()}>
{value}
</li>
);
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 需要在map的循环体中显示地声明 key
<ListItem value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
good case
function ListItem(props) {
// Correct! There is no need to specify the key here:
return <li>{props.value}</li>;
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// Correct! Key should be specified inside the array.
<ListItem key={number.toString()} value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
7 组件的命名方式
组件的命名在应用中应当清晰且唯一
- 组件采用帕斯卡命名
bad case
import reservationCard from './ReservationCard';
good case
import ReservationCard from './ReservationCard';
- 组件实例采用驼峰命名
bad case
const ReservationItem = <ReservationCard />;
good case
const reservationItem = <ReservationCard />;
8 组件间如何通信
- 父组件向子组件传递数据
父组件通过 props 向子组件传递数据
const ChildrenComponent = (props) => {
const { parentName = '' } = props
}
const ParentComponent = () => {
const [name, setName] = setState('Dad')
return (
<ChildrenComponent parentName={name} />
)
}
- 子组件向父组件传递数据
子组件通过 callback 向父组件传递数据
const ChildrenComponent = (props) => {
const [name, setName] = setState('child')
const sendNameToParent = () => {
props.onChildNameChange(name)
}
return (
<Button onClick={sendNameToParent}>点击按钮将name传递给父组件</Button>
)
}
const ParentComponent = () => {
const [childName, setChildName] = setState('')
const onChildNameChange = (cn) => {
setChildName(cn)
}
return (
<>
<p>子组件 name:{childName}</p>
<ChildrenComponent onChildNameChange={onChildNameChange} />
</>
)
}
- 同级子组件之前传递数据
子组件之间可以通过状态提升传递数据
下面的代码中父组件 ParentComponent 包含了两个 ChildrenComponent,这两个 ChildrenComponent 的状态被提升到了 ParentComponent 中
const ChildrenComponent = (props) => {
return (
<input onChange={props.onChildrenNameChange} value={props.name} />
)
}
const ParentComponent = () => {
const [childName, setChildName] = setState('')
const onChildNameChange = (cn) => {
setChildName(cn)
}
return (
<>
<ChildrenComponent onChildrenNameChange={onChildNameChange} name={childName} />
<ChildrenComponent onChildrenNameChange={onChildNameChange} name={childName} />
</>
)
}
9 <> or React.Fragment 包装
React 组件在 retrun 多个元素的时候,我们需要将这些组件包裹在一个额外的元素中,如 <div>。这个额外的元素并没有实际的意义,仅仅是为了适配 React 组件返回一个元素的模式。
在 React 15 的时候引入了 React.Fragment,作为不可见元素对 React 组件的返回进行包裹
bad case
const Component = () => {
return (
<div>
<td>Hello</td>
<td>World</td>
</div>
);
}
good case
const Component = () => {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
);
}
10 设计单一职责组件
以一个请求数据列表的组件为例,通常我们可以开发一个 <FetchDataTable> 的组件,在其中请求数据并将数据渲染到 <table> 中。
bad case
const FetchDataTable = (props) => {
const [data, setData] = useState([])
useEffect(() => {
fetch(url).then(res => {
setData(res.data)
})
}, [])
return (
<table>
{
data.map(value => {
return (
<tr key={value.id}>
<td>value.name</td>
<td>value.sex</td>
<td>value.age</td>
</tr>
)
})
}
</table>
)
}
<FetchDataTable> 组件有两个职责:
1)当组件 mount 后发送异步请求请求列表数据
2)数据变化后渲染列表
我们可以将 <FetchDataTable> 组件根据职责拆分成为两个的组件,每个组件都有唯一的职责。其中 <TableInfo> 组件只负责列表渲染,而 <FetchDataTable> 组件负责请求数据,并渲染 组件
good case
const TableInfo = (props) => {
const { data = [] } = props
return (
<table>
{
data.map(value => {
return (
<tr key={value.id}>
<td>value.name</td>
<td>value.sex</td>
<td>value.age</td>
</tr>
)
})
}
</table>
)
}
const FetchDataTable = (props) => {
const [data, setData] = useState([])
useEffect(() => {
fetch(url).then(res => {
setData(res.data)
})
}, [])
return (
<TableInfo data={data} />
)
}
11 不要使用 props 初始化 state
尽量不要使用 props 去初始化 state,props与state通信放到 useEffect 中执行
12 useState 时避免使用一个大 Object
与 Class Component 中 this.state 是一个对象不同的是,函数式组件中使用 useState 初始化 state 时,参数可以是一个对象、number 或者 string。并且可以多次声明 useState
推荐在函数式组件中尽量将 state 根据业务逻辑分割成不同的 state。
bad case
function Box() {
const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
useEffect(() => {
function handleWindowMouseMove(e) {
// Spreading "...state" ensures we don't "lose" width and height
setState(state => ({ ...state, left: e.pageX, top: e.pageY }));
}
// Note: this implementation is a bit simplified
window.addEventListener('mousemove', handleWindowMouseMove);
return () => window.removeEventListener('mousemove', handleWindowMouseMove);
}, []);
}
good case
function Box() {
const [position, setPosition] = useState({ left: 0, top: 0 });
const [size, setSize] = useState({ width: 100, height: 100 });
useEffect(() => {
function handleWindowMouseMove(e) {
setPosition({ left: e.pageX, top: e.pageY });
}
// ...
})
}
13 使用 propTypes + defaultProp
在不使用 TS 的情况下,propTypes 能够对组件的 props 执行静态类型检查。
不使用 redux 的情况下,defaultProp 会为组件的 prop 提供默认值。
推荐使用 propTypes 和 defaultProp 增强代码的健壮性。
注意:prop-types 仅会在 dev 环境中执行检查。在 prod 环境下,处于性能考虑,prop-types 会被忽略
14 避免滥用 context
Provider 的 value 发生了变化,其所有的 Comsuer 都会重新执行渲染。demo
以经验来看,滥用 Context 会造成组件的 props 源和无法控制。相比之下,大多数场景下逐层透传 props 的 prop drilling 是可以接受的。
15 使用受控组件
尽可能使用受控组件。通常只有在整合 React 和非 React 代码时才需要用到非受控组件。
非受控 form
class NameForm extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.input = React.createRef();
}
handleSubmit(event) {
alert('A name was submitted: ' + this.input.current.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
受控 form
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('A name was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
16 请记住 state 更新是异步的
在 React 的生命周期或者合成事件中,对与 state 的修改(setState)是异步执行的,也就是调用 setState 后无法立刻拿到 state 的值。
17 不要在 JSX 中使用匿名函数
在 JSX 中使用匿名函数,会造成每次渲染都会生成一个新的函数对象,对于组件来说,每次渲染都会造成 props 变化,引起性能问题。PureComponent 和 React.memo 也会失效
bad case
const Box = props => {
return (
<>
<Button onClick={props => console.log(props)} />
</>
)
}
good case
const handleClick = props = console.log(props)
const Box = props => {
return (
<>
<Button onClick={handleClick} />
</>
)
}
对于必须将函数声明在函数组件的情况,可以使用 useCallback 来缓存函数,不过需要注意的是,useCallback 通过闭包获得 state,因此 useCallback 的第二个参数需要指定 state。
如果 state 频繁变化,如 input 的值,那么 useCallback 的引用无法被缓存,也就失去了缓存的意义
bad case
function Form() {
const [text, updateText] = useState('');
const handleSubmit = useCallback(() => {
console.log(text);
}, [text]); // input 的值,经常变化
return (
<>
<input value={text} onChange={(e) => updateText(e.target.value)} />
<ExpensiveTree onSubmit={handleSubmit} /> // 渲染很耗时的组件,onSubmit如果无法被缓存,页面会非常卡顿
</>
);
}
good case
function Form() {
const [text, updateText] = useState('');
const textRef = useRef();
useEffect(() => {
textRef.current = text; // textRef 每次渲染不会发生变化
});
const handleSubmit = useCallback(() => {
const currentText = textRef.current; // 从 textRef 中读取 text
alert(currentText);
}, [textRef]); // useCallback 的第二个参数设置为 textRef
return (
<>
<input value={text} onChange={e => updateText(e.target.value)} />
<ExpensiveTree onSubmit={handleSubmit} /> // onSubmit 被缓存
</>
);
}
18 不要在 JSX 中使用 inline 对象
与 17 的理由类似,不要在 JSX 中使用 inline 对象
bad case
function Component(props) {
const aProp = { someProp: 'someValue' }
return <AnotherComponent style={{ margin: 0 }} aProp={aProp} />
}
good case
const styles = { margin: 0 };
function Component(props) {
const aProp = { someProp: 'someValue' }
return <AnotherComponent style={styles} {...aProp} />
}
19 不要异步访问 React 合成事件
React 为事件回调提供了合成事件作为参数,处于性能考虑,并不支持在异步中访问合成事件
bad case
function onClick(event) {
setTimeout(function() {
console.log(event.type); // => null
}, 0);
// 不起作用,this.state.clickEvent 的值将会只包含 null
this.setState({clickEvent: event});
}
good case
function onClick(event) {
const eventType = event.type; // => "click"
setTimeout(function() {
console.log(eventType); // => "click"
}, 0);
// 你仍然可以导出事件属性
this.setState({eventType: event.type});
}
20 安装 React Dev Tools
安装 React Dev Tools,会很好的帮助你调试 React 组件。