React 学习之 Render Props

551 阅读2分钟

先看个栗子吧

比如,有这么个功能,页面中有一块区域,鼠标在区域中移动时,里面有一个圆需要跟随鼠标移动;在下面另一块区域,鼠标在区域中移动时要显示鼠标相对于这个区域的坐标点。那么我们在之前的实现方案应该是这样的:

import React, { createRef, PureComponent } from 'react'

// 可移动球
export default class MoveBall extends PureComponent {
    state = {
        x: 0,
        y: 0
    }
    areaRef = createRef()
    handleMove = e => {
        const {left, top} = this.areaRef.current.getBoundingClientRect()
        const x = ~~(e.clientX - left)
        const y = ~~(e.clientY - top)
        this.setState({ x, y })
    }
    render() {
        return (
            <div
                ref={this.areaRef}
                className="area"
                onMouseMove={this.handleMove}
            >
                <div className="circle" style={{
                    left: this.state.x - 40,
                    top: this.state.y - 40
                }} />
            </div>
        )
    }
}

// 显示鼠标位置
export default class ShowPoint extends PureComponent {
    state = {
        x: 0,
        y: 0
    }
    areaRef = createRef()
    handleMove = e => {
        const {left, top} = this.areaRef.current.getBoundingClientRect()
        const x = ~~(e.clientX - left)
        const y = ~~(e.clientY - top)
        this.setState({ x, y })
    }
    render() {
        return (
            <div
                ref={this.areaRef}
                className="area"
                onMouseMove={this.handleMove}
            >
                <div>
                    mouse left: {this.state.x},
                    mouse top: {this.state.y}
                </div>
            </div>
        )
    }
}

如上代码所示,某些组件的各种功能及其处理逻辑完全相同,只是显示的页面内容不一样。所以这种情况一般使用以下两种处理方案 (来解决重复代码的问题):

1. 使用 HOC

将功能逻辑部分进行抽离为一个高阶组件,大致实现如下:

import React, {createRef, PureComponent} from 'react'
export default function withMove(Comp) {
    return class extends PureComponent {
        state = {
            x: 0,
            y: 0
        }
        areaRef = createRef()
        handleMove = e => {
            // 同上面代码...
        }
        render() {
            return (
                <div
                    ref={this.areaRef}
                    className="area"
                    onMouseMove={this.handleMove}
                >
                     <Comp {...this.props} {...this.state} />
                </div>
            )
        }
    }
}

// 外面使用
import withMove from './hoc/withMove'
function RenderPoint(props) {
    const {x, y} = props
    return <p>Left: {x}, Top: {y}</p>
}
const ShowPoint = withMove(RenderPoint)
export default ShowPoint

2. 使用 render props

就像上一篇 React 学习之 Context (旧与新) 所写的 ctx.Consumer 的用法一样,我可以将鼠标移动的组件抽离出去,将传递的子组件(props.children)或者任意一个属性约定为一个函数,调用函数并把参数传递过去即可。这样,这个鼠标移动的部分就可以复用了,至于具体渲染的内容,只需要在使用这个组件时用函数返回即可,示例如下:

import React, { createRef, PureComponent } from 'react'
export default class MouseMove extends PureComponent {
    state = {
        x: 0,
        y: 0
    }
    areaRef = createRef()
    handleMove = e => {
        // 同上面代码...
    }
    render() {
        return (
            <div
                ref={this.areaRef}
                className="area"
                onMouseMove={this.handleMove}
            >
                {this.props.render(this.state)}
            </div>
        )
    }
}

// 外面使用时:
import React, { memo } from 'react'
import MouseMove from './MouseMove'
const renderPoint = pt => <p>Left: {pt.x}, Top: {pt.y}</p>

function ShowPoint() {
    return <MouseMove render={renderPoint} />
}
export default memo(ShowPoint)

注意点

  • 某个组件需要某个属性,该属性是一个函数,函数的返回值用于界面渲染
  • 函数的参数作为界面渲染需要的数据传递出去
  • 使用纯组件,避免每次都传递一个新的函数,导致不必要的重新渲染
  • 通常使用属性 render 用来传递 (但实现方案不过就是通过属性调用函数,所以属性名随你定)