什么是组合组件?写个 Select 就知道了

530 阅读3分钟

Ant Design 的组件库的 Select 组件我们经常用,但是你有没有想过,Select 是如何实现的呢?

本文中,我将一步一步实现 Select,通过实践和思考,学一种新的组件设计模式:组合模式。

效果参照:选择器 Select - Ant Design

功能分析

首先,我们实现最基础的功能。

1.引入 Select 后,从 Select 中结构出 Option。

2.写入多个 Option 后,点击输入框正常展示所有 Option; 再次点击输入框后,Option 包裹框关闭。

3.点击 Option 内容,输入框展示对应的 Option, Option 包裹框关闭。

常规实现

我做了一个简易版的,效果如下: Snipaste_2022-06-27_17-00-35.png Snipaste_2022-06-27_17-01-01.png Snipaste_2022-06-27_17-01-10.png

代码实现如下:

components/Select/index.js

import React, { useState } from 'react';
import './index.css';
​
const Select = ({ value, children, ...props }) => {
    const [visible, setVisible] = useState(false);
​
    const onOptionVisibelChange = (e) => {
        setVisible(!visible)
    }
​
    return (
        <div className='common-container' onClick={onOptionVisibelChange}>
            <div className='select-container' {...props} >{value}</div>
            <div className='option-container' hidden={!visible}>
                {children}
            </div>
        </div>
    )
}
​
Select.Option = ({ value, children, ...props }) => {
    return <div className='option-style' {...props} >{children}</div>
}
​
export default Select;

components/Select/index.css

/*Select 样式*/
.common-container{
    width: fit-content;
    margin:100px auto 0;
}
.select-container{
    width: 200px;
    height:40px;
    border:1px solid #ccc;
}
.option-container{
    width: 200px;
    height:auto;
    border:1px solid #666;
    margin-top:10px;
}
​
/* Option Style*/
.option-style{
    background-color: #eee;
    padding:16px;
    margin:10px auto;
    width: 96%;
}

引用及其他使用如下:

import React, { useState } from 'react';
import Select from './components/Select';
const { Option } = Select;
​
const SelectDemo = () => {
​
    const [select, setSelect] = useState(null);
​
​
    const list = [{
        label: 'Lucy', value: 1
    }, {
        label: 'Tom', value: 2
    }, {
        label: 'Mike', value: 3
    }, {
        label: 'Steven', value: 4
    }]
​
    const onOptionClick = (label) => {
        setSelect(label);
    }
​
    return <>
        <Select value={select}>
            {
                list.map(item => <Option value={item.value} onClick={() => onOptionClick(item.label)}>{item.label}</Option>)
            }
        </Select>
    </>
}
​
export default SelectDemo;

眼尖的同学可能发现了,为了实现 Option 点击之后,改变 Select 的值,我不仅在每个 Option 上都加了 onClick 事件,还在 Select 传入了 选中值。

这是因为一般来说,Select 组件内部我无法操作 children ,所以一切的传参行为,都在 Select 和 Option 同时出现的地方进行操作。

这样的写法,未免有些冗余。我们可以看到,antd 组件库的 Select 于 Option 用法如下:

<Select defaultValue="lucy" style={{ width: 120 }} onChange={handleChange}>
      <Option value="jack">Jack</Option>
      <Option value="lucy">Lucy</Option>
</Select>

很明显,antd 并没有像常规写法一样添加 onClick 事件,那么,它是怎么做到的呢?

React.Children

官方文档地址:React 顶层 API – React (docschina.org)

React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。

即,该方法可以对 props.children 进行进一步处理。此时,我们可以修改 components/Select/index.js 内容如下:

import React, { useState } from 'react';
import './index.css';
​
const Select = ({ value, children, ...props }) => {
    const [visible, setVisible] = useState(false);
    const [select, setSelect] = useState(null);
​
    const onOptionVisibelChange = (e) => {
        console.log('on click event', e);
        setVisible(!visible)
    }
​
    const newChildren = React.Children.map(children, (child, index) => {
        // 给每个子元素添加一个 click 方法
        const { props } = child;
        const { children } = props;
        return <div onClick={() => setSelect(children)}>{child}</div>
    })
​
    return (
        <div className='common-container' onClick={onOptionVisibelChange}>
            <div className='select-container' {...props} >{select}</div>
            <div className='option-container' hidden={!visible}>
                {newChildren}
            </div>
        </div>
    )
}
​
Select.Option = ({ value, children, ...props }) => {
    return <div className='option-style' {...props} >{children}</div>
}
​
export default Select;

selectDemo.js 修改如下:

import React from 'react';
import Select from './components/Select';
const { Option } = Select;
​
const SelectDemo = () => {
​
​
    const list = [{
        label: 'Lucy', value: 1
    }, {
        label: 'Tom', value: 2
    }, {
        label: 'Mike', value: 3
    }, {
        label: 'Steven', value: 4
    }]
​
    return <>
        <Select >
            {
                list.map(item => <Option value={item.value} >{item.label}</Option>)
            }
        </Select>
    </>
}
​
export default SelectDemo;

此时,我们发现,使用该组件的方式优雅了许多,也依然能够实现原有功能。

我们可以进一步完善,给该组件添加 onChange 功能。

components/Select/index.js

import React, { useState } from 'react';
import './index.css';
​
const Select = ({ value, children, onChange, ...props }) => {
    const [visible, setVisible] = useState(false);
    const [select, setSelect] = useState(null);
​
    const onOptionVisibelChange = (e) => {
        setVisible(!visible)
    }
​
    const onOptionClick = (label, value) => {
        setSelect(label);
        onChange?.(value);
    }
​
    const newChildren = React.Children.map(children, (child, index) => {
        // 给每个子元素添加一个 click 方法
        const { props } = child;
        const { children, value } = props;
        return <div onClick={() => onOptionClick(children, value)}>{child}</div>
    })
​
    return (
        <div className='common-container' onClick={onOptionVisibelChange}>
            <div className='select-container' {...props} >{select}</div>
            <div className='option-container' hidden={!visible}>
                {newChildren}
            </div>
        </div>
    )
}
​
Select.Option = ({ value, children, ...props }) => {
    return <div className='option-style' {...props} >{children}</div>
}
​
export default Select;

selectDemo.js

import React from 'react';
import Select from './components/Select';
const { Option } = Select;
​
const SelectDemo = () => {
​
​
    const list = [{
        label: 'Lucy', value: 1
    }, {
        label: 'Tom', value: 2
    }, {
        label: 'Mike', value: 3
    }, {
        label: 'Steven', value: 4
    }]
​
    const onSelectChange = (value) => {
        console.log('value', value);
    }
​
    return <>
        <Select onChange={onSelectChange} >
            {
                list.map(item => <Option value={item.value} >{item.label}</Option>)
            }
        </Select>
    </>
}
​
export default SelectDemo;

组合组件

优化后的组件有什么特点呢?

1.使用时,减少了 props 的传递。

2.将主要的处理内容都放在了组件内部。

这就是组合组件的实现实践。我对它的概念是这么理解的:它的核心目的是,将有关联的的组件组合起来使用即可,不需要做额外的处理。

结语

后面如果想进一步的实现功能,可以自己再实践一把。

如果本文对你有帮助的话,记得给我点个赞哦,每一个赞都是对我莫大的鼓励~