React Hook的使用示例
這篇文章分享兩個(gè)使用React Hook以及函數(shù)式組件開發(fā)的簡(jiǎn)單示例。
一個(gè)簡(jiǎn)單的組件案例
Button組件應(yīng)該算是最簡(jiǎn)單的常用基礎(chǔ)組件了吧。我們開發(fā)組件的時(shí)候期望它的基礎(chǔ)樣式能有一定程度的變化,這樣就可以適用于不同場(chǎng)景了。第二點(diǎn)是我在之前做項(xiàng)目的時(shí)候?qū)懸粋€(gè)函數(shù)組件,但這個(gè)函數(shù)組件會(huì)寫的很死板,也就是上面沒有辦法再綁定基本方法。即我只能寫入我已有的方法,或者特性。希望編寫B(tài)utton組件,即使沒有寫onClick方法,我也希望能夠使用那些自帶的默認(rèn)基本方法。
對(duì)于第一點(diǎn),我們針對(duì)不同的className,來(lái)寫不同的css,是比較好實(shí)現(xiàn)的。
第二點(diǎn)實(shí)現(xiàn)起略微困難。我們不能把Button的默認(rèn)屬性全部寫一遍,如果能夠把默認(rèn)屬性全部導(dǎo)入就好了。
事實(shí)上,React已經(jīng)幫我們實(shí)現(xiàn)了這一點(diǎn)。React.ButtonHTMLAttributes<HTMLElement>里面就包含了默認(rèn)的Button屬性??墒俏覀冇植荒苤苯邮褂眠@個(gè)接口,因?yàn)槲覀兊腂utton組件可能還有一些自定義的東西。對(duì)此,我們可以使用Typescript的交叉類型
type NativeButtonProps = MyButtonProps & React.ButtonHTMLAttributes<HTMLElement>
此外,我們還需要使用resProps來(lái)導(dǎo)入其他非自定義的函數(shù)或?qū)傩浴?/p>
下面是Button組件具體實(shí)現(xiàn)方案:
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
通過(guò)上面的方式,我們就可以在我們自定義的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>
展示效果如下:

在這個(gè)代碼中我們引入了一個(gè)新的npm package稱之為classnames,具體使用方式可以參考GitHub Classnames,使用它就可以很方便實(shí)現(xiàn)className的擴(kuò)展,它的一個(gè)簡(jiǎn)單使用示例如下:
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'
通過(guò)使用classNames,就可以很方便的在Button中添加個(gè)性化的屬性。可以看到對(duì)于組件的HTML輸出結(jié)果中有hahaclassName:
<button class="btn haha btn-primary btn-lg">Hello</button>
與此同時(shí),我們上述代碼方式也解決了自定義組件沒有辦法使用默認(rèn)屬性和方法問(wèn)題。
更復(fù)雜的父子組件案例
接下來(lái)我們展示一下如何用函數(shù)組件完成一個(gè)菜單功能。這個(gè)菜單添加水平模式和垂直模式兩種功能模式。點(diǎn)開某個(gè)菜單詳情,將這個(gè)詳情作為子組件。
當(dāng)然,菜單這個(gè)功能根本就不需要父組件傳數(shù)據(jù)到子組件(子組件指的是菜單詳情),我們?yōu)榱藢W(xué)習(xí)和演示如何將父組件數(shù)據(jù)傳給子組件,強(qiáng)行給他添加這個(gè)功能。有點(diǎn)畫蛇添足,大家理解一下就好。
首先介紹父子組件的功能描述。Menu是整體父組件,MenuItem是每一個(gè)具體的小菜單,SubMenu里面是可以點(diǎn)開的下拉菜單。

下圖是展開后的樣子:

整體代碼結(jié)構(gòu)如下:
<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>
在這個(gè)組件中,我們用到了useState,另外因?yàn)樯婕案附M件傳數(shù)據(jù)到子組件,所以還用到了useContext(父組件數(shù)據(jù)傳遞到子組件是指的父組件的index數(shù)據(jù)傳遞到子組件)。另外,我們還會(huì)演示使用自定義的onSelect來(lái)實(shí)現(xiàn)onClick功能(萬(wàn)一你引入React泛型不成功,或者不知道該引入哪個(gè)React泛型,還可以用自定義的補(bǔ)救一下)。
如何寫onSelect
為了防止后面在代碼的汪洋大海中難以找到onSelect,這里先簡(jiǎn)單的抽出來(lái)做一個(gè)onSelect書寫示例。比如我們?cè)贛enu組件中使用onSelect,它的使用方式和onClick看起來(lái)是一樣的:
<Menu onSelect={(index) => {alert(index)}}>
在具體這個(gè)Menu組件中具體使用onSelect可以這樣寫:
type SelectCallback = (selectedIndex: string) => void
interface MenuProps {
onSelect?: SelectCallback;
}
實(shí)現(xiàn)handleClick的方法可以寫成這樣:
const handleClick = (index: string) => {
// onSelect是一個(gè)聯(lián)合類型,可能存在,也可能不存在,對(duì)此需要做判斷
if (onSelect) {
onSelect(index)
}
}
到時(shí)候要想把這個(gè)onSelect傳遞給子組件時(shí),使用onSelect: handleClick綁定一下就好。(可能你沒看太懂,我也不知道該咋寫,后面會(huì)有整體代碼分析,可能聯(lián)合起來(lái)看會(huì)比較容易理解)
React.Children
在講解具體代碼之前,還要再說(shuō)說(shuō)幾個(gè)小知識(shí)點(diǎn),其中一個(gè)是React.Children。
React.Children 提供了用于處理 this.props.children 不透明數(shù)據(jù)結(jié)構(gòu)的實(shí)用方法。
為什么我們會(huì)需要使用React.Children呢?是因?yàn)槿绻婕暗礁附M件數(shù)據(jù)傳遞到子組件時(shí),可能需要對(duì)子組件進(jìn)行二次遍歷或者進(jìn)一步處理。但是我們不能保證子組件是到底有沒有,是一個(gè)還是兩個(gè)或者多個(gè)。
this.props.children 的值有三種可能:如果當(dāng)前組件沒有子節(jié)點(diǎn),它就是 undefined ;如果有一個(gè)子節(jié)點(diǎn),數(shù)據(jù)類型是 object ;如果有多個(gè)子節(jié)點(diǎn),數(shù)據(jù)類型就是 array 。所以,處理 this.props.children 的時(shí)候要小心[1]。
React 提供一個(gè)工具方法 React.Children 來(lái)處理 this.props.children 。我們可以用 React.Children.map 來(lái)遍歷子節(jié)點(diǎn),而不用擔(dān)心 this.props.children 的數(shù)據(jù)類型是 undefined 還是 object[1]。
所以,如果有父子組件的話,如果需要進(jìn)一步處理子組件的時(shí)候,我們可以使用React.Children來(lái)遍歷,這樣不會(huì)因?yàn)閠his.props.children類型變化而出錯(cuò)。
React.cloneElement
React.Children出現(xiàn)時(shí)往往可能伴隨著React.cloneElement一起出現(xiàn)。因此,我們也需要介紹一下React.cloneElement。
在開發(fā)復(fù)雜組件中,經(jīng)常會(huì)根據(jù)需要給子組件添加不同的功能或者顯示效果,react 元素本身是不可變的 (immutable) 對(duì)象, props.children 事實(shí)上并不是 children 本身,它只是 children 的描述符 (descriptor) ,我們不能修改任何它的任何屬性,只能讀到其中的內(nèi)容,因此 React.cloneElement 允許我們拷貝它的元素,并且修改或者添加新的 props 從而達(dá)到我們的目的[2]。
例如,有的時(shí)候我們需要對(duì)子元素做進(jìn)一步處理,但因?yàn)镽eact元素本身是不可變的,所以,我們需要對(duì)其克隆一份再做進(jìn)一步處理。在這個(gè)Menu組件中,我們希望它的子組件只能是MenuItem或者是SubMenu兩種類型,如果是其他類型就會(huì)報(bào)警告信息。具體來(lái)說(shuō),可以大致將代碼寫成這樣:
if (displayName === 'MenuItem' || displayName === 'SubMenu') {
// 以element元素為樣本克隆并返回新的React元素,第一個(gè)參數(shù)是克隆樣本
return React.cloneElement(childElement, {
index: index.toString()
})
} else {
console.error("Warning: Menu has a child which is not a MenuItem component")
}
父組件數(shù)據(jù)如何傳遞給子組件
通過(guò)使用Context來(lái)實(shí)現(xiàn)父組件數(shù)據(jù)傳遞給子組件。如果對(duì)Context不太熟悉的話,可以參考官方文檔,Context,在父組件中我們通過(guò)createContext來(lái)創(chuàng)建Context,在子組件中通過(guò)useContext來(lái)獲取Context。
index數(shù)據(jù)傳遞
Menu組件中實(shí)現(xiàn)父子組件中數(shù)據(jù)傳遞變量主要是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; // 用于哪個(gè)menu子組件是高亮顯示
className?: string;
mode?: MenuMode;
style?: React.CSSProperties;
onSelect?: SelectCallback; // 點(diǎn)擊子菜單時(shí)可以觸發(fā)回調(diào)
defaultOpenSubMenus?: string[];
}
// 確定父組件傳給子組件的數(shù)據(jù)類型
interface IMenuContext {
index: string;
onSelect?: SelectCallback;
mode?: MenuMode;
defaultOpenSubMenus?: string[]; // 需要將數(shù)據(jù)傳給context
}
// 創(chuàng)建傳遞給子組件的context
// 泛型約束,因?yàn)閕ndex是要輸入的值,所以這里寫一個(gè)默認(rèn)初始值
export const MenuContext = createContext<IMenuContext>({index: '0'})
const Menu: React.FC<MenuProps> = (props) => {
const { className, mode, style, children, defaultIndex, onSelect, defaultOpenSubMenus} = props
// MenuItem處于active的狀態(tài)應(yīng)該是有且只有一個(gè)的,使用useState來(lái)控制其狀態(tài)
const [ currentActive, setActive ] = useState(defaultIndex)
const classes = classNames('menu-demo', className, {
'menu-vertical': mode === 'vertical',
'menu-horizontal': mode === 'horizontal'
})
// 定義handleClick具體實(shí)現(xiàn)點(diǎn)擊menuItem之后active變化
const handleClick = (index: string) => {
setActive(index)
// onSelect是一個(gè)聯(lián)合類型,可能存在,也可能不存在,對(duì)此需要做判斷
if (onSelect) {
onSelect(index)
}
}
// 點(diǎn)擊子組件的時(shí)候,觸發(fā)onSelect函數(shù),更改高亮顯示
const passedContext: IMenuContext = {
// currentActive是string | undefined類型,index是number類型,所以要做如下判斷進(jìn)一步明確類型
index: currentActive ? currentActive : '0',
onSelect: handleClick, // 回調(diào)函數(shù),點(diǎn)擊子組件時(shí)是否觸發(fā)
mode: mode,
defaultOpenSubMenus,
}
const renderChildren = () => {
return React.Children.map(children, (child, index) => {
// child里面包含一大堆的類型,要想獲得我們想要的類型來(lái)提供智能提示,需要使用類型斷言
const childElement = child as React.FunctionComponentElement<MenuItemProps>
const { displayName } = childElement.type
if (displayName === 'MenuItem' || displayName === 'SubMenu') {
// 以element元素為樣本克隆并返回新的React元素,第一個(gè)參數(shù)是克隆樣本
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,
// 實(shí)現(xiàn)高亮的具體邏輯
'is-active': context.index === index
})
const handleClick = () => {
// disabled之后就不能使用onSelect,index因?yàn)槭强蛇x的,所以可能不存在,需要用typeof來(lái)做一個(gè)判斷
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)
// 接下來(lái)會(huì)使用string數(shù)組的一些方法,所以先進(jìn)行類型斷言,將其斷言為string數(shù)組類型
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返回的會(huì)是true或者false,這樣就是一個(gè)動(dòng)態(tài)值
const classes = classNames('menu-item submenu-item', className, {
'is-active': context.index === index
})
// 用于實(shí)現(xiàn)顯示或隱藏下拉菜單
const handleClick = (e: React.MouseEvent) => {
e.preventDefault()
setOpen(!menuOpen)
}
let timer: any
// toggle用于判斷是打開還是關(guān)閉
const handleMouse = (e: React.MouseEvent, toggle: boolean) => {
clearTimeout(timer)
e.preventDefault()
timer = setTimeout(()=> {
setOpen(toggle)
}, 300)
}
// 三元表達(dá)式,縱向
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) },
} : {}
// 用于渲染下拉菜單中的內(nèi)容
// 返回兩個(gè)值,第一個(gè)是child,第二個(gè)是index,用i表示
const renderChildren = () => {
const subMenuClasses = classNames('menu-submenu', {
'menu-opened': menuOpen
})
// 下面功能用于實(shí)現(xiàn)在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 (
// 展開運(yùn)算符,向里面添加功能,hover放在外面
<li key={index} className={classes} {...hoverEvents}>
<div className="submenu-title" {...clickEvents}>
{title}
</div>
{renderChildren()}
</li>
)
}
SubMenu.displayName = 'SubMenu'
export default SubMenu
參考資料
以上就是React Hook的使用示例的詳細(xì)內(nèi)容,更多關(guān)于React Hook的使用的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解react-router 4.0 下服務(wù)器如何配合BrowserRouter
這篇文章主要介紹了詳解react-router 4.0 下服務(wù)器如何配合BrowserRouter,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12
關(guān)于React動(dòng)態(tài)加載路由處理的相關(guān)問(wèn)題
這篇文章主要介紹了關(guān)于React動(dòng)態(tài)加載路由處理的相關(guān)問(wèn)題,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-01-01
antd?table動(dòng)態(tài)修改表格高度的實(shí)現(xiàn)
本文主要介紹了antd?table動(dòng)態(tài)修改表格高度的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07
React使用react-sortable-hoc如何實(shí)現(xiàn)拖拽效果
這篇文章主要介紹了React使用react-sortable-hoc如何實(shí)現(xiàn)拖拽效果問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07
React-vscode使用jsx語(yǔ)法的問(wèn)題及解決方法
很多朋友在安裝插件ES7 React/Redux/GraphQL/React-Native snippets還是不能完全支持jsx語(yǔ)法,糾結(jié)是什么原因呢,該如何處理呢,下面小編給大家分享本文幫助大家解決React-vscode使用jsx語(yǔ)法問(wèn)題,感興趣的朋友一起看看吧2021-06-06
react?hooks?計(jì)數(shù)器實(shí)現(xiàn)代碼
這篇文章主要介紹了react?hooks計(jì)數(shù)器實(shí)現(xiàn)代碼,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-08-08
react-router-dom入門使用教程(前端路由原理)
這篇文章主要介紹了react-router-dom入門使用教程,主要包括react路由相關(guān)理解,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08
React中的useEffect useLayoutEffect到底怎么用
這篇文章主要介紹了React中的useEffect useLayoutEffect具體使用方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2023-02-02
react-native動(dòng)態(tài)切換tab組件的方法
在APP中免不了要使用tab組件,有的是tab切換,也有的是tab分類切換.這篇文章主要介紹了react-native動(dòng)態(tài)切換tab組件的方法,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2018-07-07

