React Hook的使用示例
這篇文章分享兩個使用React Hook以及函數(shù)式組件開發(fā)的簡單示例。
一個簡單的組件案例
Button組件應(yīng)該算是最簡單的常用基礎(chǔ)組件了吧。我們開發(fā)組件的時候期望它的基礎(chǔ)樣式能有一定程度的變化,這樣就可以適用于不同場景了。第二點是我在之前做項目的時候?qū)懸粋€函數(shù)組件,但這個函數(shù)組件會寫的很死板,也就是上面沒有辦法再綁定基本方法。即我只能寫入我已有的方法,或者特性。希望編寫B(tài)utton組件,即使沒有寫onClick方法,我也希望能夠使用那些自帶的默認(rèn)基本方法。
對于第一點,我們針對不同的className,來寫不同的css,是比較好實現(xiàn)的。
第二點實現(xiàn)起略微困難。我們不能把Button的默認(rèn)屬性全部寫一遍,如果能夠把默認(rèn)屬性全部導(dǎo)入就好了。
事實上,React已經(jīng)幫我們實現(xiàn)了這一點。React.ButtonHTMLAttributes<HTMLElement>里面就包含了默認(rèn)的Button屬性??墒俏覀冇植荒苤苯邮褂眠@個接口,因為我們的Button組件可能還有一些自定義的東西。對此,我們可以使用Typescript的交叉類型
type NativeButtonProps = MyButtonProps & React.ButtonHTMLAttributes<HTMLElement>
此外,我們還需要使用resProps來導(dǎo)入其他非自定義的函數(shù)或?qū)傩浴?/p>
下面是Button組件具體實現(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
通過上面的方式,我們就可以在我們自定義的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,使用它就可以很方便實現(xiàn)className的擴(kuò)展,它的一個簡單使用示例如下:
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中添加個性化的屬性??梢钥吹綄τ诮M件的HTML輸出結(jié)果中有hahaclassName:
<button class="btn haha btn-primary btn-lg">Hello</button>
與此同時,我們上述代碼方式也解決了自定義組件沒有辦法使用默認(rèn)屬性和方法問題。
更復(fù)雜的父子組件案例
接下來我們展示一下如何用函數(shù)組件完成一個菜單功能。這個菜單添加水平模式和垂直模式兩種功能模式。點開某個菜單詳情,將這個詳情作為子組件。
當(dāng)然,菜單這個功能根本就不需要父組件傳數(shù)據(jù)到子組件(子組件指的是菜單詳情),我們?yōu)榱藢W(xué)習(xí)和演示如何將父組件數(shù)據(jù)傳給子組件,強(qiáng)行給他添加這個功能。有點畫蛇添足,大家理解一下就好。
首先介紹父子組件的功能描述。Menu是整體父組件,MenuItem是每一個具體的小菜單,SubMenu里面是可以點開的下拉菜單。

下圖是展開后的樣子:

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

