Basically when a user clicks “Add Item”, I show a bunch of options like Add Note, Add Link, Add Photo and so on. Whenever one of these options is clicked, I slot in the relevant component I.e ItemTextScreen
, ItemLinkScreen
, ItemPhotoScreen
in the ContentDialog
via the AddItem
.
Now my challenge is, what is the ideal way to allow a button click like “Save” to trigger a handleSubmit implementation inside ItemTextScreen
. I’m new to React so forgive my ignorance. Currently , ContentDialog
lives in the AddItem
and waits for the impending option click whereby it takes in one of the ItemTextScreen
, ItemLinkScreen
, ItemPhotoScreen
.
Basically I’ve researched into imperativeHandle
and refs
but I am not sure if these are the Reacty ways of doing things or if I should be doing it differently.
ContentDialog.js
import { useEffect, useState } from 'react';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import useMediaQuery from "@mui/material/useMediaQuery";
export default function ContentDialog(props) {
const [toggle, setToggle] = useState(true);
const bigScreen = useMediaQuery((theme) => theme.breakpoints.up("md"));
useEffect(() => {
setToggle(props.open);
}, [props.open]);
return (
<div>
<Dialog
fullScreen={!bigScreen}
open={toggle}
onClose={props.handleClose}
aria-labelledby="scroll-dialog-title"
aria-describedby="scroll-dialog-description"
>
<DialogTitle id="scroll-dialog-title">{props.title}</DialogTitle>
<DialogContent>
{props.children}
</DialogContent>
<DialogActions>
<Button onClick={props.handleClose}>Cancel</Button>
<Button>Save</Button>
</DialogActions>
</Dialog>
</div>
);
}
AddItem.js
import ArticleOutlinedIcon from "@mui/icons-material/ArticleOutlined";
import AudioItem from "./Screens/ItemAudioScreen";
import BookmarkBorderOutlinedIcon from "@mui/icons-material/BookmarkBorderOutlined";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import ButtonGroup from "@mui/material/ButtonGroup";
import ContentDialog from "@/components/dialogs/ContentDialog";
import LinkItem from "./Screens/ItemLinkScreen";
import MicNoneOutlinedIcon from "@mui/icons-material/MicNoneOutlined";
import PhotoItem from "./Screens/ItemPhotoScreen";
import PhotoOutlinedIcon from "@mui/icons-material/PhotoOutlined";
import TextItem from "./Screens/ItemTextScreen";
import VideoItem from "./Screens/ItemVideoScreen";
import VideoLibraryOutlinedIcon from "@mui/icons-material/VideoLibraryOutlined";
import { grey } from "@mui/material/colors";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useState } from "react";
function AddItem() {
const bigScreen = useMediaQuery((theme) => theme.breakpoints.up("md"));
const orientation = bigScreen ? "horizontal" : "vertical";
const [currentItemComponent, setCurrentItemComponent] = useState(null);
const [openContentDialog, setOpenContentDialog] = useState(false);
const handleOpenContentDialog = (itemComponent) => {
setCurrentItemComponent(itemComponent);
setOpenContentDialog(true);
};
const handleCloseContentDialog = () => {
setOpenContentDialog(false);
};
return (
<Box sx={{ ml: { md: 3 } }}>
<ContentDialog
open={openContentDialog}
handleClose={handleCloseContentDialog}
>
{currentItemComponent}
</ContentDialog>
<ButtonGroup
fullWidth
orientation={orientation}
size="medium"
color="primary"
aria-label="large button group"
>
<Button
onClick={() => handleOpenContentDialog(<TextItem title="Note" />)}
>
<ArticleOutlinedIcon sx={{ mr: 1 }} /> Write a Note
</Button>
<Button
onClick={() => handleOpenContentDialog(<PhotoItem title="Photo" />)}
>
<PhotoOutlinedIcon sx={{ mr: 1 }} /> Upload a Photo
</Button>
<Button onClick={() => handleOpenContentDialog(<VideoItem title="Video" />)}>
<VideoLibraryOutlinedIcon sx={{ mr: 1 }} /> Embed a Video
</Button>
<Button onClick={() => handleOpenContentDialog(<AudioItem title="Audio" />)}>
<MicNoneOutlinedIcon sx={{ mr: 1 }} /> Record an Audio
</Button>
<Button onClick={() => handleOpenContentDialog(<LinkItem title="Link" />)}>
<BookmarkBorderOutlinedIcon sx={{ mr: 1 }} /> Bookmark a Link
</Button>
</ButtonGroup>
</Box>
);
}
export default AddItem;
ItemTextScreen.js
import React from 'react'
function TextItem() {
const submit = () => {
console.log('submit');
}
return (
<div>Vestibulum ac diam sit amet quam vehicula elementum sed sit amet dui. Donec rutrum congue leo eget malesuada. Nulla porttitor accumsan tincidunt. Pellentesque in ipsum id orci porta dapibus. Cras ultricies ligula sed magna dictum porta.</div>
)
}
export default TextItem
2
Answers
This pattern is not the most common, and it should probably be an indicator that the way your code is structured is not the most sustainable (scalable, maintainable, understandable…). But it is done and it works fine.
Here’s the basic idea:
Now at this point, you could swap
<Child>
at any point and it would still work, assuming that whichever component you swap it for also knows to do theuseImperativeHandle
part.And going even more abstract, if even
Parent
doesn’t know in advance which child will be slot in, you can make use ofcloneElement
to inject props into a child:A more "standard" way of doing this would be to restore the proper "direction" in the control flow of your component tree:
Dialog
should be the last component in the tree since it needs data from one of theItemFoo
types to be able to function. Here’s what that would look like:@AliGajani, if you are using React Hook Form, it can be made simpler.
Hope this helps.