这篇文章分享两个使用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的使用的资料请关注Devmax其它相关文章!

React Hook的使用示例的更多相关文章

  1. ios – React native链接到另一个应用程序

    如果是错误的,有人知道如何调用正确的吗?

  2. ios – React Native – 在异步操作后导航

    我正在使用ReactNative和Redux开发移动应用程序,我正面临着软件设计问题.我想调用RESTAPI进行登录,如果该操作成功,则导航到主视图.我正在使用redux和thunk所以我已经实现了异步操作,所以我的主要疑问是:我应该把逻辑导航到主视图?我可以直接从动作访问导航器对象并在那里执行导航吗?.我对组件中的逻辑没有信心.似乎不是一个好习惯.有没有其他方法可以做到这一点?

  3. 在ios中使用带有React Native(0.43.4)的cocoapods的正确方法是什么?

    我已经挖掘了很多帖子试图使用cocoapods为本地ios库设置一个反应原生项目,但我不可避免地在#import中找到了丢失文件的错误.我的AppDelegate.m文件中的语句.什么是使用反应原生的可可豆荚的正确方法?在这篇文章发表时,我目前的RN版本是0.43.4,而我正在使用Xcode8.2.1.这是我的过程,好奇我可能会出错:1)

  4. ios – React Native WebView滚动行为无法按预期工作

    如何确保滚动事件的行为与ReactNative应用程序中的浏览器相同?

  5. ios – React Native – BVLinearGradient – 找不到’React/RCTViewManager.h’文件

    谢谢.解决方法几天前我遇到了完全相同的问题.问题是在构建应用程序时React尚未链接.试试这个:转到Product=>Scheme=>管理方案…=>点击你的应用程序Scheme,然后点击Edit=>转到Build选项卡=>取消选中ParallelizeBuild然后点击标志添加目标=>搜索React,选择第一个名为React的目标,然后单击Add然后在目标列表中选择React并将其向上拖动到该列表中的第一个.然后转到Product=>再次清理并构建项目.这应该有所帮助.

  6. ios – React Native – NSNumber无法转换为NSString

    解决方法在你的fontWeight()函数中也许变成:

  7. ios – React native error – react-native-xcode.sh:line 45:react-native:command not found命令/ bin/sh失败,退出代码127

    尝试构建任何(新的或旧的)项目时出现此错误.我的节点是版本4.2.1,react-native是版本0.1.7.我看过其他有相同问题的人,所以我已经更新了本机的最新版本,但是我仍然无法通过xcode构建任何项目.解决方法要解决此问题,请使用以下步骤:>使用节点版本v4.2.1>cd进入[你的应用]/node_modules/react-native/packager>$sh./packager.s

  8. 反应原生 – 如何通过Xcode构建React Native iOS应用程序到设备?

    我试图将AwesomeProject应用程序构建到设备上.构建成功并启动屏幕显示,但后来我看到一个红色的“无法连接到开发服务器”屏幕.它表示“确保节点服务器正在运行–从Reactroot运行”npmstart“.看起来节点服务器已经运行,因为当我做npm启动时,我收到一个EADDRINUSE消息,表示该端口已经在使用.解决方法从设备访问开发服务器您可以使用开发服务器快速迭代设备.要做到这一点,你的

  9. 静音iOS推送通知与React Native应用程序在后台

    我有一个ReactNative应用程序,我试图获得一个发送到JavaScript处理程序的静默iOS推送通知.我看到的行为是AppDelegate中的didReceiveRemoteNotification函数被调用,但是我的JavaScript中的处理程序不会被调用,除非应用程序在前台,或者最近才被关闭.我很困惑的事情显然是应用程序正在被唤醒,并且它的didReceiveRemoteNotifi

  10. 如何为iOS的React Native设置分析

    所以我已经完成了一个针对iOS的ReactNative项目,但是我想在其中分析.我尝试了react-native-google-analytics软件包,但是问题阻止了它的正常工作.此外,react-native-cordova-plugin软件包只适用于Android,因此插入Cordova插件进行分析的能力现在已成为问题.我也没有Swift/ObjectiveC的经验,所以将完全失去GA的插入.有没有人有任何建议如何连接GoogleAnalytics的ReactNativeforiOS?

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部