React Hook的使用示例

这篇文章分享两个使用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>

展示效果如下:

在这个代码中我们引入了一个新的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里面是可以点开的下拉菜单。

下图是展开后的样子:

整体代码结构如下:

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

React.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子组件:

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

参考资料

  • React.Children的用法
  • React.cloneElement 的使用

以上就是React Hook的使用示例的详细内容,更多关于React Hook的使用的资料请关注我们其它相关文章!

(0)

相关推荐

  • 基于react hooks,zarm组件库配置开发h5表单页面的实例代码

    最近使用React Hooks结合zarm组件库,基于js对象配置方式开发了大量的h5表单页面.大家都知道h5表单功能无非就是表单数据的收集,验证,提交,回显编辑,通常排列方式也是自上向下一行一列的方式显示 , 所以一开始就考虑封装一个配置化的页面生成方案,目前已经有多个项目基于此方式配置开发上线,思路和实现分享一下. 使用场景 任意包含表单的h5页面(使用zarm库,或自行适配自己的库) 目标 代码实现简单和简洁 基于配置 新手上手快,无学习成本 老手易扩展和维护 写之前参考了市面上的一些方案

  • react中常见hook的使用方式

    1.什么是hook? react hook是react 16.8推出的方法,能够让函数式组件像类式组件一样拥有state.ref.生命周期等属性. 2.为什么要出现hook? 函数式组件是全局当中一个普通函数,在非严格模式下this指向window,但是react内部开启了严格模式,此时this指向undefined,无法像类式组件一样使用state.ref,函数式组件定义的变量都是局部的,当组件进行更新时会重新定义,也无法存储,所以在hook出现之前,函数式组件有很大的局限性,通常情况下都会使

  • react hooks入门详细教程

    State Hooks 案例: import { useState } from 'react'; function Example() { const [count, setCount] = useState(0); //count:声明的变量:setCount:改变count值的函数:0:count的初始值 return ( <div> <p>You clicked {count} times</p> <button onClick={() => set

  • 使用hooks写React组件需要注意的5个地方

    Hook是React16.8开始新增的特性.虽然React官方文档已经作出了针对React hooks的相关概念的讲解,但是光看官方文档是很难将hooks使用好的,在编写hooks的过程中很容易跳进陷阱和错误.本文总结了5个不好的地方. 01.不需要render的场景下使用useState 在函数组件中我们可以使用useState来管理状态,这使得对状态的管理变得很简单,但是也容易被滥用,我们通过下面的代码样例看下容易忽略的地方. 不推荐× function ClickButton(props)

  • React Hooks 实现和由来以及解决的问题详解

    与React类组件相比,React函数式组件究竟有何不同? 一般的回答都是: 类组件比函数式组件多了更多的特性,比如 state,那如果有 Hooks 之后呢? 函数组件性能比类组件好,但是在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别. 性能主要取决于代码的作用,而不是选择函数式还是类组件.尽管优化策略有差别,但性能差异可以忽略不计. 参考官网:(https://zh-hans.reactjs.org/docs/hooks-faq.html#are-hooks-slow-b

  • react的hooks的用法详解

    hooks的作用 它改变了原始的React类的开发方式,改用了函数形式;它改变了复杂的状态操作形式,让程序员用起来更轻松;它改变了一个状态组件的复用性,让组件的复用性大大增加. useState // 声明状态 const [ count , setCount ] = useState(0); // 使用状态 <p>You clicked {count} times</p> <button onClick={()=>{setCount(count+1)}}>cli

  • 详解如何使用React Hooks请求数据并渲染

    前言 在日常的开发中,从服务器端异步获取数据并渲染是相当高频的操作.在以往使用React Class组件的时候,这种操作我们已经很熟悉了,即在Class组件的componentDidMount中通过ajax来获取数据并setState,触发组件更新. 随着Hook的到来,我们可以在一些场景中使用Hook的写法来替代Class的写法.但是Hook中没有setState.componentDidMount等函数,又如何做到从服务器端异步获取数据并渲染呢?本文将会介绍如何使用React的新特性Hook

  • React 使用Hooks简化受控组件的状态绑定

    开始之前 阅读本文需要对以下几项有一定了解 ECMAScript 6 文章中大量用到了 ES6 语法,比如解构赋值和函数参数默认值.剩余参数.展开语法.箭头函数等. Hooks React 在 16.8 版本中推出了 Hooks,它允许你在"函数组件"中使用"类组件"的一些特性. React 本身提供了一些 Hooks,比如 useState.useReducer 等.通过在一个以"use"作为命名起始的函数中调用这些 Hooks,就得到了一个

  • 一百多行代码实现react拖拽hooks

    前言 源码总共也就一百多行,看完这个大致可以理解一些成熟的react拖拽库的实现思路,比如react-dnd,然后你上手这些库的时候就非常快了. 使用hooks实现的大致效果动图如下: 我们的目标是实现一个useDrag和useDrop的hooks,类似以下用法就可以轻松让元素可以拖拽,并且在拖拽的各个生命周期,如下,可以自定义传递消息(顺便介绍几个拖拽会触发的事件). dragstart:用户开始拖拉时,在被拖拉的节点上触发,该事件的target属性是被拖拉的节点. dragenter:拖拉进

  • 如何对react hooks进行单元测试的方法

    写在前面 使用 react hook 来做公司的新项目有一段时间了,大大小小的坑踩了不少.由于是公司项目,因此必须要编写单元测试来确保业务逻辑的正确性以及重构时代码的可维护性与稳定性,之前的项目使用的是 react@15.x 的版本,使用 enzyme 配合 jest 来做单元测试毫无压力,但新项目使用的是 react@16.8 ,编写单元测试的时候,遇到不少阻碍,因此总结此篇文章算作心得分享出来. 配合 enzyme 来进行测试 首先,enzyme 对于 hook 的支持程度,可以参考这个 i

  • react中hook介绍以及使用教程

    前言 最近由于公司的项目开发,就学习了在react关于hook的使用,对其有个基本的认识以及如何在项目中去应用hook.在这篇博客中主要从以下的几个点进行介绍: hook简介 hook中常用api的使用 hook在使用过程中需要去注意的地方 hook中怎样去实现class组件中的声明周期函数 hook 首先介绍关于hook的含义,以及其所要去面对的一些场景 含义:Hook 是 React 16.8 的新增特性.它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性

随机推荐