gremo
gremo

Reputation: 48487

How to "control" a state component from another, without using a global state pattern in React?

Say that you have a <Button> and <Dialog> component. On button click, the dialog should open (open prop). Normally you would put both components togheter, using react useState to control the state:

export const FilterDialog() {
  const [open, setOpen] = useState(false);

  const handleOpen = () => setOpen(true);
  const handleClose = () => setOpen(false);

  return (
    <Button onClick={handleOpen}
      Open
    </Button>
    <Dialog open={open}>
      <Button onClick={handleClose}
        Close
      </Button>
    </Dialog>
  );
};

This isn't a good solution as other part of the application may open the FilterDialog.

Thr problem could easly solved using a state pattern where state is global and the Dialog is attached to the isFilterDialogOpen state slice.

Is there any way to avoid the use of the global state pattern and have multiple button triggers controlling the dialog?

Upvotes: 1

Views: 37

Answers (2)

Chris Hamilton
Chris Hamilton

Reputation: 10994

Just pass in open and handleClose as props

export const FilterDialog = ({open, onClose}) => {
  return (
      <Dialog open={open}>
        <Button onClick={onClose}>Close</Button>
      </Dialog>
  );
};
export const App = () => {
  const [open, setOpen] = useState(false);

  const handleOpen = () => setOpen(true);
  const handleClose = () => setOpen(false);
  return (
    <>
      <Button onClick={handleOpen}>Open</Button>
      <FilterDialog open={open} onClose={handleClose} />
    </>
  );
}

This is a common pattern to pass in callback functions that get executed by dialog buttons. Usually you'll have an onConfirm or something as well. You can reuse the same dialog to do different things.

For example, let's say this dialog lets the user input a filter string, and you want to filter an array from a different component when the user presses ok. You can pass in a function that takes that string as a parameter.

export const FilterDialog = ({open, onClose, onOk}) => {
  const [value, setValue] = useState("")
  return (
      <Dialog open={open}>
        <input value={value} onChange={(e) => setValue(e.target.value)}/>
        <Button onClick={() => onOk(value)}>OK</Button>
        <Button onClick={onClose}>Close</Button>
      </Dialog>
  );
};
export const App = () => {
  const [open, setOpen] = useState(false);
  const [arr, setArr] = useState(["apple", "banana", "orange"]);

  const handleOpen = () => setOpen(true);
  const handleClose = () => setOpen(false);
  const handleFilter = (filterString) => {
    console.log(arr.filter((fruit) => fruit.startsWith(filterString)));
    setOpen(false);
  };

  return (
    <>
      <Button onClick={handleOpen}>Open</Button>
      <FilterDialog open={open} onClose={handleClose} onOk={handleFilter} />
    </>
  );
}

By your comment it sounds like you just want a global dialog that can be opened from anywhere. You just make a context alongside your dialog like so:

export const DialogCtx = createContext(null);

export const DialogCtxProvider = ({ children }) => {
  const [open, setOpen] = useState(false);
  return (
    <DialogCtx.Provider value={{ open, setOpen }}>
      {children}
    </DialogCtx.Provider>
  );
};

export const FilterDialog = () => {
  const { open, setOpen } = useContext(DialogCtx);

  const handleClose = () => setOpen(false);
  return (
    <Dialog open={open}>
      <Button onClick={handleClose}>Close</Button>
    </Dialog>
  );
};

And wrap your app in it - or whatever level you need it at:

root.render(
  <DialogCtxProvider>
    <App />
  </DialogCtxProvider>
);

Then just use the useContext hook wherever you want to open it from.

export default function App() {
  const { setOpen } = useContext(DialogCtx);
  return <button onClick={() => setOpen(true)}>open</button>;
}

Upvotes: 1

G.L
G.L

Reputation: 101

If it's a component communicating to another component then the approaches available are React.Context, Redux state, just a parent state that's encapsulating those two components (in which will look like a global state) or via useRef. I'm not sure what issue you wanna avoid here but it looks somewhat okay. Happy to be corrected here.

About other parts of the app that may open it. We can create separate components (different dialog types) but if it's just one instance, we can just close and reopen the Dialog to re-render it.

Upvotes: 1

Related Questions