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>
)
}
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. ```tsx const useModal = () => { const [open, setOpen] = useState(false); ...
Jump to solution
7 Replies
Brendonovich
Brendonovich•17mo ago
One way is to make children a render prop:
interface AddGeneralModalProps
extends Omit<DialogProps, 'children'>,
VariantProps<typeof contentVariants> {
children: (p: ModalProps) => DialogProps['children']
}

export interface ModalProps {
setOpen: (v: boolean) => void;
}

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({ setOpen })}
</DialogContent>
</Dialog>
);
};

export default AddGeneralModal;

// Usage:
const SpecificForm = (props: ModalProps) => {
const { mutate } = api.model.post.useMutation({
onSuccess: async () => {
props.setOpen(false)
},
});

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

const MyPage = () => {
return (
<AddGeneralModal>
{(modalProps) => <SpecificForm {...modalProps }/>}
</AddGeneralModal>
)
}
interface AddGeneralModalProps
extends Omit<DialogProps, 'children'>,
VariantProps<typeof contentVariants> {
children: (p: ModalProps) => DialogProps['children']
}

export interface ModalProps {
setOpen: (v: boolean) => void;
}

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({ setOpen })}
</DialogContent>
</Dialog>
);
};

export default AddGeneralModal;

// Usage:
const SpecificForm = (props: ModalProps) => {
const { mutate } = api.model.post.useMutation({
onSuccess: async () => {
props.setOpen(false)
},
});

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

const MyPage = () => {
return (
<AddGeneralModal>
{(modalProps) => <SpecificForm {...modalProps }/>}
</AddGeneralModal>
)
}
Solution
Brendonovich
Brendonovich•17mo ago
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>
)
}
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>
)
}
cupofcrypto
cupofcrypto•17mo ago
Thanks for the quick answer, actually I thought of lifting state since in a different project (at work) I already use Zustand to handle the state of different modals, but just between you and me I don't like it much 😅 Anyway, maybe I am just confused now, but lifting this state up means that I'll have to handle the state for each modal but I believe this will be the best approach anyway. Thanks for the support @brendonovich
Keef
Keef•17mo ago
what a quality love
Brendonovich
Brendonovich•17mo ago
gonna keep writing essays here until Spacedrive has enough engineering blog posts that I can just link as replies
Keef
Keef•17mo ago
BASICALLY I've never touched render props so its cool to see a specific use case for them
Want results from more Discord servers?
Add your server