重构 React 组件以供重用

Refactoring a React component for reuse

我正在寻找重构以下 React 组件的建议:

const Block = () => {
    const {blockId} = useParams();
    const {register, control, handleSubmit} = useForm();

    const isNewBlock = typeof blockId === 'undefined';

    const [saveBlockMutation] = useSaveBlockMutation();
    const [deleteBlockMutation] = useDeleteBlockMutation();
    const {data, loading, error} = useGetBlockQuery({
        skip: isNewBlock,
        variables: {block_id: parseInt(blockId!)}
    });

    const saveBlock = (input: any /* todo: type it */) => {
        saveBlockMutation({variables: {input: input}})
            .then(result => {
                if (result.data?.saveBlock) {
                    // show notification
                }
            })
    };

    const deleteBlock = (blockId: number) => {
        deleteBlockMutation({variables: {block_id: blockId}})
            .then(result => {
                if (result.data?.deleteBlock) {
                    // show notification
                }
            })
    }

    return (
        <LoaderHandler loading={loading} error={error}>
            {!loading && (
                <>
                    <Header text={data ? "Block: " + data.block.identifier : "Add Block"}>
                        <Button onClick={handleSubmit(saveBlock)}>Save</Button>
                        {!isNewBlock && (<Button onClick={() => deleteBlock(parseInt(blockId!))}>Delete</Button>)}
                    </Header>
                    <Form data={data} register={register} control={control}/>
                </>
            )}
        </LoaderHandler>
    )
}

这目前工作正常,但我将添加一些其他组件,它们的行为应该完全相同:

我觉得我应该能够将列表中的所有内容提取到更通用的内容中,除了“呈现表单”部分。

我无法确定那个“东西”是什么。也许 HOC 适合这里?我最终会得到类似的东西:

const Block = (props: WithCrudProps) => {
    // we only render a form here
}

export default withCRUD(
    Block, 
    {
        deleteMutation: DeleteBlockMutation,
        saveMutation: SaveBlockMutation,
        getQuery: GetBlockQuery,
        // etc.
    }
);

但感觉很快就会变得一团糟。解决这个问题的“反应方式”是什么?

我认为 withCRUD 很难有一个好的实现,因为这里的所有关系:

// you need param name to extract it from params:
const params = useParams();
const param = params[paramName];
// then you need to convert param to query variables:
const queryVariables = makeQueryVariables(param)
// and you will need more of that for mutations

所以我会推荐自定义挂钩

interface UseCrudParams<T, Vars> {
  id?: number;
  initialData?: T;
  onSave: (vars: Vars) => Promise<void>;
  onDelete: () => Promise<void>
}

function useCrud<T, Vars>({
  id,
  initalData,
  onSave,
  onDelete,
}: UseCrudParams<T, Vars>): CrudProps { /* ... */}

// and then when you use it you adapt mutations to form
const formProps = useCrud({
  id: blockId,
  initialData: data,
  onSave: variables => saveBlockMutation({ variables  }),
  onDelete: () => deleteBlockMutation({ variables: { block_id: blockId } }),
})

并为表单布局创建 UI 组件:

function FormLayout({ header, loading, error, onSave, showDelete, onDelete, children }: FormLayoutProps) {
  return (
        <LoaderHandler loading={loading} error={error}>
            {!loading && (
                <>
                    <Header text={header}>
                        <Button onClick={onSave}>Save</Button>
                        {showDelete && (<Button onClick={onDelete}>Delete</Button>)}
                    </Header>
                    {children}
                </>
            )}
        </LoaderHandler>
    )
}