react指令式这个说法第一次接触是在antd组件库中,当时感觉眼前一亮,就觉得很多东西会变得很方便,将很多无关紧要的组件从显式声明解耦出来,也减少了页面中变量状态的维护
于是当时扒了扒源码,借鉴了一下,封装了一个万用的组件
首先封装了一个hook,这个hook决定了调用声明式组件的api和组件显示的位置
ts
import usePatchElement from 'antd/es/_util/hooks/usePatchElement';
import React, { useMemo, useState } from 'react';
import { useCreation } from 'ahooks';
import { createPortal } from 'react-dom';
import { InstructionMountArgs, TsUtils } from '@/utils/TsUtils';
interface Apis {
closeFn: (result?: any) => void,
promise: Promise<any>
}
export const useElementsContextHolder = () => {
const [elements, patchElement] = usePatchElement();
const [instanceMap] = useState(new Map<string, Apis>());
const fns = useCreation(() => ({
instructionMountPromise<P, R>({ Component, props, getContainer }: InstructionMountArgs<P>) {
const { promise, resolve } = TsUtils.getSeparatePromise<R>();
const key = TsUtils.getUUID();
let element: React.ReactElement = React.createElement(Component, { ...props, key, closeResolve: (data?: R) => this.closeElement(key, data) });
if (getContainer) {
element = createPortal(element, getContainer);
}
const removeElementFn = patchElement(element);
const closeFn: Apis['closeFn'] = (result) => {
removeElementFn();
instanceMap.delete(key);
resolve(result);
};
instanceMap.set(key, {
closeFn,
promise,
});
return promise;
},
closeElement<R>(key: string, result?: R) {
instanceMap.get(key).closeFn(result);
},
}), []);
return [fns, (
<React.Fragment>
{elements}
</React.Fragment>
)] as const;
};
api最好是全局调用,当然不是全局也可以,毕竟迭代中总会出现一些额外的情况
这里我将api挂载在一个全局的appInstance中
ts
export class AppInstance {
ContextHolder: ReturnType<typeof useElementsContextHolder>[0];
getContextHolder() { return this.ContextHolder; }
setContextHolder(ContextHolder: ContextHolderPlugin['ContextHolder']) {
this.ContextHolder = ContextHolder;
}
ContextHolderComponent() {
const [contextHolderApi, contextHolder] = useElementsContextHolder();
useMount(() => {
this.setContextHolder(contextHolderApi);
});
return contextHolder;
}
}
<AntdConfigProvider>
<appInstance.ContextHolderComponent />
</AntdConfigProvider>
最后一步就是包装指令式命令了,我这里用antd的选择文件举例,antd的选择文件需要显式声明,而包装之后只需在点击中promise等待就够了
ts
import React, { FC, useRef } from 'react';
import { Upload, UploadProps } from 'antd';
import { css, cx } from '@emotion/css';
import { useMount } from 'ahooks';
import { InstructionMountProps } from '@/utils/TsUtils';
import { appInstance } from '@/runTime';
const SelectFiles: FC<UploadProps & InstructionMountProps> = (props) => {
const ref = useRef<HTMLDivElement>();
useMount(() => {
ref.current.click();
});
return (
<div className={cx(css`height: 0;overflow: hidden`)}>
<Upload
{...props}
beforeUpload={(file, FileList) => {
props.closeResolve(FileList);
return Upload.LIST_IGNORE;
}}
>
<div ref={ref}>上传</div>
</Upload>
</div>
);
};
export const openSelectFilesPromise = (args: UploadProps) => {
return appInstance.getContextHolder().instructionMountPromise<UploadProps, File[]>({
Component: SelectFiles,
props: args,
});
};
例如我有一个流程:弹框输入文件名->选择文件->上传文件->弹框提示上传成功
按照声明式流程,至少需要显式声明两个弹框一个上传按钮,还需要变量控制是否显示弹框,但是使用指令式封装之后只需要全部在异步流程中就可以了
ts
async click() {
const name = await openInputModal();
const [file] = await openSelectFilesPromise();
await uploadFile();
await showUploadSuccessModal();
}
显式声明写的太麻烦了,这里就不写了,比指令式要麻烦不知道多少