Normally this is how you use MUI Dialog. The code below is taken from the docs:
export default function AlertDialog() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
return (
<div>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
Open Dialog
</Button>
<Dialog open={open} onClose={handleClose}>
{...}
</Dialog>
</div>
);
}
But I want it to create the Dialog imperatively, sort of like fire and forget. I do not want to embed the Dialog component in other components whenever I need to create them. Ideally I'd want to call it like this
createDialog(<>
<h1>My Dialog</h1>
<span>My dialog content</span>
<button onClick={() => closeDialog()}>Close</button>
</>)
So my component definition'd look like this
const createDialog = () => {
// ???
}
const closeDialog = () => {
// ???
}
export default function AlertDialog() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => setOpen(true);
const handleClose = () => {
createDialog(<>
<h1>My Dialog</h1>
<span>My dialog content</span>
<button onClick={() => closeDialog()}>Close</button>
</>)
};
return (
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
Open Dialog
</Button>
);
}
You can reuse dialogs using React's Provider pattern. The official React document has explained in good detail so I won't cover it again here.
First create a custom Provider component in this case I'll call DialogProvider. This component will manage a list of Dialogs in local state.
const DialogContext = React.createContext();
export default function DialogProvider({ children }) {
const [dialogs, setDialogs] = React.useState([]);
return (
<DialogContext.Provider {...}>
{children}
</DialogContext.Provider>
);
}
As you can see, we have an array of dialogs here, it contains the dialog props that will be mapped to the actually <Dialog /> component when rendering.
export default function DialogProvider({ children }) {
const [dialogs, setDialogs] = React.useState([]);
return (
<DialogContext.Provider {...}>
{children}
{dialogs.map((dialog, i) => {
return <DialogContainer key={i} {...dialog} />;
})}
</DialogContext.Provider>
);
}
The <DialogContainer/> is the parent component of the <Dialog/>. Put anything that you want to be reusable in there. Here is a minimum example to get you started.
function DialogContainer(props: DialogContainerProps) {
const { children, open, onClose, onKill } = props;
return (
<Dialog open={open} onClose={onClose} onExited={onKill}>
{children}
</Dialog>
);
}
We can create and remove the dialog using setState as normal.
const [dialogs, setDialogs] = React.useState([]);
const createDialog = (option) => {
const dialog = { ...option, open: true };
setDialogs((dialogs) => [...dialogs, dialog]);
};
const closeDialog = () => {
setDialogs((dialogs) => {
const latestDialog = dialogs.pop();
if (!latestDialog) return dialogs;
if (latestDialog.onClose) latestDialog.onClose();
return [...dialogs].concat({ ...latestDialog, open: false });
});
};
But how do we call them in other components when we defined them here? Well, remember we're using Provider component here, which means we can pass the context data down so other components can reference, in this case we want to pass the createDialog and closeDialog down.
const [dialogs, setDialogs] = React.useState([]);
const createDialog = (option) => {/*...*/};
const closeDialog = () => {/*...*/};
const contextValue = React.useRef([createDialog, closeDialog]);
return (
<DialogContext.Provider value={contextValue.current}>
{children}
{dialogs.map((dialog, i) => ...)}
</DialogContext.Provider>
);
We're almost done here, now we need to add the DialogProvider to the component tree.
export default function App() {
return (
<DialogProvider>
<App {...} />
</DialogProvider>
);
}
But before we can use them, we should create a hook to easily access the context from the parent. So in your DialogProvider.jsx
export const useDialog = () => React.useContext(DialogContext);
Now we can use it like this.
import { useDialog } from "./DialogProvider";
export default function Content() {
const [openDialog, closeDialog] = useDialog();
const onOpenDialog = () => {
openDialog({
children: (
<>
<DialogTitle>This dialog is opened imperatively</DialogTitle>
<DialogContent>Some content</DialogContent>
<DialogActions>
<Button color="primary" onClick={closeDialog}>Close</Button>
</DialogActions>
</>
)
});
};
return (
<Button variant="contained" onClick={onOpenDialog}>
Show dialog
</Button>
);
}
Live Demo
You can play around in the live demo here
Related
Probably I'm missing something really simple, but:
I have an array:
const [weight, setWeight] = useState([]);
And I want to map over it to dynamically render a component:
const renderWeight = () => {
weight.map((e) => {
<Weight />;
});
};
I want this to happen when I click on a submit button, and I want the components to render in a specific div.
I can't find a way to do this.
Thank you!
You can use a show variable as toggler to show/hide the weight component like this
import React, { useState } from "react";
const App = () => {
const [weight, setWeight] = useState([1, 2, 3]);
const [show, setShow] = useState(false);
return (
<div className="container">
<div className="weight">{show && weight.map((e) => <Weight />)}</div>
<button onClick={() => setShow(!show)}>Click to show/hide</button>
</div>
);
};
export default App;
I thing you need to return the mapped array
const renderWeight = () => {
return weight.map((e) => {
<Weight />;
});
};
note: Make sure to assign a key to each component rendered
You have to return it.
const [show ,setShow] = useState(false)
const renderWeight = () => {
return weight.map((e) => {
return <Weight />;
});
};
return (
<>
<button onClick={() => setShow(!show)}>Click to show/hide</button>
{
show?
renderWeight():''
}
</>
);
const Parent = ({list}) => {
const closeAll = () => {
// What should be in here?
}
return (
<>
<button onClick={() => closeAll()}>Close All</button>
{list.map(item => <Accordion item={item}/>)}
</>
)
}
const Accordion = ({item}) => {
const [open, setOpen] = useState(false);
return (
<div onClick={() => setOpen(o => !o)}>
<p>item.name</p>
{open && <p>item.detail</p>
</div>
)
}
Basically, as above, there is the Accordion components and a parent component that hosts all of them. Each Accordion component has a state called open. I want to change state of each child from parent component. How can I send an order to a child component to change its state?
Lift your state up into Parent.
closeAll can just map over the list and set all the open properties to false.
Have a handleClick callback that you pass down to Accordion which sets the state of the clicked item's open property to the inverse in Parent
Take a look at the react docs for lifting state up.
import { useState } from "react";
const data = [
{
detail: "foo",
name: "bar"
},
{
detail: "foo1",
name: "bar1"
}
];
const Parent = ({ defaultList = data }) => {
const [list, setList] = useState(
defaultList.map((i) => ({
...i,
open: false
}))
);
const closeAll = () => {
setList(
list.map((i) => ({
...i,
open: false
}))
);
};
const handleClick = (i) => {
const newList = [...list];
newList[i].open = !list[i].open;
setList(newList);
};
return (
<>
<button onClick={() => closeAll()}>Close All</button>
{list.map((item, i) => (
<Accordion item={item} handleClick={() => handleClick(i)} />
))}
</>
);
};
const Accordion = ({ item, handleClick }) => {
return (
<div>
<button onClick={handleClick}>{item.name}</button>
{item.open && <p>{item.detail}</p>}
</div>
);
};
export default Parent;
If you are unable to lift your state there is an alternative approach using react refs.
Create ref (initially an empty array) that each Accordion will push its own close state setting function into when it first renders.
In Parent, loop over the the array of close state settings functions inside the ref and execute each.
const Parent = ({ list = data }) => {
const myRef = useRef([]);
const closeAll = () => {
myRef.current.forEach((c) => c());
};
return (
<>
<button onClick={() => closeAll()}>Close All</button>
{list.map((item, i) => (
<Accordion item={item} myRef={myRef} />
))}
</>
);
};
const Accordion = ({ item, myRef }) => {
const [open, setOpen] = useState(false);
useEffect(() => {
myRef.current.push(() => setOpen(false));
}, [myRef]);
return (
<div>
<button onClick={() => setOpen((o) => !o)}>{item.name}</button>
{open && <p>{item.detail}</p>}
</div>
);
};
export default Parent;
Using an internal state for the component is not recommended, at least from my point of view for what you are doing.
you can control the open state of each list item from its properties like the example here:
const Parent = ({ list }) => {
const [isAllClosed, setIsAllClosed] = useState(false);
const closeAll = () => {
setIsAllClosed(true)
};
return (
<>
<button onClick={closeAll}>Close All</button>
{list.map((item) => (
item.open = isAllClosed != null ? (!isAllClosed) : true;
<Accordion item={item} />
))}
</>
);
};
const Accordion = ({ item }) => {
return (
<div onClick={() => console.log('item clicked')}>
<p>item.name</p>
{item.open ? <p>item.detail</p> : null}
</div>
);
};
I also replaced you short circuit evaluation open && <p>item.detail</p> to a ternary. The reason for that is that you will get a string false being printed if not true, does it make sense?
You will need somehow control the state of the whole list whether an item is open or not from whoever is using the parent.
But avoid using internal state when you can.
I think you can try creating a state variable inside the parent and passing it as a prop to the child to control the behavior of the child.
const Parent = ({ list }) => {
const [masterOpen, setMasterOpen] = useState(true);
<>
<button onClick={() => setMasterOpen(false)}>Close All</button>
{list.map((item) => (
<Accordion item={item} parentOpen={masterOpen} />
))}
</>
);
};
const Accordion = ({ item, parentOpen }) => {
const [open, setOpen] = useState(false);
if (!parentOpen) {
setOpen(false);
}
return (
<div onClick={() => setOpen((o) => !o)}>
<p>{item.name}</p>
{open && <p>item.detail</p>}
</div>
);
};
I'm testing the use-context-selector lib to prevent unnecessary rendering in react ContextApi. I create this simple application:
export const HomeProvider = (props: PropsWithChildren<{}>) => {
const [x, setX] = React.useState(0);
const [y, setY] = React.useState(0);
return (
<HomeContext.Provider value={{ x, setX, y, setY }}>
{props.children}
</HomeContext.Provider>
);
};
export const HomeModule = () => {
return (
<HomeProvider>
<CounterScreen></CounterScreen>
<br />
<br />
<MessageScreen></MessageScreen>
</HomeProvider>
);
};
The child components are:
export const MessageScreen = () => {
const x = useContextSelector(HomeContext, (context) => context.x);
const setX = useContextSelector(HomeContext, (context) => context.setX);
return (
<>
<h1>Teste X</h1>
{x} <button onClick={() => setX(x + 1)}>X</button>
</>
);
};
export const MessageScreen = () => {
const x = useContextSelector(HomeContext, (context) => context.x);
const setX = useContextSelector(HomeContext, (context) => context.setX);
return (
<>
<h1>Teste Y</h1>
{y} <button onClick={() => setY(y + 1)}>X</button>
</>
);
};
The result, when I click in any one of those button is that indeed the child components doesn't rerender:
https://i.stack.imgur.com/9adCh.png
But, when I use the option: Highlight updates when components render in settings, all components on the screen is highlighted.
So, am i missing something or i'm right about the child components not rerendering ?? I saw other videos on youtube that show that when using this lib, the component that didn't render was not highlighted.
I have a page with the following structure
const Upload = (props) => {
return (
<BaseLayout>
<ToolbarSelection />
<Box>
<FileDropArea />
</Box>
</BaseLayout>
)
}
I have a method which works in the component <FileDropArea />
This is the method used as example
const allSelection = () => {
setFiles((files) =>
files.map((file) => {
file.checked = true;
return file;
})
);
};
In React how can i call this method allSelection from the <ToolbarSelection /> component, where i have my simple button like <Button>All Selection</Button>
You need to use React Context like this:
//create a fileContext.js
const fileContext = React.createContext();
const useFileContext = () => React.useContext(fileContext);
const FileContextProvider = ({ children }) => {
const [files, setFiles] = useState([]);
const allSelection = () => {
setFiles((files) =>
files.map((file) => {
file.checked = true;
return file;
})
);
};
// if you have other methods which may change the files add them here
return (
<fileContext.Provider
value={{
files,
setFiles,
allSelection,
}}
>
{children}
</fileContext.Provider>
);
};
use fileContextProvider in your upload file
const Upload = (props) => {
return (
<FileContextProvider>
<BaseLayout>
<ToolbarSelection />
<Box>
<FileDropArea />
</Box>
</BaseLayout>
</FileContextProvider>
);
};
use it, for example in ToolbarSelection like this:
const ToolbarSelection = () => {
const {files, allSelection} = useFileContext();
// do other stuff
}
React Hooks
I assume you are looking to make the allSelection function reusable. Hooks are a great way to make logic reusable across components.
Create a custom hook useAllSelection. Note that hooks should have a use prefix.
const useAllSelection = (files) => {
const [files, setFiles] = useState([]);
const handleAllSelection = () => {
setFiles((files) =>
files.map((file) => {
file.checked = true;
return file;
})
);
};
return { handleAllSelection };
};
const ToolbarSelection = () => {
// import the hook and use
const { handleAllSelection } = useAllSelection();
return (
<button onClick={handleAllSelection}>All Selection</button>
)
}
ReactJS allows to perform this scenario in a different way. Let me explain it: if you press a button in the ToolbarSelection, pass the value of the new state of that button to FileDropArea as props. Then, in the FileDropArea render, call the method or not depending on the value of that property
const Upload = (props) => {
return (
<BaseLayout>
<ToolbarSelection
onSelectionClick={(value) => setSelected(value)}
/>
<Box>
<FileDropArea
selected = { /* state of a button in the Toolbar */}
/>
</Box>
</BaseLayout>
)
}
Note how the callback in the Toolbar changes the state, and how this new state is passed to FileDropArea as property
I want to open a modal from a parent component with props, and then close it when everything it's done and notify to the parent in case he wants to open again.
const ModalChild = (props) => {
const [Activate, setActivate] = useState(props.Activate);
const toggle = () => setActivate(!Activate);
useEffect(() => {
setActivate(props.Activate)
}
}, []);
<Modal isOpen={Activate} toggle={false} >
<text>hello {props.hey}<text>
</Modal>
}
and the parents component, something like this:
const Accountlist= () => {
const [Activate, setActivate] = useState(false);
const toggle = (value) => {
setActivate(true)
}
render(
<button onClick={() => toggle(value)} />
<ModalEdit props={Activate}/>
)}
Please, anyone have anyidea?
If it were me, I will do it like this.
const Accountlist= () => {
const [activate, setActivate] = useState(false);
const toggleActivate = (value) => {
setActivate(value);
}
return [
<button onClick={() => toggleActivate(!activate)} />,
<ModalEdit isActivated={activate} toggleActivate={toggleActivate} />
]}
And at the child component.
const ModalChild = ({ isActivated, toggleActivate }) => {
useEffect(() => {
toggleActivate(!isActivated);
}, []);
<Modal isOpen={isActivated} toggle={() => toggleActivate(false)} >
<text>hello {props.hey}<text>
</Modal>
}
I'm not sure what you are trying to do but the rest with the logic I'll leave it to you.