React 组件封装思路
React组件封装以及Hook的综合运用
前言
React使用JSX编写UI组件,在React中,通过组件概念的封装,可以保证页面代码的复用性同时更好去维护,提高开发效率。
基于Props和ref去封装组件
在我们的页面开发中这种封装思路应该是最常见的。
父组件通过传入Props从而控制子组件的行为和表现。同时,子组件也可以借由父组件传入的ref将自己的部分内容暴露给父组件进行访问
// 父组件
import { useState, useRef } from'react';
import ChildComponent from './ChildComponent';
function parentComponent(){
const [count,setCount] = useState(1);
const childRef = useRef(null);
function letChildSay(){
childRef.current.childSay();
}
return (
<>
<button onClick={letChildSay}>childSay</button>
<ChildComponent ref={childRef} count={count} />
</>
)
}
// 子组件
import { useRef,forwardRef,useImperativeHandle } from'react';
function ChildComponent({count},ref){
const childRef = useRef(null);
useImperativeHandle(ref,()=>({
childSay(){
console.log('child say')
}
}))
return <div>{count}</div>
}
export default forwardRef(ChildComponent);
但这样做有个弊端,我们的Props和ref是需要一级级传递并且需要自己去一级级去管理传递。
面对动态嵌套组件,参数的传递变得十分困难。由此引出Context去帮助我们去解决这个痛点。
基于Context
import { createContext, useContext } from 'react';
interface IDrawerContext {
drawerVisible: boolean;
drawerToggle: () => void;
drawerClose: () => void;
drawerOpen: () => void;
}
const DrawerContext = createContext<IDrawerContext | null>(null);
function useDrawerContext() {
const context = useContext(DrawerContext);
if (!context) {
throw new Error('useDrawerContext must be used within a DrawerProvider');
}
return context;
}
export { DrawerContext, useDrawerContext };
我们在一个单独的文件中定义了一个DrawerContext和useDrawerContext方法。useDrawerContext是一个hook,它的作用是检测当前上下文是否存在DrawerContext,如果不存在则抛出一个错误。在TS项目中这个hook能够帮我们解决访问Context时TS类型推导存在undefined的问题。
import useToggle from '@/Hooks/state/useToggle';
import { useDrawerContext, DrawerContext } from '@/service/context/Drawer';
function Drawer({ children }: { children: React.ReactNode }) {
const [drawerVisible, { toggle: drawerToggle, setDefault: drawerClose, setReverse: drawerOpen }] = useToggle<boolean, boolean>(false, true);
return (
<DrawerContext.Provider
value={{
drawerVisible,
drawerToggle,
drawerClose,
drawerOpen
}}>
<div className="drawer drawer-end">
<input type="checkbox" className="drawer-toggle" checked={drawerVisible} onChange={drawerToggle} />
{children}
</div>
</DrawerContext.Provider>
);
}
Drawer.PageContent = function DrawerPageContent({ children }: { children: React.ReactNode }) {
return <div className="drawer-content">{children}</div>;
};
Drawer.Content = function DrawerContent({ children }: { children: React.ReactNode }) {
const { drawerToggle } = useDrawerContext();
return (
<div className="drawer-side">
<label onClick={drawerToggle} className="drawer-overlay"></label>
<div className="menu min-h-full w-[60%] bg-nav text-main md:w-80 md:p-4">{children}</div>
</div>
);
};
export default Drawer;
这是一个典型的抽屉UI组件,PageContent和Content分别是主页面的内容和抽屉容器内容。我们将Drawer组件作为一个Provider组件,将Drawer组件内部的state和action暴露给子组件。
主页面的内容和抽屉容器内容,也就是PageContent和Content组件中的children,它们作为JSX.Elemnt插入到相应的位置。在这些组件或者这些组件的后代组件,都能通过useDrawerContext方法访问到Drawer组件内部的state和action,控制抽屉的打开和关闭。
值得注意的是,Drawer本身作为一个祖先组件提供ContextProvider,而Drawer.PageContent和Drawer.Content则作为Drawer的子组件也仅存在于Drawer的内部,它们只负责将相应的内容放置对应的位置进行渲染,并且能让它们的后代组件能够访问到Context。
