skip to Main Content

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.

enter image description here

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


  1. 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:

    function Parent() {
      const childControls = useRef({})
      const onClick = () => childControls.current.toggle()
      return (
        <>
          <button onClick={onClick}>click me</button>
          <Child controls={childControls} />
        </>
      )
    }
    
    function Child({ controls }) {
      const [state, setState] = useState(false)
      useImperativeHandle(controls, () => ({
        toggle: () => setState(!state),
      }))
      return <p>{state ? "I was clicked" : "I wasn't clicked"}</p>
    }
    

    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 the useImperativeHandle part.

    function Parent() {
      const childControls = useRef({})
      const onClick = () => childControls.current.toggle()
      const [which, setWhich] = useState(false)
      return (
        <>
          <button onClick={onClick}>click me</button>
          {which && <Child controls={childControls} />}
          {!which && <Other controls={childControls} />}
        </>
      )
    }
    

    And going even more abstract, if even Parent doesn’t know in advance which child will be slot in, you can make use of cloneElement to inject props into a child:

    function App() {
      return (
        <Parent>
          <Child />
        </Parent>
      )
    }
    
    function Parent({ children }) {
      const childControls = useRef({})
      const onClick = () => childControls.current.toggle()
      return (
        <>
          <button onClick={onClick}>click me</button>
          {cloneElement(children, {controls: childControls})}
        </>
      )
    }
    

    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 the ItemFoo types to be able to function. Here’s what that would look like:

    const OPTIONS = [
      Child1,
      Child2,
    ]
    
    export default function App() {
      const [option, setOption] = useState(0)
      const Component = OPTIONS[option]
      return (
        <>
          <button onClick={() => setOption(0)}>option 1</button>
          <button onClick={() => setOption(1)}>option 2</button>
          <Component />
        </>
      )
    }
    
    function Child1() {
      return (
        <Dialog onClick={() => console.log("click from #1")}>
          <p>content from #1</p>
        </Dialog>
      )
    }
    
    function Dialog({
      children,
      onClick,
    }) {
      return (
        <div>
          {children}
          <button onClick={onClick}>click me</button>
        </div>
      )
    }
    
    Login or Signup to reply.
  2. @AliGajani, if you are using React Hook Form, it can be made simpler.

    • Wrap the ContentDialog element in AddItem.js with the FormProvider from React Hook Form.
    • Use the useFormContext hook to access the form methods anywhere inside its children.
    • Write the submit action handler in AddItem.js
    • Pass it as a prop to ContentDialog which will then attach it to the save button as a callback.
    • Wrap the {currentItemComponent} with form tag and define the fields inside the respective elements – ItemTextScreen etc.
    • Pass the form data to the submit handler.

    Hope this helps.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search