React自定义Hook之useMutilpleRef
概要
我们在React开发时候,有时候需要绑定列表中的多个元素,便于后面对列表中单个元素的操作,但是常用的hook函数useRef只能绑定一个DOM元素,本文提供一个可以解决该问题的自定义hook方法,useMutilpleRef。
代码及实现
基本功能
本例使用Map来缓存DOM元素的引用,如果DOM元素存在,则缓存在Map中;如果DOM元素被删除,则在Map中对应的键值对也就被删除。
代码实现
export const useMutipleRefs = <T extends HTMLElement>() => {const refs = useRef<Map<string|number, T> |null>(new Map<string|number, T>());const setRef = (key:string) => (el:T | null) => {if (el) {refs.current!.set(key, el);}else {refs.current!.delete(key);}} useEffect(()=>{return ()=> {refs.current = null};}, []);return {refs, setRef};
}
- 该方法不用接受任何参数,采用泛型约束,只处理DOM元素;
- 采用Map来缓存DOM元素,key值可以是字符串,以支持React 18的useId hook函数,也可以是普通数字;
- 该方法的返回值包括两部分,首先是绑定了DOM元素引用的Map,该Map的内容可以被迭代查询,以支持各种复杂的应用场景;
- 返回值的另一部分是setRef 方法,该方法在绑定元素的时候被调用,参数可以接受HTMLElement和空值,空值用于删除的情况;
- 在组件被卸载时候,释放资源。
使用实例
下面是一个人员列表的例子,可以通过性别过滤列表中的元素,每个元素需要支持删除操作。
关键代码如下, 完整代码请见附录:
响应式数据定义如下:
const [gender, setGender] = useState<Gender>(Gender.Female);
const [filteredIds, setFilteredIds] = useState<Array<number|string>>([]);
const userUI = useMemo(()=> users.filter(u=> u.gender === gender && !filteredIds.includes(u.id)), [gender, filteredIds]);
监听内容如下:
- 对当前选中的性别进行监听;
- 对要删除的User进行监听;
- 对列表内容进行缓存,只有选中的性别和要删除的Id数组发生变化,它才变化。
主要的点击事件代码如下:
type funcType = (event:MouseEvent)=>void;
const handleClickGender = (gender:Gender):funcType=>(event:MouseEvent)=> {setGender(gender);
}
const clickDelete = (index:number|string)=> {console.log(index);setFilteredIds(prev=>[...prev, index]);
}
- 性别过滤按钮点击时候,触发handleClickGender
- 删除按钮点击时候,触发clickDelete
Refs 相关的代码
const {refs, setRef} = useMutipleRefs();
useEffect(()=> {console.log(refs.current);
}, [userUI])<tbody>{userUI.map((user, index)=>(<tr key={user.id} ref={setRef(user.id)}><td>{index + 1}</td><td>{user.name}</td><td>{user.gender === Gender.Female ? "Female": "Male"}</td><td>{user.married? "Yes": "No"}</td><td><button className="btn btn-success" onClick={()=>clickDelete(user.id)}>Delete</button></td></tr>))}
</tbody>
- 调用useMutipleRefs过的初始的refs的Map对象和setRef方法
- 对表格中的每个tr,调用setRef,传入用户Id
代码运行
程序默认是获取所有Female的User信息,因此初始时候,有三个User的引用
点击Male按钮,显示有两个Male的User
Map中的数据更新为所有Male的Tr引用。
点击删除第一个Delete按钮
数据成功删除
Map中缓存的DOM引用,也同时被删除
使用中额外注意的问题
本文提供的缓存方法,监听的DOM对象的类型是HTMLElement,它是DOM元素的基类,如果要使用其它DOM的特有属性,请在调用时候,明确具体类型。
例如存的DOM元素是input,并且需要在后面使用input的value,在调用该方法是,请明确Input的的类型,useMutipleRefs(),否则无获取不到其value值。
结论
本文提供的自动Hook useMutipleRefs可以实现对列表中多个元素引用值的缓存,在DOM元素在创建时候,可以同步创建,删除时候可以同步删除。
附录
完整代码
"use client"
import { FC, useEffect, useMemo, useState } from "react";
import { ReactElement, MouseEvent,useId } from "react";
import { usePrevious, useMutipleRefs } from "../hooks";
import "@/app/assets/bootstrap.min.css"
interface IProps{}enum Gender {Male=0,Female
}interface IUser {id:number|string;name:string;gender:Gender;married:boolean;
}const Team:FC<IProps> = ({}):ReactElement => {const users = [{id:useId(),name:"abc",gender:Gender.Female,married:false},{id:useId(),name:"def",gender:Gender.Male,married:false},{id:useId(),name:"ghi",gender:Gender.Female,married:true},{id:useId(),name:"jkl",gender:Gender.Male,married:false},{id:useId(),name:"mno",gender:Gender.Female,married:true}] as Array<IUser>;const [gender, setGender] = useState<Gender>(Gender.Female);type funcType = (event:MouseEvent)=>void;
const handleClickGender = (gender:Gender):funcType=>(event:MouseEvent)=> {setGender(gender);
} const [filteredIds, setFilteredIds] = useState<Array<number|string>>([]);
const userUI = useMemo(()=> users.filter(u=> u.gender === gender && !filteredIds.includes(u.id)), [gender, filteredIds]);const {refs, setRef} = useMutipleRefs();const clickDelete = (index:number|string)=> {console.log(index);setFilteredIds(prev=>[...prev, index]);}useEffect(()=> {console.log(refs.current); }, [userUI])return <><div className="btn-group" role="group" aria-label="Basic outlined example"><button className="btn btn-primary" onClick={handleClickGender(Gender.Female)}>Female</button><button className="btn btn-warning" onClick={handleClickGender(Gender.Male)}>Male</button></div><table className="table table-striped" style={{width:"50%"}}><thead><tr><th>Id</th><th>Name</th><th>Gender</th><th>Married</th><th>Operation</th></tr> </thead><tbody>{userUI.map((user, index)=>(<tr key={user.id} ref={setRef(user.id)}><td>{index + 1}</td><td>{user.name}</td><td>{user.gender === Gender.Female ? "Female": "Male"}</td><td>{user.married? "Yes": "No"}</td><td><button className="btn btn-success" onClick={()=>clickDelete(user.id)}>Delete</button></td></tr>))}</tbody></table></>
}export default Team;