最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React Hook简单使用案例

    正文概述 掘金(鹏厂搬砖工)   2021-04-05   565

    这篇文章分享两个使用React Hook以及函数式组件开发的简单示例。

    一个简单的组件案例

    Button组件应该算是最简单的常用基础组件了吧。我们开发组件的时候期望它的基础样式能有一定程度的变化,这样就可以适用于不同场景了。第二点是我在之前做项目的时候写一个函数组件,但这个函数组件会写的很死板,也就是上面没有办法再绑定基本方法。即我只能写入我已有的方法,或者特性。希望编写Button组件,即使没有写onClick方法,我也希望能够使用那些自带的默认基本方法。

    对于第一点,我们针对不同的className,来写不同的css,是比较好实现的。

    第二点实现起略微困难。我们不能把Button的默认属性全部写一遍,如果能够把默认属性全部导入就好了。

    事实上,React已经帮我们实现了这一点。React.ButtonHTMLAttributes<HTMLElement>里面就包含了默认的Button属性。可是我们又不能直接使用这个接口,因为我们的Button组件可能还有一些自定义的东西。对此,我们可以使用Typescript的交叉类型

    type NativeButtonProps = MyButtonProps & React.ButtonHTMLAttributes<HTMLElement>
    

    此外,我们还需要使用resProps来导入其他非自定义的函数或属性。

    下面是Button组件具体实现方案:

    import React from 'react'
    import classNames from 'classnames'
    
    type ButtonSize = 'large' | 'small'
    type ButtonType = 'primary' | 'default' | 'danger'
    
    interface BaseButtonProps {
      className?: string;
      disabled?: boolean;
      size?: ButtonSize;
      btnType?: ButtonType;
      children?: React.ReactNode;
    }
    
    type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLElement>
    const Button: React.FC<NativeButtonProps>= (props) => {
      const {
        btnType,
        className,
        disabled,
        size,
        children,
        // resProps用于取出所有剩余属性
        ...resProps
      } = props
      // btn, btn-lg, btn-primary
      const classes = classNames('btn', className, {
        [`btn-${btnType}`]: btnType,
        [`btn-${size}`]: size,
        'disabled': disabled
      })
      return (
        <button
          className={classes}
          disabled={disabled}
          {...resProps}
        >
          {children}
        </button>
      )
    }
    
    Button.defaultProps = {
      disabled: false,
      btnType: 'default'
    }
    
    export default Button
    

    通过上面的方式,我们就可以在我们自定义的Button组件中使用比如onClick方法了。使用Button组件案例如下:

    <Button disabled>Hello</Button>
    <Button btnType='primary' size='large' className="haha">Hello</Button>
    <Button btnType='danger' size='small' onClick={() => alert('haha')}>Test</Button>
    

    展示效果如下:

    React Hook简单使用案例

    在这个代码中我们引入了一个新的npm package称之为classnames,具体使用方式可以参考GitHub Classnames,使用它就可以很方便实现className的扩展,它的一个简单使用示例如下:

    classNames('foo', 'bar'); // => 'foo bar'
    classNames('foo', { bar: true }); // => 'foo bar'
    classNames({ 'foo-bar': true }); // => 'foo-bar'
    classNames({ 'foo-bar': false }); // => ''
    classNames({ foo: true }, { bar: true }); // => 'foo bar'
    classNames({ foo: true, bar: true }); // => 'foo bar'
    
    // lots of arguments of various types
    classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
    
    // other falsy values are just ignored
    classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'
    

    通过使用classNames,就可以很方便的在Button中添加个性化的属性。可以看到对于组件的HTML输出结果中有hahaclassName:

    <button class="btn haha btn-primary btn-lg">Hello</button>
    

    与此同时,我们上述代码方式也解决了自定义组件没有办法使用默认属性和方法问题。

    更复杂的父子组件案例

    接下来我们展示一下如何用函数组件完成一个菜单功能。这个菜单添加水平模式和垂直模式两种功能模式。点开某个菜单详情,将这个详情作为子组件。

    当然,菜单这个功能根本就不需要父组件传数据到子组件(子组件指的是菜单详情),我们为了学习和演示如何将父组件数据传给子组件,强行给他添加这个功能。有点画蛇添足,大家理解一下就好。

    首先介绍父子组件的功能描述。Menu是整体父组件,MenuItem是每一个具体的小菜单,SubMenu里面是可以点开的下拉菜单。

    React Hook简单使用案例

    下图是展开后的样子:

    React Hook简单使用案例

    整体代码结构如下:

    <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 >
        <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.map

    为什么我们会需要使用React.Children呢?是因为如果涉及到父组件数据传递到子组件时,可能需要对子组件进行二次遍历或者进一步处理。但是我们不能保证子组件是到底有没有,是一个还是两个或者多个。

    所以,如果有父子组件的话,如果需要进一步处理子组件的时候,我们可以使用React.Children来遍历,这样不会因为this.props.children类型变化而出错。

    React.cloneElement

    React.Children出现时往往可能伴随着React.cloneElement一起出现。因此,我们也需要介绍一下React.cloneElement。

    例如,有的时候我们需要对子元素做进一步处理,但因为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子组件:

    import React from 'react'
    import { useContext } from 'react'
    import classNames from 'classnames'
    import { MenuContext } from './menu'
    
    export interface MenuItemProps {
      index: string;
      disabled?: boolean;
      className?: string;
      style?: React.CSSProperties;
    }
    
    const MenuItem: React.FC<MenuItemProps> = (props) => {
      const { index, disabled, className, style, children } = props
      const context = useContext(MenuContext)
      const classes = classNames('menu-item', className, {
        'is-disabled': disabled,
        // 实现高亮的具体逻辑
        'is-active': context.index === index
      })
      const handleClick = () => {
        // disabled之后就不能使用onSelect,index因为是可选的,所以可能不存在,需要用typeof来做一个判断
        if (context.onSelect && !disabled && (typeof index === 'string')) {
          context.onSelect(index)
        }
      }
      return (
        <li className={classes} style={style} onClick={handleClick}>
          {children}
        </li>
      )
    }
    
    MenuItem.displayName = 'MenuItem'
    export default MenuItem
    

    最后是SubMenu子组件:

    import React, { useContext, FunctionComponentElement, useState } from 'react'
    import classNames from 'classnames'
    import { MenuContext } from './menu'
    import { MenuItemProps } from './menuItem'
    
    export interface SubMenuProps {
      index?: string;
      title: string;
      className?: string
    }
    
    const SubMenu: React.FC<SubMenuProps> = ({ index, title, children, className }) => {
      const context = useContext(MenuContext)
      // 接下来会使用string数组的一些方法,所以先进行类型断言,将其断言为string数组类型
      const openedSubMenus = context.defaultOpenSubMenus as Array<string>
      // 使用include判断有没有index
      const isOpened = (index && context.mode === 'vertical') ? openedSubMenus.includes(index) : false
      const [ menuOpen, setOpen ] = useState(isOpened)  // isOpened返回的会是true或者false,这样就是一个动态值
      const classes = classNames('menu-item submenu-item', className, {
        'is-active': context.index === index
      })
      // 用于实现显示或隐藏下拉菜单
      const handleClick = (e: React.MouseEvent) => {
        e.preventDefault()
        setOpen(!menuOpen)
      }
      let timer: any
      // toggle用于判断是打开还是关闭
      const handleMouse = (e: React.MouseEvent, toggle: boolean) => {
        clearTimeout(timer)
        e.preventDefault()
        timer = setTimeout(()=> {
          setOpen(toggle)
        }, 300)
      }
      // 三元表达式,纵向
      const clickEvents = context.mode === 'vertical' ? {
        onClick: handleClick
      } : {}
      const hoverEvents = context.mode === 'horizontal' ? {
        onMouseEnter: (e: React.MouseEvent) => { handleMouse(e, true) },
        onMouseLeave: (e: React.MouseEvent) => { handleMouse(e, false) },
      } : {}
    
      // 用于渲染下拉菜单中的内容
      // 返回两个值,第一个是child,第二个是index,用i表示
      const renderChildren = () => {
        const subMenuClasses = classNames('menu-submenu', {
          'menu-opened': menuOpen
        })
        // 下面功能用于实现在subMenu里只能有MenuItem
        const childrenComponent = React.Children.map(children, (child, i) => {
          const childElement = child as FunctionComponentElement<MenuItemProps>
          if (childElement.type.displayName === 'MenuItem') {
            return React.cloneElement(childElement, {
              index: `${index}-${i}`
            })
          } else {
            console.error("Warning: SubMenu has a child which is not a MenuItem component")
          }
        })
        return (
          <ul className={subMenuClasses}>
            {childrenComponent}
          </ul>
        )
      }
      return (
        // 展开运算符,向里面添加功能,hover放在外面
        <li key={index} className={classes} {...hoverEvents}>
          <div className="submenu-title" {...clickEvents}>
            {title}
          </div>
          {renderChildren()}
        </li>
      )
    }
    
    SubMenu.displayName = 'SubMenu'
    export default SubMenu
    
    参考资料
    1. React.Children的用法
    2. React.cloneElement 的使用

    起源地下载网 » React Hook简单使用案例

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元