React 最佳实践

293 阅读8分钟

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 写法
componentDidMountuseEffect(() => {
    // componentDidMount
}, []);
componentWillUnmountuseEffect(() => { return () => {
    // componentWillUnmount
} });
componentDidUpdateuseEffect(() => {
    // name 变化时会执行
}, [name]);
shouldComponentUpdateconst MyComponent = React.memo(
     _MyComponent,
     (prevProps, nextProps) => nextProps.count !== prevProps.count
)
forceUpdateconst 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:useEffectuseLayoutEffect。两者的区别在于:

与 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 组件。

unnamed.jpg