I want to create a dialog component to render it once and use it throughout the whole application, instead of creating one dialog component every time a dialog needs to be displayed.
Context API correctly shows the dialog initially. If the dialog only contains static text, then it's shown correctly. But if the dialog contains any dynamic element, it doesn't react to changes.
I handle the dialogs with custom hooks.
This is an example of a dialog with static text that works:
export function useStaticTextDialog(callback) {
const appContext = useContext(AppContext);
const openDialog = () => appContext.openAppDialog(appDialogContext);
const onDialogOk = () => {
appContext.closeAppDialog(); // close unique app dialog first in case the callback uses it
callback();
};
const appDialogContext = {
title: 'Static text',
content: 'This static text works.',
onOk: onDialogOk,
onClose: appContext.closeAppDialog,
};
return { open: openDialog };
}
And it's called somewhere like this:
const staticTextDialog = useStaticTextDialog(callback);
staticTextDialog.open();
The single component is rendered once in the App component:
import AppDialog from 'components/appDialog';
function App() {
return (
<AppContextProvider>
<Router />
<AppDialog />
</AppContextProvider>
);
}
The context is very simple:
import React, { createContext, useState } from 'react';
const AppContext = createContext();
function AppContextProvider({ children }) {
const [isAppDialogOpen, setIsAppDialogOpen] = useState(false);
const [appDialogContext, setAppDialogContext] = useState({});
const openAppDialog = appDialogContext => {
setAppDialogContext(appDialogContext);
setIsAppDialogOpen(true);
};
const closeAppDialog = () => setIsAppDialogOpen(false);
const appContextValue = {
isAppDialogOpen,
appDialogContext,
openAppDialog,
closeAppDialog,
};
return <AppContext.Provider value={appContextValue}>{children}</AppContext.Provider>;
}
export { AppContext, AppContextProvider };
Well, so far that works. But if a dynamic element is added to the dialog, it doesn't react to the changes and shows stale values:
export function useDynamicTextDialog(callback) {
const appContext = useContext(AppContext);
const [name, setName] = useState();
const openDialog = newName => {
setName(newName);
appContext.openAppDialog(appDialogContext);
};
const onDialogOk = () => {
appContext.closeAppDialog(); // close unique app dialog first in case the callback uses it
callback(name);
};
const appDialogContext = {
title: 'Overwrite warning',
content: `The name "${name}" already exists. Are you sure you want to overwrite it?`,
onOk: onDialogOk,
onClose: appContext.closeAppDialog,
};
return { open: openDialog };
}
The dialog also doesn't work if it contains form elements, like text box or drop down list. It doesn't react to the new values from the user.
I find this behaviour unexpected, I think it should react to the changes and work properly. This same way in fact works when multiple dialog component are rendered, one every time a dialog is needed. Context API is not used in this case. Example:
import DialogTemplate from 'components/dialogTemplate';
export function useDynamicTextDialog(callback) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [name, setName] = useState();
const openDialog = newName => {
setName(newName);
setIsDialogOpen(true);
};
const closeDialog = () => setIsDialogOpen(false);
const onDialogOk = () => {
closeDialog ();
callback(name);
};
const dialogContext = {
title: 'Overwrite warning',
content: `The name "${name}" already exists. Are you sure you want to overwrite it?`,
onOk: onDialogOk,
onClose: closeDialog,
};
const Dialog = <DialogTemplate dialogContext={dialogContext} />;
return { Dialog, open: openDialog };
}
Then it's open somewhere, like with the single application dialog:
const dynamicTextDialog = useDynamicTextDialog(callback);
dynamicTextDialog.open();
But in this case every dialog component must be rendered:
function Component() {
const { dynamicTextDialog } = useDynamicTextDialog();
return (
<div>
{...more elements}
{applicationClosingWarningDialog.Dialog}
</div>
);
}
So, if 20 dialogs are used, 20 dialog components must be rendered, although they are almost identical except for the content.
That's why I want to use just one single dialog component for the whole application, reusing it every time and just changing the content. But Context API doesn't work.
Why does it work for the example with the dialog template but not for the example with the application dialog with Context, even though the data is handled the same way?
How to get it to work with Context API?
Context API doesn't react to changes of the dialog, even though it's used in the same way as the template component that works.
So, a render must be specifically triggered when the dialog changes:
useEffect(() => {
appContext.setAppDialogContext(dialogContext);
}, [name]);
The complete hook:
export function useDynamicTextDialog(callback) {
const appContext = useContext(AppContext);
const [name, setName] = useState();
useEffect(() => {
appContext.setAppDialogContext(dialogContext);
}, [name]);
const openDialog = newName => {
setName(newName);
appContext.openAppDialog(appDialogContext);
};
const onDialogOk = () => {
appContext.closeAppDialog(); // close unique app dialog first in case the callback uses it
callback(name);
};
const appDialogContext = {
title: 'Overwrite warning',
content: `The name "${name}" already exists. Are you sure you want to overwrite it?`,
onOk: onDialogOk,
onClose: appContext.closeAppDialog,
};
return { open: openDialog };
}
Demo with dialogs with templates and single dialog application:
https://ams777.github.io/react-single-app-dialog
Demo code: