天天看點

[第26期] 幹貨!介紹4個實用的React實踐技巧總結

背景

Hooks 自推出以來就很火, 它改變了我們編寫React 代碼的方式, 有助于我們寫更簡潔的代碼。

今天這邊文章不是說Hooks的,Hooks之外, 還有很多實用的技巧可以幫助我們便攜簡潔實用的代碼。

今天我就整理了8個使用的技巧,其中有些也是我在公司項目中實踐的,現在整理出來分享給大家, 希望對大家有所啟發。

正文

1. 使用字元串來定義一個React元素

舉個簡單的例子:

// 我們可以通過把一個字元串'div' 指派給一個變量, 就像:
import React from 'react'

const MyComponent = 'div'

function App() {
  return (
    <>
      <MyComponent>
        <h3>I am inside a {'<div />'} element</h3>
      </MyComponent>
    </>
  )
}

           

複制

React 内部會調用

React.createElement

, 使用這個字元串來生成這個元素。

另外, 你也可以顯式的定義

component

來決定渲染的内容, 比如:

// 定義一個MyComponent
function MyComponent({ component: Component = 'div', name, age, email }) {
  
  return (
    <Component>
      <h1>Hi {name} </h1>
      <>
        <h6>You are {age} years old</h6>
        <small>Your email is {email}</small>
      </>
    </Component>
  )
}
           

複制

适用方式:

function App() {
  return (
    <>
      <MyComponent component="div" name="KK" age={18} email="[email protected]">
    </>
  )
}
           

複制

這種方式, 你也可以傳入一個自定義的元件, 比如:

function Dashboard({ children }) {
  return (
    <div style={{ padding: '25px 12px' }}>
      {children}
    </div>
  )
}

function App() {
  return (
    <>
      <MyComponent component={Dashboard} name="KK" age={18} email="[email protected]">
    </>
  )
}
           

複制

如果你遇到處理

一類相似的

元素或者元件,可以通過這種自定義的方式抽象出來,簡化你的代碼。

舉個現實的例子:

比如我們現在要做一個貨物打包的需求, 可以單個打, 也可以批量打, 針對共同點可以寫自定義元件:

import React from 'react'
import withTranslate from '@components/withTranslate'
import PackComponent from './PackComponent'
import usePack, { check } from './usePack'

let PackEditor = (props) => {
  const packRes = usePack(props)
  return (
    <PackComponent
      {...packRes}
    />
  )
}

PackEditor = withTranslate(PackEditor)
PackEditor.check = check

export default PackEditor
           

複制

這樣在不同的業務子產品中, 就可以靈活的使用了, 非常友善。

2. 定義錯誤邊界

在Javascript裡,我們都是使用

try/catch

來捕捉可能發生的異常,在

catch

中處理錯誤。比如:

function getFromLocalStorage(key, value) {
  try {
    const data = window.localStorage.get(key)
    return JSON.parse(data)
  } catch (error) {
    console.error
  }
}
           

複制

這樣, 即便發生了錯誤, 我們的應用也不至于崩潰白屏。

React 歸根結底也是Javascript,本質上沒什麼不同, 是以同樣的使用

try/catch

也沒有問題。

然而, 由于React 實作機制的原因, 發生在元件内部的Javascript 錯誤會破壞内部狀态, render會産生錯誤:

https://github.com/facebook/react/issues/4026

基于以上原因,React 團隊引入了

Error Boundaries

:

https://reactjs.org/docs/error-boundaries.html

Error boundaries

, 其實就是React元件, 你可以用找個元件來處理它捕捉到的任何錯誤資訊。

當元件樹崩潰的時候,也可以顯示你自定義的UI,作為回退。

看 React 官方提供的例子:https://reactjs.org/docs/error-boundaries.html#introducing-error-boundaries

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

複制

使用方式:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>
           

複制

Live Demo By Dan Abramov:

https://codepen.io/gaearon/pen/wqvxGa?editors=0010

3.高階元件

通俗點講, 所謂高階元件就是, 你丢一個元件進去, 增加一些屬性或操作, 再丢出來。

一般來說, 你可以把一些具備共同點的元件抽象成一個高階元件, 然後再不同的子產品中複用。

比如, 我們的系統中, 有一類按鈕要加個

border

, 很多地方都要用到, 我們把它抽象出來:

import React from 'react'

// Higher order component
const withBorder = (Component, customStyle) => {
  class WithBorder extends React.Component {
    render() {
      const style = {
        border: this.props.customStyle ? this.props.customStyle.border : '3px solid teal'
      }
      return <Component style={style} {...this.props} />
    }
  }
  
  return WithBorder
}

function MyComponent({ style, ...rest }) {
  return (
    <div style={style} {...rest}>
        <h2>
          This is my component and I am expecting some styles.
        </h2>
    </div>
  )
}

export default withBorder(MyComponent, { border: '4px solid teal' })

           

複制

經過withBorder裝飾的MyComponent元件, 就具備了統一border這項功能, 後面如果如果要做修改, 就可以在這個中間層統一處理, 非常友善。

在我的項目裡, 也用了一些高階元件, 舉個具體的例子:

PackEditor = withTranslate(PackEditor)

           

複制

我們的這個

PackEditor

就是一個增強過的元件, 增加了什麼功能呢?

正如名字表述的,

withTranslate

, 增加了一個翻譯功能, 下面也給大家看看這個元件是怎麼實作的:

import React from 'react'
import { Provider } from 'react-redux'
import { injectIntl } from 'react-intl'
import { store } from '@redux/store'
import { Intl } from './Locale'

const withTranslate = BaseComponent => (props) => {
  // avoid create a new component on re-render
  const IntlComponent = React.useMemo(() => injectIntl(
    ({ intl, ...others }) => (
      <BaseComponent
        intl={intl}
        translate={(id, values = {}) => { // 注入翻譯方法
          if (!id) { return '' }
          return intl.formatMessage(
            typeof id === 'string' ? { id } : id,
            values
          )
        }}
        {...others}
      />
    )
  ), [])

  IntlComponent.displayName = `withTranslate(${BaseComponent.displayName || 'BaseComponent'})`
  
  return (
    <Provider store={store}>
      <Intl>
        <IntlComponent
          {...props}
        />
      </Intl>
    </Provider>
  )
}

export default withTranslate
           

複制

用法很靈過:

const Editor = withTranslate(({
  // ...
  translate,
}) => {
  // ...
   return (
     <>
      {translate('xxx')}}
     </>
   )
})
           

複制

十分的友善。

4. Render props

Rrender prop 是指一種在 React 元件之間使用一個值為函數的 prop 共享代碼的簡單技術, 和 HOC 類似, 都是元件間的邏輯複用問題。

更具體地說,Render prop 是一個用于告知元件需要渲染什麼内容的函數。

下面看一下簡單的例子:

以下元件跟蹤 Web 應用程式中的滑鼠位置:

class Mouse extends React.Component {
  state = { x: 0, y: 0 };

  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <>
        <h1>移動滑鼠!</h1>
        <Mouse />
      </>
    );
  }
}
           

複制

當光标在螢幕上移動時,元件顯示其(x,y)坐标。

現在的問題是:

我們如何在另一個元件中複用這個行為?

換個說法,若另一個元件需要知道滑鼠位置,我們能否封裝這一行為,以便輕松地與其他元件共享它??

假設産品想要這樣一個功能:在螢幕上呈現一張在螢幕上追逐滑鼠的貓的圖檔。

我們或許會使用 <Cat mouse={{ x, y }} prop 來告訴元件滑鼠的坐标以讓它知道圖檔應該在螢幕哪個位置。

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}
           

複制

這個需求如此簡單,你可能就直接修改Mouse元件了:

class Mouse extends React.Component {
  state = { x: 0, y: 0 };

  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        <Cat mouse={this.state} />
      </div>
    );
  }
}           

複制

巴适~ 簡單粗暴, 一分鐘完成任務。

可是,如果下次産品再要想加條狗呢?

以上的例子,雖然可以完成了貓追滑鼠的需求,還沒有達到以可複用的方式真正封裝行為的目标。

當我們想要滑鼠位置用于不同的用例時,我們必須建立一個新的元件,專門為該用例呈現一些東西.

這也是 render prop 的來曆:

我們可以提供一個帶有函數 prop 的元件,它能夠動态決定什麼需要渲染的,而不是将寫死到元件裡.

修改一下上面的代碼:

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

           

複制

class Mouse extends React.Component {
  state = { x: 0, y: 0 };

  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>移動滑鼠!</h1>
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}

           

複制

提供了一個render 方法,讓動态決定什麼需要渲染。

事實上,render prop 是因為模式才被稱為 render prop ,不一定要用名為 render 的 prop 來使用這種模式。

任何被用于告知元件需要渲染什麼内容的函數 prop, 在技術上都可以被稱為 "render prop".

另外,關于 render prop 一個有趣的事情是你可以使用帶有 render prop 的正常元件來實作大多數高階元件 (HOC)。

例如,如果你更喜歡使用 withMouse HOC 而不是元件,你可以使用帶有 render prop 的正常輕松建立一個:

function withMouse(Component) {
  return class extends React.Component {
    render() {
      return (
        <Mouse render={mouse => (
          <Component {...this.props} mouse={mouse} />
        )}/>
      );
    }
  }
}
           

複制

也是非常的簡潔清晰。

有一點需要注意的是, 如果你在定義的render函數裡建立函數, 使用 render prop 會抵消使用 React.PureComponent 帶來的優勢。

因為淺比較 props 的時候總會得到 false,并且在這種情況下每一個 render 對于 render prop 将會生成一個新的值。

class Mouse extends React.PureComponent {
  // 與上面相同的代碼......
}

class MouseTracker extends React.Component {
  render() {
    return (
      <>
        <Mouse render={mouse => ( // 這是不好的!每個渲染的 `render` prop的值将會是不同的。
          <Cat mouse={mouse} />
        )}/>
      </>
    );
  }
}
           

複制

在這樣例子中,每次渲染,它會生成一個新的函數作為的 prop,因而在同時也抵消了繼承自 React.PureComponent 的元件的效果!

為了繞過這一問題,有時你可以定義一個 prop 作為執行個體方法,類似這樣:

class MouseTracker extends React.Component {
  renderTheCat(mouse) {
    return <Cat mouse={mouse} />;
  }

  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={this.renderTheCat} />
      </div>
    );
  }
}
           

複制

5.元件性能

性能優化是永恒的主題, 這裡不一一細說, 提供幾份資源供你參考:

  • React.memo https://reactjs.org/docs/react-api.html#reactmemo
  • React.useMemo https://flaviocopes.com/react-hook-usememo/
  • React.useCallback https://reactjs.org/docs/hooks-reference.html#usecallback
  • React.PureComponent https://reactjs.org/docs/react-api.html#reactpurecomponent
  • Optimizing performance https://reactjs.org/docs/optimizing-performance.html

總結

以上幾點都是我們經常要使用的技巧, 簡單實用, 分享給大家, 希望能給大家帶來一些幫助或啟發,謝謝。