Send setter to `props.children`

So I think this is silly, but at the same time I don't find a way out of it, so I am here asking for help.

I am trying to make a Modal that can accept any children and that children will receive the setOpen fn to open/close the modal independently.

Let's show some code:
interface AddGeneralModalProps
  extends DialogProps,
    VariantProps<typeof contentVariants> {}

const AddGeneralModal = ({ children, size }: AddGeneralModalProps) => {
  const [open, setOpen] = useState(false);

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <Button className={buttonVariants()}>Open Modal</Button>
      </DialogTrigger>
      <DialogContent className={cn(contentVariants({ size }))}>
        <DialogHeader>
          <DialogTitle>Modal Title</DialogTitle>
          <DialogDescription>
            Modal Desc
          </DialogDescription>
        </DialogHeader>

        {children}
      </DialogContent>
    </Dialog>
  );
};

export default AddGeneralModal;

// Usage:
const SpecificForm = () => {
  const { mutate } = api.model.post.useMutation({
    onSuccess: async () => {
      // Here I would like to close the modal
    },
  });

  return (
    <Form>
      <Button onClick={ () => mutate() }> Submit < /Button>
    </Form>
  )
}

const MyPage = () => {
  return (
    <AddGeneralModal>
      <SpecificForm />
    </AddGeneralModal>
  )
}


As you can see my attempt is to generalize the Dialog component from Shadcn/ui but I really cannot wrap my head around on how to pass setOpen down to SpecificForm and also make TypeScript happy.

Thanks for the help.
Solution
My preferred way though is to hoist the state of the modal outside of the JSX and store it in a hook.
const useModal = () => {
  const [open, setOpen] = useState(false);

  // any other modal-related state can go in this hook
  
  return { open, setOpen }
};

interface AddGeneralModalProps
  extends DialogProps,
    VariantProps<typeof contentVariants> {
  modal: ReturnType<typeof useModal>
}

const AddGeneralModal = ({ children, size, modal }: AddGeneralModalProps) => {
  return (
    <Dialog open={modal.open} onOpenChange={modal.setOpen}>
      <DialogTrigger asChild>
        <Button className={buttonVariants()}>Open Modal</Button>
      </DialogTrigger>
      <DialogContent className={cn(contentVariants({ size }))}>
        <DialogHeader>
          <DialogTitle>Modal Title</DialogTitle>
          <DialogDescription>
            Modal Desc
          </DialogDescription>
        </DialogHeader>

        {children}
      </DialogContent>
    </Dialog>
  );
};

export default AddGeneralModal;

// Usage:
const SpecificForm = (props: { onSuccess: () => void | Promise<void>}) => {
  const { mutate } = api.model.post.useMutation({
    onSuccess: async () => {
      await props.onSuccess()
    },
  });

  return (
    <Form>
      <Button onClick={ () => mutate() }> Submit < /Button>
    </Form>
  )
}

const MyPage = () => {
  const modal = useModal();

  return (
    <AddGeneralModal modal={modal}>
      <SpecificForm onSuccess={() => modal.setOpen(false)} />
    </AddGeneralModal>
  )
}
Was this page helpful?