React tips

7.1

Error boundary

static getDerivedStateFromError(error)

error 对象 { message, stack }。 页面显示 stack 可以使用 error.stack.toString()

Large list performance

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
}
Edit wo790jz1p7

其他方案

组件组合(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>

当使用自定义组件时,绑定会失效。根据文档要求,包装的组件需要定义valueonChange属性接口。

例如,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 syntaxrest 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 工具

React DevTools

Redux DevTools

受控组件: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 数据不刷新

使用 dvaantd 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>
  );
}
📖