<Menu defaultIndex={'0'} onSelect={(index) => {alert(index)}} mode="vertical" defaultOpenSubMenus={['2']}> <MenuItem index={'0'}> cool link </MenuItem> <MenuItem index={'1'}> cool link 2 </MenuItem> <SubMenu title="dropdown"> <MenuItem index={'3'}> dropdown 1 </MenuItem> <MenuItem index={'4'}> dropdown 2 </MenuItem> </SubMenu> <MenuItem index={'2'}> cool link 3 </MenuItem> </Menu>
在这个组件中,我们用到了useState,另外因为涉及父组件传数据到子组件,所以还用到了useContext(父组件数据传递到子组件是指的父组件的index数据传递到子组件)。另外,我们还会演示使用自定义的onSelect来实现onClick功能(万一你引入React泛型不成功,或者不知道该引入哪个React泛型,还可以用自定义的补救一下)。
如何写onSelect为了防止后面在代码的汪洋大海中难以找到onSelect,这里先简单的抽出来做一个onSelect书写示例。比如我们在Menu组件中使用onSelect,它的使用方式和onClick看起来是一样的:
<Menu onSelect={(index) => {alert(index)}}>
在具体这个Menu组件中具体使用onSelect可以这样写:
type SelectCallback = (selectedIndex: string) => void interface MenuProps { onSelect?: SelectCallback; }
实现handleClick的方法可以写成这样:
const handleClick = (index: string) => { // onSelect是一个联合类型,可能存在,也可能不存在,对此需要做判断 if (onSelect) { onSelect(index) } }
到时候要想把这个onSelect传递给子组件时,使用onSelect: handleClick绑定一下就好。(可能你没看太懂,我也不知道该咋写,后面会有整体代码分析,可能联合起来看会比较容易理解)
React.Children在讲解具体代码之前,还要再说说几个小知识点,其中一个是React.Children。
React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。
为什么我们会需要使用React.Children呢?是因为如果涉及到父组件数据传递到子组件时,可能需要对子组件进行二次遍历或者进一步处理。但是我们不能保证子组件是到底有没有,是一个还是两个或者多个。
this.props.children 的值有三种可能:如果当前组件没有子节点,它就是 undefined ;如果有一个子节点,数据类型是 object ;如果有多个子节点,数据类型就是 array 。所以,处理 this.props.children 的时候要小心[1]。
React 提供一个工具方法 React.Children 来处理 this.props.children 。我们可以用 React.Children.map 来遍历子节点,而不用担心 this.props.children 的数据类型是 undefined 还是 object[1]。
所以,如果有父子组件的话,如果需要进一步处理子组件的时候,我们可以使用React.Children来遍历,这样不会因为this.props.children类型变化而出错。
React.cloneElementReact.Children出现时往往可能伴随着React.cloneElement一起出现。因此,我们也需要介绍一下React.cloneElement。
在开发复杂组件中,经常会根据需要给子组件添加不同的功能或者显示效果,react 元素本身是不可变的 (immutable) 对象, props.children 事实上并不是 children 本身,它只是 children 的描述符 (descriptor) ,我们不能修改任何它的任何属性,只能读到其中的内容,因此 React.cloneElement 允许我们拷贝它的元素,并且修改或者添加新的 props 从而达到我们的目的[2]。
例如,有的时候我们需要对子元素做进一步处理,但因为React元素本身是不可变的,所以,我们需要对其克隆一份再做进一步处理。在这个Menu组件中,我们希望它的子组件只能是MenuItem或者是SubMenu两种类型,如果是其他类型就会报警告信息。具体来说,可以大致将代码写成这样:
if (displayName === 'MenuItem' || displayName === 'SubMenu') { // 以element元素为样本克隆并返回新的React元素,第一个参数是克隆样本 return React.cloneElement(childElement, { index: index.toString() }) } else { console.error("Warning: Menu has a child which is not a MenuItem component") }
父组件数据如何传递给子组件通过使用Context来实现父组件数据传递给子组件。如果对Context不太熟悉的话,可以参考,Context,在父组件中我们通过createContext来创建Context,在子组件中通过useContext来获取Context。
index数据传递Menu组件中实现父子组件中数据传递变量主要是index。
最后附上完整代码,首先是Menu父组件:
import React, { useState, createContext } from 'react' import classNames from 'classnames' import { MenuItemProps } from './menuItem' type MenuMode = 'horizontal' | 'vertical' type SelectCallback = (selectedIndex: string) => void export interface MenuProps { defaultIndex?: string; // 用于哪个menu子组件是高亮显示 className?: string; mode?: MenuMode; style?: React.CSSProperties; onSelect?: SelectCallback; // 点击子菜单时可以触发回调 defaultOpenSubMenus?: string[]; } // 确定父组件传给子组件的数据类型 interface IMenuContext { index: string; onSelect?: SelectCallback; mode?: MenuMode; defaultOpenSubMenus?: string[]; // 需要将数据传给context } // 创建传递给子组件的context // 泛型约束,因为index是要输入的值,所以这里写一个默认初始值 export const MenuContext = createContext<IMenuContext>({index: '0'}) const Menu: React.FC<MenuProps> = (props) => { const { className, mode, style, children, defaultIndex, onSelect, defaultOpenSubMenus} = props // MenuItem处于active的状态应该是有且只有一个的,使用useState来控制其状态 const [ currentActive, setActive ] = useState(defaultIndex) const classes = classNames('menu-demo', className, { 'menu-vertical': mode === 'vertical', 'menu-horizontal': mode === 'horizontal' }) // 定义handleClick具体实现点击menuItem之后active变化 const handleClick = (index: string) => { setActive(index) // onSelect是一个联合类型,可能存在,也可能不存在,对此需要做判断 if (onSelect) { onSelect(index) } } // 点击子组件的时候,触发onSelect函数,更改高亮显示 const passedContext: IMenuContext = { // currentActive是string | undefined类型,index是number类型,所以要做如下判断进一步明确类型 index: currentActive ? currentActive : '0', onSelect: handleClick, // 回调函数,点击子组件时是否触发 mode: mode, defaultOpenSubMenus, } const renderChildren = () => { return React.Children.map(children, (child, index) => { // child里面包含一大堆的类型,要想获得我们想要的类型来提供智能提示,需要使用类型断言 const childElement = child as React.FunctionComponentElement<MenuItemProps> const { displayName } = childElement.type if (displayName === 'MenuItem' || displayName === 'SubMenu') { // 以element元素为样本克隆并返回新的React元素,第一个参数是克隆样本 return React.cloneElement(childElement, { index: index.toString() }) } else { console.error("Warning: Menu has a child which is not a MenuItem component") } }) } return ( <ul className={classes} style={style}> <MenuContext.Provider value={passedContext}> {renderChildren()} </MenuContext.Provider> </ul> ) } Menu.defaultProps = { defaultIndex: '0', mode: 'horizontal', defaultOpenSubMenus: [] } export default Menu
然后是MenuItem子组件: