React tips
Error boundary
static getDerivedStateFromError(error)
error 对象 { message, stack }
。 页面显示 stack 可以使用 error.stack.toString()
Large list performance
- react-window Render large lists and tabular data
change element on hover
toggleHover = () => {
this.setState(prevState => ({ isHovered: !prevState.isHovered }))
}
render() {
return (
<div onMouseEnter={this.toggleHover} onMouseLeave={this.toggleHover}>
{ isHovered ? 'a' : 'b' }
</div>
)
}
React.Fragment
占位符,支持直接返回多个同级子元素
<React.Fragment>
<div>Hello</div>
<div>World</div>
</React.Fragment>
<>
<div>Hello</div>
<div>World</div>
</>
React v17
react-v17 是一个过渡版本(stepping stone),没有添加新特性。用于支持后续版本的平滑更新。
- Event Delegation: 事件绑定到 root DOM container
- New JSX Transform
- React Native: 有独立的更新计划
npm i react@17.0.0 react-dom@17.0.0
Upgrade react-scripts
查看 changelog,选择合适的版本,例如 4.0.1
npm i react-scripts@4.0.1
checkbox and radio
checkbox
e.target.checked
function updateType(e) {
setType(e.target.checked)
}
<div>
<input
type="checkbox"
id="type"
checked={type}
onChange={updateType}
/>
<label htmlFor="type">Type available</label>
</div>
e.target.value
function toggleDates(e) {
const newValue = e.target.value
const index = dates.indexOf(newValue)
if (index === -1) {
dates.push(newValue)
} else {
dates.splice(index, 1)
}
setDates([...dates])
}
<input
type="checkbox"
id={option}
value={option}
checked={dates.includes(option)}
onChange={toggleDates}
/>
radio
function updateType(e) {
setType(e.target.value)
}
{options.map(option => (
<span>
<input
type="radio"
name="type"
id={option}
value={option}
checked={type === option}
onChange={updateType}
/>
<label htmlFor={option}>{option}</label>
</span>
))}
select
function updateType(e) {
const newValue = e.target.value
setType(newValue)
}
<select value={type} onChange={updateType}>
{typeOptions.map(({ text, value }) => (
<option key={value} value={value}>{text}</option>
))}
</select>
Dom Elements
Fast Refresh / HMR
Fast Refresh 是从 React 的角度来重新实现 HMR。原先的 HMR 是 Webpack 提供的功能。在导出函数式组件时避免使用匿名函数。
Error Boundaries
https://reactjs.org/docs/error-boundaries.html
错误处理组件,专门处理子组件的报错。定义方式:类组件、定义任一static getDerivedStateFromError()
或 componentDidCatch()
生命周期方法。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
Then you can use it as a regular component
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
无法捕捉的情况
- event handlers
- Asynchronous code(e.g. setTimeout or requestAnimationFrame callbacks)
- Server side rendering
- Errors thrown in the error boundary itself(rather than its children)
Code splitting
https://reactjs.org/docs/code-splitting.html
代码分开后,可以只加载当前页面必要的代码。从而极大的提高单个页面的加载速度。
- import()
- React.lazy
- Suspense
import React, { Suspense } from 'react';
const Component = React.lazy(() => import('./Component'));
<Suspense fallback={<div>Loading...</div>}>
<Component />
</Suspense>
Uncaught SyntaxError: Unexpected token '<
出错该报错的一种情况是使用React-router
, 只配置了一级域名,却在地址栏输入了二级域名。
解决方案:需要检查地址是否正确。
页面刷新时不会触发 componentWillUnmount
事件
需要同时使用beforeunload
事件,以确保回调函数在页面刷新和关闭时都会调用。
componentDidMount() {
window.addEventListener('beforeunload', callback.bind(this))
},
componentWillUnmount() {
window.removeEventListener('beforeunload', callback);
}
input onChange 渲染慢(lag)
用户输入触发onChange
后一般会修改state
,导致 render。当输入比较快时会频繁触发 render。如果 render 更新组件较多,就会出现卡顿。
可能的解决方案
-
检查主要组件是否过大且每次按键都会触发重渲染。考虑使用多个小组件。
-
检查子组件是否有不必要的渲染。why-did-you-update 可以检测到这些渲染。考虑切换为纯组件、无状态组件,或使用 shouldComponentUpdate。
-
使用 uncontrolled components,让原始的 DOM 处理 input。可以用
ref
获取对应的 DOM 和值。
Context
Context
用于在树形结构组件中共享数据。适用于嵌套层次较深的情况。
使用 React.createContext
创建ThemeContext
,使用ThemeContext.Provicer
将子组件包起来。
const ThemeContext = React.createContext("light");
class Demo extends React.Component {
render() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
那么任意子组件都可以获取到ThemeContext
的值了,
static contextType = ThemeContext;
render() {
// this.context => dark
}
其他方案
组件组合(component composition)
通过传递组件来避免多个属性在组件间传递
Render Props
子组件依赖父组件进行渲染
function as children
class Lazyload extends React.Component {
render() {
const loading = "loading...";
return <div>{this.props.children(loading)}</div>;
}
}
render() {
return (
<Lazyload>
{ loading => <div>{loading}</div> }
</Lazyload>
);
}
组件接收到的this.props.children
是一个函数,通过调用函数的参数来传递属性。
Code Daily - Using Functions as Children and Render Props in React Components
渲染数组元素时,使用 index 作为 key
特定情况下,可以使用 index 作为 key
- 数组是静态的,不是计算出来的,也不会改变
- 数组不会被重排或过滤
- 数组元素没有唯一 id
否则,在重新渲染时,会出现元素重复或遗漏的情况。React 根据 key 来对比 DOM 是否发生改变。参考
React form 包装自定义组件
React form是用于处理表单的高阶组件。通常会用来绑定或装饰表单元素,如 input,select 等。提交时会自动校验、整合数据等。
<form>
<input {...getFieldProps('name', { ...options })} />
</form>
<form>
{getFieldDecorator('name', otherOptions)(<input />)}
</form>
当使用自定义组件时,绑定会失效。根据文档要求,包装的组件需要定义value
和onChange
属性接口。
例如,CustomComponent
需要能处理 this.props.value 和 this.props.onChange,才能用于 React form 中。
<form>
<CustomComponent {...getFieldProps('name', { ...options })} />
</form>
<form>
{getFieldDecorator('name', otherOptions)(<CustomComponent />)}
</form>
父组件向子组件传递部分属性
全部属性
return <WrappedComponent {...this.props} />;
部分属性
使用spread syntax 和 rest parameters语法
const { propOne, propTwo, ...otherProps } = this.props;
return <WrappedComponent {...otherProps} onClick={this.showConfirm} />;
修改多层嵌套的 state
拷贝后修改
const book = Object.assign({}, this.state.book);
book.name = "some thing";
this.setState({ book });
使用 spread operator
this.setState((prevState) => ({
book: {
...prevState.book,
name: "some thing",
},
}));
使用库
immutability-helper 提供语法糖高效方便地修改不可变数据(复制数据后进行修改)
import update from "immutability-helper";
const state1 = ["x"];
const state2 = update(state1, { $push: ["y"] }); // ['x', 'y']
React event: SyntheticEvent
React 的事件响应函数接收 SyntheticEvent 实例,是对浏览器原生事件对象的统一包装。
onClick, onKeyDown 等事件响应函数接收的对象都是 SyntheticEvent 实例。
event.nativeEvent
可以获取原生事件。
Event Pooling
SyntheticEvent 对象会被复用(pooled)以优化性能,意味着在响应函数调用后该对象会被清空(nullified)。无法被异步使用。
需要异步获取对象时,可以保存对象的属性,或调用 event.persist()
移除复用功能。
function onClick(event) {
console.log(event) // Will be nullified object
console.log(event.type) // 'click'
const eventType = event.type // 'click'
setTimeout(function () {
console.log(event.type) // null
console.log(eventType) // 'click'
}, 0)
// Won't work
this.setState({ clickEvent: event })
// You can still save event properties
this.setState({ eventType: event.type })
}
检测属性变化
componentDidMount
初次渲染时调用。可用于同步属性到状态、发起网络请求等。
componentDidUpdate(prevProps, prevState, snapshot)
实例方法。可用于比较当前属性(this.props)与之前属性(prevProps)。不会在初次渲染时调用。
componentDidUpdate(prevProps) {
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID)
}
}
在componentDidUpdate
中如果修改 state 后会再次触发componentDidUpdate
。可以在实例this
上设置开关变量,以避免循环调用。
componentDidUpdate(prevProps, prevState) {
const finish = this.props.finish;
if (!this.finish && finish) {
this.method();
}
this.finish = finish;
}
getDerivedStateFromProps(nextProps, prevState)
静态方法,用于新属性(nextProps)导致 state 变化的场景。返回值为更新的 state。
static getDerivedStateFromProps(nextProps, prevState) {
const finish = nextProps.out && nextProps.out.finish;
if (finish && !prevState.finish) {
return {
finish,
};
}
return null;
}
componentWillReceiveProps(nextProps)
实例方法。当组件接收到新的属性后会调用本方法(不包括首次渲染)。常用于 props 变化导致 state 变化的场景。注意比较新属性(nextProps)与当前属性(this.props)的差异。
UNSAFE_componentWillReceiveProps() will continue to work until version 17。该方法将会被弃用,可以考虑链接中的其它实现方案。
Updating 触发的事件
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
状态修改后 UI 刷新慢
setState
后如果有耗时较长的代码逻辑,如图片转换等,可能导致 state 刷新 UI 有几秒的延迟。可以在setState
回调中使用 setTimeout 调用这些代码,并可以设置 100 这样少许的等待,在 UI 刷新后执行后续代码。
UI 及时给予用户反馈,会极大地提升用户体验
setState(
{
loading: true,
},
() => {
setTimeout(() => {
// Slow code like canvas.toDataURL
}, 100);
}
);
Debug 工具
受控组件:controlled component
HTML 中 input 等元素会自动根据用户输入更新 value。如果通过 React 的方式来获取用户输入并更新其 value,则称这类 input 元素为 controlled component
。相反的,叫做uncontrolled component
handleChange(event) {
this.setState({ value: event.target.value });
}
render() {
return (
<input type="text" value={this.state.value} onChange={this.handleChange} />
);
}
获取 DOM
ReactDOM.findDOMNode
获取组件对应的 DOM
import ReactDOM from "react-dom";
class MyComponent extends Component {
myMethod() {
const node = ReactDOM.findDOMNode(this);
}
}
Ref
获取组件实例或组件内的 DOM
// this.input.current 可以获取到 DOM
class MyComponent extends Component {
constructor(props) {
super(props);
this.input = React.createRef();
}
render() {
return <input ref={this.input} />;
}
}
import() 按需加载
对代码分开打包,并仅加载必需的代码可以极大地提升性能。
dynamic import()
以函数的形式接收模块名为参数,返回 Promise,会解析成模块的命名空间对象。
例子:
moduleA.js
const moduleA = "Hello";
export { moduleA };
App.js
import React, { Component } from "react";
class App extends Component {
handleClick = () => {
import("./moduleA")
.then(({ moduleA }) => {
// Use moduleA
})
.catch((err) => {
// Handle failure
});
};
render() {
return (
<div>
<button onClick={this.handleClick}>Load</button>
</div>
);
}
}
export default App;
会将 moduleA.js
和其特有的依赖打包成单独的代码包,并仅在点击按钮后加载。
也可以使用 async
/ await
语法.
react-loadable 动态按需加载组件(code splitting)
使用
import Loadable from "react-loadable";
import Loading from "./Loading";
const LoadableComponent = Loadable({
loader: () => import("./Dashboard"),
loading: Loading,
});
export default class LoadableDashboard extends React.Component {
render() {
return <LoadableComponent />;
}
}
Ant Design Modal 数据不刷新
使用 dva 和 antd mobile 开发时,遇到封装后的 Modal 数据不刷新的问题。发现在封装组件中需要传入children
元素,即使为空,也要传入一个空元素,例如<span></span>
。
事件响应函数的传递
以onClick
为例,(1)接受事件对象为参数时,可以直接传函数。(2)需要自由传递可用的变量作为参数时,也可以传一个匿名函数,在函数体中调用响应函数。(3)预先在constructor
中使用bind
指定this
和参数。
// 接受事件参数
<div onClick={this.save}></div>
// 接受自定义参数
<div onClick={() => this.save(name, history)}
// in constructor
this.save = this.save.bind(this, name, history)
button 点击进行路由跳转
组合组件:用 Route 包装 button
import { Route } from "react-router-dom";
const Button = () => (
<Route
render={({ history }) => (
<button
type="button"
onClick={() => {
history.push("/new-location");
}}
>
Click Me!
</button>
)}
/>
);
高阶组件:withRouter 返回处理过的组件
import { withRouter } from "react-router-dom";
// this also works with react-router-native
const Button = withRouter(({ history }) => (
<button
type="button"
onClick={() => {
history.push("/new-location");
}}
>
Click Me!
</button>
));
window is not defined
前后端同构,import 的库里面直接使用 window 时会报错。
解决
检查 window 对象。若有定义,使用 require 加载库。
if (typeof window != "undefined") {
var Fingerprint = require("fingerprintjs2");
} else {
//...
}
Refs 使用
使用 Refs,可以直接访问内部的 DOM 或 React elements。Refs and the DOM
React 16.3 引入了
React.createRef()
。之前的版本建议使用callback refs
。
Creating Refs
用React.createRef()
创建,并传递给 ref 属性。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
使用
const node = this.myRef.current;
ref.current 的值取决于所在节点的类型,
-
HTML element => DOM element
可以使用 DOM 的属性和方法
-
custom class component => instance of the component
可以使用 props, state, setState, class methods 等
-
Do not use ref on functional components
Callback Refs
传给 ref 属性一个回调函数,函数参数即为获取到的 component instance 或 DOM element。
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = null;
this.setTextInputRef = element => {
this.textInput = element;
};
this.focusTextInput = () => {
// Focus the text input using the raw DOM API
if (this.textInput) this.textInput.focus();
};
}
componentDidMount() {
// autofocus the input on mount
this.focusTextInput();
}
render() {
// Use the `ref` callback to store a reference to the text input DOM
// element in an instance field (for example, this.textInput).
return (
<div>
<input type="text" ref={this.setTextInputRef} />
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
cloneElement 设置 ref
render() {
return (
<div>
{React.Children.map(this.props.children, (element, idx) => {
return React.cloneElement(element, { ref: idx });
})}
</div>
);
}