Display custom Modal dynamically in react app - reactjs

In a react app, I'm trying to display a modal with a form. Any suggestions on how to do that.
Thanks in advance.

Since usually there is only one modal at the same moment on your screen, it's a pretty common practice to use Context/Provider for this purpose. First you need to create context for your modal:
const ModalContext = React.createContext();
Then your own ModalProvider:
const ModalProvider = ({ children }) => {
const [ modal, setModal ] = React.useState(null);
const showModal = renderProp => setModal(renderProp);
const hideModal = () => setModal(null);
return (
<ModalContext.Provider value={ showModal }>
{ children }
{ modal && (
<div className="overlay" />
{ renderProp(hideModal) }
) }
</ModalContext.Provider>
);
}
Own modal provider should wrap your app on some root level:
const App = () => (
<ModalProvider>
<LandingPage />
</ModalProvider>
);
Then, you just need to use this context in any component to show modal:
const renderModal(hideModal) => {
// call hideModal to hide modal
};
const Page = () => {
const showModal = useContext(ModalContext);
const onClick = showModal(renderModal);
return (
<Button onClick={ onClick }>Show Modal!</Button>
);
}
If you already have redux in your app you can use it for the same purpose - by setting some global state with modal data:
const modalReducer = (state, { type, payload }) => {
switch (type) {
case SHOW_MODAL:
return { ...state, modal: payload };
case HIDE_MODAL:
return { ...state, modal: null };
default:
return state;
}
};

Related

React Context Not Updating when clicking open modal button

I am trying to open a modal by updating the state of the component. The component is wrapped in a Context Provider.
Although the button seems to be clicking successfully, the Modal will not open
Here is the code with the container which contains the "Open Modal Button"
import { type FC, useRef } from 'react'
import infomation from '#/assets/icons/infomation.svg'
import { useModal } from '#/providers/ModalProvider'
import styles from './Instructions.module.scss'
const Instructions: FC = () => {
const card = useRef<HTMLDivElement>(null)
const { openDemoModal } = useModal()
const onOpenModalClick = () => {
openDemoModal()
console.log(openDemoModal)
}
return (
<section ref={card} className={`card ${styles.card}`}>
<div className={styles.background} />
<div>OPEN THE MODAL DOWN BELOW</div>
<button variant="outlined" fullWidth onClick={onOpenModalClick}>
Open Modal
</button>
</section>
)
}
export default Instructions
Here is the file which contains the Context for the Modal, I have tried setting up the context in INITIAL_STATE and tried updating it using the "onOpenModalClick" function - but it doesn't seem to be able to update the ShowModal.current value below.
import { type FC, type PropsWithChildren, createContext, useContext, useRef } from 'react'
import Modal from '#/components/modal/Modal'
type ContextValue = {
showModal: boolean
openDemoModal: () => void
}
const INITIAL_STATE: ContextValue = {
showModal: false,
openDemoModal: () => {},
}
export const ModalContext = createContext(INITIAL_STATE)
export const ModalProvider: FC<PropsWithChildren> = ({ children }) => {
const showModal = useRef(INITIAL_STATE.showModal)
const openDemoModal = () => {
showModal.current = true
}
console.log(showModal.current)
return (
<ModalContext.Provider value={{ showModal: showModal.current, openDemoModal }}>
{children}
<Modal show={showModal.current} setShow={(shouldShow: boolean) => (showModal.current = shouldShow)} />
</ModalContext.Provider>
)
}
export function useModal() {
const context = useContext(ModalContext)
if (!context) {
throw new Error('useModal must be used within a ModalProvider')
}
return context
}
Is there any way to update the onOpenModalClick button to make it change the value of showModal.current in the Provider file?
Sorry if the post is unclear, this is my first Stack Overflow post. Let me know if I need to post anymore of the components.
I tried to add a button to the component which updates the Context, however the state failed to update
The useRef does not trigger a re-render when the value changes. Instead you can use a useState hook. Which would look something like this.
type ContextValue = {
showModal: boolean;
openDemoModal: () => void;
};
const INITIAL_STATE: ContextValue = {
showModal: false,
openDemoModal: () => console.warn('No ModalProvider'),
};
export const ModalContext = createContext(INITIAL_STATE);
export const ModalProvider: FC<PropsWithChildren> = ({ children }) => {
const [showModal, setShowModal] = useState(false);
const openDemoModal = () => {
setShowModal(true)
};
console.log(showModal);
return (
<ModalContext.Provider
value={{ showModal, openDemoModal }}
>
{children}
<Modal
show={showModal}
setShow={setShowModal}
/>
</ModalContext.Provider>
);
};

React Uncaught Invariant Violation: Element type is invalid: expected a string

I am trying to use hooks and React context/provider to show modal in my app. But when I try to show it I am getting this error:
Here is the piece of code:
My Provider
function ModalProvider({ children }) {
const [ModalContent, setModalContent] = useState(null);
const [modalData, setModalData] = useState(null);
const [open, setOpen] = useState(false);
const showModal = useCallback(
({ content, data }) => {
setModalContent(content);
setModalData(data);
setOpen(true);
},
[setModalContent, setModalData, setOpen],
);
const hideModal = useCallback(
() => {
setModalContent(null);
setModalData(null);
setOpen(false);
},
[setModalContent, setModalData, setOpen],
);
const value = useMemo(
() => ({
ModalContent,
open,
modalData,
showModal,
hideModal,
}),
[ModalContent, modalData, open, showModal, hideModal],
);
return (
<ModalContext.Provider value={value}>
<Modal isOpen={open} setIsOpen={setOpen} titleText="Hello from Modal">
{ModalContent && <ModalContent />}
</Modal>
{children}
</ModalContext.Provider>
);
ModalProvider.propTypes = {
children: PropTypes.node,
};
export default memo(ModalProvider);
Implementation
const Modal = () => {
return (
<div><h1>Test Modal</h1></div>
);
}
const Home = ({
logout,
}) => {
const { showModal, hideModal } = useModalDispatcher();
return (
<div>
<Button onClick={() => showModal({ content: Modal })} size="small">SHOW</Button>
<Button onClick={() => hideModal()} size="small">HIDE</Button>
</div>
);
};
Home.displayName = 'Home';
Home.propTypes = {
logout: PropTypes.func.isRequired,
};
export default Home;
The only way that this is working is when I use the prop "type" something like ModalContent.type I do not really know why is that.
The problem here is that you saving the component as a function when using the setModalContent function.
That is so because state can be initialized and updated with a function that returns the initial state or the updated state, you need to supply a function that in turn returns the function you want to put in state.
So, in order to get this working you could wrap the execution of you setModalContent inside the showModal function with an anonymous function, like this:
const showModal = useCallback(
({ content, data }) => {
setModalContent(() => content);
setModalData(data);
setOpen(true);
},
[setModalContent, setModalData, setOpen],
);
Hope it helps!
Can you try changing this, in your ModalProvider component
<Modal isOpen={open} setIsOpen={setOpen} titleText="Hello from Modal">
{ModalContent && <ModalContent />}
</Modal>
To this
<Modal isOpen={open} setIsOpen={setOpen} titleText="Hello from Modal">
{ModalContent && {ModalContent}}
</Modal>

Multiple Modals with React Hooks

I´m building some modals for fun but I can´t make it work for multiple modals.
useModal hook
import { useState } from 'react';
const useModal = () => {
const [isShowing, setIsShowing] = useState(false);
const toggle = () => {
setIsShowing(!isShowing);
}
return {
isShowing,
toggle,
}
};
export default useModal;
Modal component
import React, { useEffect } from 'react';
const Modal = (props) => {
const { toggle, isShowing, children } = props;
useEffect(() => {
const handleEsc = (event) => {
if (event.key === 'Escape') {
toggle()
}
};
if (isShowing) {
window.addEventListener('keydown', handleEsc);
}
return () => window.removeEventListener('keydown', handleEsc);
}, [isShowing, toggle]);
if (!isShowing) {
return null;
}
return (
<div className="modal">
<button onClick={ toggle } >close</button>
{ children }
</div>
)
}
export default Modal
in my Main component
If I do this the only modal in the page works fine
const { isShowing, toggle } = useModal();
...
<Modal isShowing={ isShowing } toggle={ toggle }>first modal</Modal>
but when I try to add another one it doesn´t work. it doesn´t open any modal
const { isShowingModal1, toggleModal1 } = useModal();
const { isShowingModal2, toggleModal2 } = useModal();
...
<Modal isShowing={ isShowingModal1 } toggle={ toggleModal1 }>first modal</Modal>
<Modal isShowing={ isShowingModal2 } toggle={ toggleModal2 }>second modal</Modal>
what I´m doing wrong? thank you
if you want to check it out please go to https://codesandbox.io/s/hopeful-cannon-guptw?fontsize=14&hidenavigation=1&theme=dark
Try that:
const useModal = () => {
const [isShowing, setIsShowing] = useState(false);
const toggle = () => {
setIsShowing(!isShowing);
};
return [isShowing, toggle];
};
then:
export default function App() {
const [isShowing, toggle] = useModal();
const [isShowingModal1, toggleModal1] = useModal();
const [isShowingModal2, toggleModal2] = useModal();
if you have multiple modal then dynamically inject <div> inside the page body and map modal to it.
to create div use,
var divTag = document.createElement("div");
divTag.setAttribute('id', 'modal');
document.getElementsByTagName('body')[0].appendChild(divTag);
document.getElementById('modal').innerHTML = modalHtML;
This should work.

How to make a custom hook with useState which can update multiple elements if state changes?

Consider the following example:
const useCounter = () => {
const [count, setCount] = useState(0);
return [ count, setCount ];
};
const Shower = () => {
const [ value ] = useCounter();
console.log(value); //stays 0
return value;
}
const Setter = () => {
const [ value, setValue ] = useCounter();
console.log(value); //updates on click
return <button onClick={() => setValue(value+1)}>
Add
</button>
}
const App = () => {
return (
<div className="App">
<Setter />
<Shower />
</div>
);
}
What am I doing wrong? I'd expect that it will use the same state no matter where and how many times it gets used, and if that state updates, it should update every component which uses it I think.
Any suggestions?
That's what react context api try to solve.
const CounterContext = React.createContext({
count: 0,
setCount: () => null
})
const CounterProvider = ({ children }) => {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={{
count, setCount
}}>
{children}
</CounterContext.Provider>
)
}
const useCounter = () => {
return React.useContext(CounterContext)
};
useCounter will now provide you the same count and setCount in every component you call it.
To use it:
const Shower = () => {
const { count } = useCounter();
return count;
}
const Setter = () => {
const { count, setCount } = useCounter();
return <button onClick={() => setCount(count+1)}>
Add
</button>
}
const App = () => {
return (
<CounterProvider>
<div className="App">
<Setter />
<Shower />
</div>
</CounterProvider>
);
}
useState returns a pair of value and setter. A piece of data and a way to change it, but everytime you instantiate a new Component a new instace of this pair will be created as well. hooks are a great way to share statetul logic between components, not state itself. Shower get's called and a instance of useCounter is created. Setter gets called and a new instance is created. The structure is the same, the state is not.
To share state between components use props, redux or Context API
When sharing things between functional components, I like to use the pattern below, it is the redux-ish reusable version of Federkun's answer above:
// this component should be an ancestor of component sharing state
// note that it works no matter how deep your subcomponents are in the render tree
class SharedStateContextProvider extends React.Component {
/* static propTypes = {
sharedContext: PropTypes.object,
reducer: PropTypes.func,
initialState: PropTypes.object,
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.arrayOf(PropTypes.node),
]),
} */
constructor(props) {
super(props);
this.state = {
contextValue: { state: props.initialState, dispatch: this.handleDispatch },
};
}
handleDispatch = (action) => {
const { reducer } = this.props;
const { contextValue: { state: sharedState } } = this.state;
const newState = reducer(sharedState, action);
if (newState !== sharedState) {
this.setState(
() => ({
contextValue: { state: newState, dispatch: this.handleDispatch }
})
);
}
}
render() {
const { sharedContext: Context, children } = this.props;
const { contextValue } = this.state;
return (
<Context.Provider value={contextValue}>
{children}
</Context.Provider>
);
}
}
// the actual shared context
const CounterContext = React.createContext();
// add as much logic as you want here to change the state
// as you would do with redux
function counterReducer(state, action) {
switch(action.type) {
case 'setValue':
return {
...state,
value: action.data
};
default:
return state;
}
}
// counterContext is a prop so the dependency in injected
const Shower = ({ counterContext }) => {
// well known redux-ish interface
const { state, dispatch } = React.useContext(counterContext);
console.log(state.value);
return state.value;
}
// counterContext is a prop so the dependency in injected
const Setter = ({ counterContext }) => {
// well known redux-ish interface
const { state, dispatch } = React.useContext(counterContext);
console.log(state.value); //updates on click
return <button onClick={() => dispatch({ type: 'setValue', data: state.value+1 })}>
Add
</button>
}
// the actual shared state
const initialCounterState = { value: 0 };
const App = () => {
return (
<div className="App">
<SharedStateContextProvider
sharedContext={CounterContext}
reducer={counterReducer}
initialState={initialCounterState}
>
<Setter counterContext={CounterContext} />
<Shower counterContext={CounterContext} />
</SharedStateContextProvider>
</div>
);
}
const root = document.getElementById('root');
ReactDOM.render(<App />, root);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

UIkit's modals in React: integration

I'm working on this project where the frontend is in React with UIkit for the user interface. The integration between the parts looks poorly implemented. I'm going to explain why. There is a Modal component, something like
export class Modal extends Component {
static getByName = name => UIkit.modal(`[data-modal-name='${name}']`)
static show = name => {
const modal = Modal.getByName(name)
if (modal) modal.show()
}
static hide = name => {
const modal = Modal.getByName(name)
if (modal) modal.hide()
}
render() {
// a modal
}
}
this is used in this way
export const LoginFormModal = props => (
<Modal name="login-form" className="login-form-modal" hideClose>
<LoginForm />
</Modal>
)
and show/hide is called programmatically where needed (even redux's actions)
Modal.hide("login-form")
this is in a Redux action, like this
export const login = credentials => {
return dispatch => {
dispatch(showLoader())
API.authentication.login(
credentials,
response => {
setCurrentUser(
Object.assign({}, response.user, { user_id: response.user.id })
)
Modal.hide("login-form")
dispatch(loginSucceded(response))
dispatch(hideLoader())
dispatch(push("/"))
dispatch(fetchNotificationsCounter())
},
error => {
dispatch(loginFailed(error))
dispatch(hideLoader())
}
)
}
}
This seems to work. Until you leave a component. When you come back to it, the second time the programmatically hide does not work anymore.
Anyone can lead me to how integrate the parts in a more react-appropriate way?
Using the parts of uikit which manipulate the dom (show, hide) is obviously hard to connect with React (and probably you shouldn't), however:
You need to move the call of the functions show and hide inside the Component by passing the bool of the state of the modal (eg. modalopen) . A good hook is the componentWillReceiveProps which can be used to check the previus props
componentWillReceiveProps(nextProps) {
if (nextProps.modalopen !== this.props.modalopen) {
if (nextProps.modalopen) {
getByName(...).show()
} else {
getByName(...).hide()
}
}
}
(this is inside the Modal class)
The thing I don't like and that is definitely not a "React-way" is that the code is mutating state directly from an action creator (!). From React docs:
For example, instead of exposing open() and close() methods on a
Dialog component, pass an isOpen prop to it.
So what if you had one modal that would be controlled by the redux state? Here is a possible implementation:
ModalWindow - will react to state changes and render depending what's in store:
import React from 'react';
import InfoContent from './InfoContent';
import YesOrNoContent from './YesOrNoContent';
import { MODAL_ACTION } from './modal/reducer';
class ModalWindow extends React.Component {
renderModalTitle = () => {
switch (this.props.modalAction) {
case MODAL_ACTION.INFO:
return 'Info';
case MODAL_ACTION.YES_OR_NO:
return 'Are you sure?';
default:
return '';
}
};
renderModalContent = () => {
switch (this.props.modalAction) {
case MODAL_ACTION.INFO:
return <InfoContent />;
case MODAL_ACTION.YES_OR_NO:
return <YesOrNoContent />;
default:
return null;
}
};
render() {
return (
this.props.isModalVisible ?
<div>
<p>{this.renderTitle()}</p>
<div>
{this.renderModalContent()}
</div>
</div>
:
null
);
}
}
export default connect((state) => ({
modalAction: state.modal.modalAction,
isModalVisible: state.modal.isModalVisible,
}))(ModalWindow);
modal reducer it will expose API to show/hide modal window in the application:
export const SHOW_MODAL = 'SHOW_MODAL';
export const HIDE_MODAL = 'HIDE_MODAL';
const INITIAL_STATE = {
isModalVisible: false,
modalAction: '',
};
export default function reducer(state = INITIAL_STATE, action) {
switch (action.type) {
case SHOW_MODAL:
return { ...state, isModalVisible: true, modalAction: action.modalAction };
case HIDE_MODAL:
return { ...state, isModalVisible: false };
default:
return state;
}
}
export const MODAL_ACTION = {
YES_OR_NO: 'YES_OR_NO',
INFO: 'INFO',
};
const showModal = (modalAction) => ({ type: SHOW_MODAL, modalAction });
export const hideModal = () => ({ type: HIDE_MODAL });
export const showInformation = () => showModal(MODAL_ACTION.INFO);
export const askForConfirmation = () => showModal(MODAL_ACTION.YES_OR_NO);
So basically you expose simple API in form of redux action-creators to control the state of your ModalWindow. Which you can later use like:
dispatch(showInformation())
...
dispatch(hideModal())
Of course, there could be more to it like optional configuration that would be passed to action creators or queue for modals.
I use a combination of a hook and a component for this.
Hook:
import { useState } from "react";
import UIkit from "uikit";
export default function useModal() {
const [isOpen, setIsOpen] = useState(false);
const [ref, setRef] = useState(null);
const open = (e) => {
UIkit.modal(ref).show();
setIsOpen(true);
};
const close = (e) => {
UIkit.modal(ref).hide();
UIkit.modal(ref).$destroy(true);
setIsOpen(false);
};
return [setRef, isOpen, open, close];
}
Component:
import React, { forwardRef } from "react";
const Modal = forwardRef(({ children, isOpen, full, close }, ref) => (
<div
ref={ref}
data-uk-modal="container: #root; stack: true; esc-close: false; bg-close: false"
className={`uk-flex-top ${full ? "uk-modal-container" : ""}`}
>
<div className="uk-modal-dialog uk-margin-auto-vertical">
<button
type="button"
className="uk-modal-close-default"
data-uk-icon="close"
onClick={close}
/>
{isOpen && children()}
</div>
</div>
));
export default Modal;
Consumption:
function Demo() {
const [ref, isOpen, open, close] = useModal();
return (
<div>
<button
type="button"
className="uk-button uk-button-default"
onClick={open}
>
upgrade
</button>
<Modal isOpen={isOpen} close={close} ref={ref} full>
{() => (
<div>
<div className="uk-modal-header">
<h2 className="uk-modal-title">title</h2>
</div>
<div className="uk-modal-body">
body
</div>
</div>
)}
</Modal>
</div>
);
}
Read more: https://reactjs.org/docs/integrating-with-other-libraries.html

Resources