ReactDOM.render is not rendering element created through React.createElement - javascript

I created a modal component that utilizes React's context, and hooks for easy usage. But for some reason, the element that I want to render in the modal doesn't appear.
Here's my code for the ModalContainer which contains the provider and the modal
ModalContainer.tsx
export type ModalElement =
| React.ComponentClass<any, any>
| React.FC<any>
| JSX.Element;
const ModalContainer: React.FC = ({ children }) => {
const [visible, setVisible] = useState(false);
const modal = useRef<HTMLDivElement>(null);
const showModal = (element: ModalElement) => {
if (element && modal.current) {
if (React.isValidElement(element)) {
ReactDOM.render(element, modal.current);
} else {
const el = React.createElement(
element as React.ComponentClass<any, any> | React.FC<any>,
{}
);
ReactDOM.render(el, modal.current);
}
setVisible(true);
}
};
const dismissModal = () => {
setVisible(false);
};
return (
<ModalContext.Provider value={{ showModal, dismissModal }}>
{children}
<Modal visible={visible} onClose={dismissModal} ref={modal} />
</ModalContext.Provider>
);
};
export default ModalContainer;
useModal.ts
export const useModal = (element: ModalElement) => {
const context = useContext(ModalContext);
if (!context) {
Error(`Can't get modal context`);
}
const show = useCallback(() => {
context.showModal(element);
}, [context, element]);
const dismiss = useCallback(() => {
context.dismissModal();
}, [context]);
return [show, dismiss];
};
In a way that you'll use it as such:
const DivModal: React.FC = () => {
return <div>Test</div>
}
const Component: React.FC = () => {
const [show, dismiss] = useModal(DivModal);
const onClick = () => {
show();
}
return <button onClick={onClick}/>
}
The problem seems to happen when I create an element using React.createElement in the ModalContainer.tsx found specifically in this line:
const el = React.createElement(
element as React.ComponentClass<any, any> | React.FC<any>,
{}
);
ReactDOM.render(el, modal.current);
The modal renders properly if I replace el in the render function with a test JSX such as <div>test</div>. Can't really figure out what's wrong so I'll post it here just in case anyone knows what the problem is. Thanks!

Related

[React][Mui] How to set anchor on Popover and pass it through props

I have a TableCell Component that contains a Button and a Popover that opens with the Button.
interface Props {
productId: Uuid
postId: string
}
const ApproveButtonCell = ({
productId,
postId,
}: Props) => {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
const formOpen = Boolean(anchorEl)
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
return (
<BaseTableCell>
<ApproveButton onClick={handleOpen} />
<ThemeProvider theme={muiTheme}>
<ApprovePopover
productId={productId}
postId={postId}
open={formOpen}
anchorEl={anchorEl}
onClose={handleClose}
/>
</ThemeProvider>
</BaseTableCell>
)
}
To increase reusability, I want to change my component like this:
interface Props {
Button: ReactNode
Popover: ReactNode
}
const PopoverButtonCell = ({ Button, Popover }: Props) => {
return (
<BaseTableCell>
{Button}
<ThemeProvider theme={muiTheme}>{Popover}</ThemeProvider>
</BaseTableCell>
)
}
export default PopoverButtonCell
And I Use This Component like this:
interface Props {
productId: Uuid
postId: string
}
const ProductTable = () => {
const [approvPopoverAnchorEl, setApprovPopoverAnchorEl] = useState<HTMLElement | null>(
null
)
const approvePopoverOpen = Boolean(approvPopoverAnchorEl)
const handleApprovePopoverOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
setApprovPopoverAnchorEl(event.currentTarget)
}
const handleApprovePopoverClose = () => {
setApprovPopoverAnchorEl(null)
}
return(
<Table>
...
<PopoverButtonCell
Button={<ApproveButton onClick={handleApprovePopoverOpen} />}
Popover={
<ApprovePopover
productId={productId}
postId={postId}
open={approvePopoverOpen}
anchorEl={approvPopoverAnchorEl}
onClose={handleApprovePopoverClose}
/>
}
/>
</Table>
)
}
I thought it would work now, but I could see that the anchor wasn't working properly.
like this
How do I make the Popover stick well to the Button?

Passing the callback function

I have a Modelmenu that is nested in the parent component, it implements the function of opening a modal window by click. Also in this parent component there is a child component to which you need to bind the function of opening a modal window by one more element. I can do this if all the logic is in the parent component, but this is a bad practice, since I will have to duplicate the code on each page in this way. How can I do this? I'm sorry, I'm quite new, I can't understand the callback.
Parent:
const Home: NextPage = () => {
const handleCallback = (handleOpen: any) => {
}
return (
<>
<ModalMenu parentCallback={handleCallback}/>
<Slider handleCallback={handleCallback}/>
</>
)
}
Modal:
export const ModalMenu: FC = (props) => {
const [play, setPlay] = useState<boolean>(false)
const handleOpen = () => {
props.parentCallback(setPlay(!play))
};
const handleClose = () => {
setPlay(false)
setPlay(!play)
};
return
}
Child:
export const Slider: FC= (props) => {
return (
<>
<Image nClick={props.handleCallback}/>
</>
I did as advised in the comments using hook, it works fine, maybe it will be useful to someone. Custom hook is really convenient
export const useModal = () => {
const [play, setPlay] = useState<boolean>(false)
const handleOpen = () => {
setPlay(!play)
};
const handleClose = () => {
setPlay(false)
setPlay(!play)
};
return {
play, handleOpen, handleClose
}
}
If you're looking to pass down values and/or functions from a parent to two or more children, I think it's better to just have the values and functions in the parent
Parent :
const Home: NextPage = () => {
const [play, setPlay] = useState<boolean>(false)
const handleOpen = () => {
setPlay(prev => !prev)
};
const handleClose = () => {
setPlay(false)
setPlay(!play)
};
return (
<>
<ModalMenu handleOpen={handleOpen} handleClose={handleClose} play={play}/>
<MainSlider <ModalMenu handleOpen={handleOpen} handleClose={handleClose} play={play}/>
</>
)
}
if you want to pass an interface to the props in the children for typescript your interface will look something like this
interface iProps {
play : boolean;
handleOpen : () => void;
handleClose : () => void;
}
export const ModalMenu: FC = (props):iProps => {
// you can easily access all you want
const {handleClose, handleOpen, play} = props
return
}
export const Slider: FC= (props): iProps => {
const {handleClose, handleOpen, play} = props
return (
<>
<Image onClick={handleOpen}/>
</>

How do I nest multiple React contexts of the same type

Say I want to create a modal and handle the state through Reacts Context API
export const ModalUIProvider: FC = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const openModal = useCallback(() => setIsOpen(true), [setIsOpen]);
const closeModal = useCallback(() => setIsOpen(false), [setIsOpen]);
const toggleModal = useCallback(
() => setIsOpen(!isOpen),
[isOpen, setIsOpen]
);
const modalContext = useMemo(() => {
return {
isModalOpen: isOpen,
openModal,
closeModal,
toggleModal,
};
}, [isOpen, openModal, closeModal, toggleModal]);
return (
<ModalContext.Provider value={modalContext}>
{children}
</ModalContext.Provider>
);
};
export const useModal = () => {
return useContext<IModalContext>(ModalContext);
};
<ModalUIProvider>
<ParentModal />
</ModalUIProvider>
const { isModalOpen, openModal, closeModal } = useModal();
Now, within this modal I want to have a button, that upon being clicked opens another model ontop of the current one.
How can I do this, without interfering with the wrapping context?

Is it possible to expose a function defined within a React function component to be called in other components?

I'm refactoring some old code for an alert widget and am abstracting it into its own component that uses DOM portals and conditional rendering. I want to keep as much of the work inside of this component as I possibly can, so ideally I'd love to be able to expose the Alert component itself as well as a function defined inside of that component triggers the render state and style animations so that no outside state management is required. Something like this is what I'm looking to do:
import Alert, { renderAlert } from '../Alert'
const CopyButton = () => (
<>
<Alert text="Text copied!" />
<button onClick={() => renderAlert()}>Copy Your Text</button>
</>
)
Here's what I currently have for the Alert component - right now it takes in a state variable from outside that just flips when the button is clicked and triggers the useEffect inside of the Alert to trigger the renderAlert function. I'd love to just expose renderAlert directly from the component so I can call it without the additional state variable like above.
const Alert = ({ label, color, stateTrigger }) => {
const { Alert__Container, Alert, open } = styles;
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
const portalElement = document.getElementById('portal');
const renderAlert = (): void => {
setAlertRendered(false);
setAlertVisible(false);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
useEffect(() => {
renderAlert();
}, [stateTrigger])
const ele = (
<div className={Alert__Container}>
{ alertRendered && (
<div className={`${Alert} ${alertVisible ? open : ''}`}>
<DesignLibAlert label={label} color={color}/>
</div>
)}
</div>
);
return portalElement
? ReactDOM.createPortal(ele, portalElement) : null;
};
export default Alert;
Though it's not common to "reach" into other components and invoke functions, React does allow a "backdoor" to do so.
useImperativeHandle
React.forwardRef
The idea is to expose out the renderAlert function imperatively via the React ref system.
Example:
import { forwardRef, useImperativeHandle } from 'react';
const Alert = forwardRef(({ label, color, stateTrigger }, ref) => {
const { Alert__Container, Alert, open } = styles;
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
const portalElement = document.getElementById('portal');
const renderAlert = (): void => {
setAlertRendered(false);
setAlertVisible(false);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
useEffect(() => {
renderAlert();
}, [stateTrigger]);
useImperativeHandle(ref, () => ({
renderAlert,
}));
const ele = (
<div className={Alert__Container}>
{ alertRendered && (
<div className={`${Alert} ${alertVisible ? open : ''}`}>
<DesignLibAlert label={label} color={color}/>
</div>
)}
</div>
);
return portalElement
? ReactDOM.createPortal(ele, portalElement) : null;
});
export default Alert;
...
import { useRef } from 'react';
import Alert from '../Alert'
const CopyButton = () => {
const ref = useRef();
const clickHandler = () => {
ref.current?.renderAlert();
};
return (
<>
<Alert ref={ref} text="Text copied!" />
<button onClick={clickHandler}>Copy Your Text</button>
</>
)
};
A more React-way to accomplish this might be to abstract the Alert state into an AlertProvider that renders the portal and handles the rendering of the alert and provides the renderAlert function via the context.
Example:
import { createContext, useContext, useState } from "react";
interface I_Alert {
renderAlert: (text: string) => void;
}
const AlertContext = createContext<I_Alert>({
renderAlert: () => {}
});
const useAlert = () => useContext(AlertContext);
const AlertProvider = ({ children }: { children: React.ReactElement }) => {
const [text, setText] = useState<string>("");
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
...
const renderAlert = (text: string): void => {
setAlertRendered(false);
setAlertVisible(false);
setText(text);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
const ele = <div>{alertRendered && <div> ..... </div>}</div>;
return (
<AlertContext.Provider value={{ renderAlert }}>
{children}
// ... portal ...
</AlertContext.Provider>
);
};
...
const CopyButton = () => {
const { renderAlert } = useAlert();
const clickHandler = () => {
renderAlert("Text copied!");
};
return (
<>
<button onClick={clickHandler}>Copy Your Text</button>
</>
);
};
...
function App() {
return (
<AlertProvider>
...
<div className="App">
...
<CopyButton />
...
</div>
...
</AlertProvider>
);
}

How to test arrow function calling in react component with jest?

im newbie in jest/enzyme testing with react and im trying to test react Button Component by passing props and im getting this error Cannot read property 'onIncrement' of undefined.
describe("Name of the group", () => {
const counter = 'pc';
const onIncrement = jest.fn();
const props = {
onIncrement,
counter
};
it("should be clicked ", () => {
const button = shallow(<Button {...{props}}>Increment</Button>);
button.find(".increment").simulate("click");
expect(button).toHaveBeenCalledWith('pc');
});
});
import React from "react";
export const Button = ( props ) => {
return (
<button
id="inc"
className="increment"
onClick={() => props.onIncrement(props.counter)}
>
Increment
</button>
);
};
You need to change this:
export const Button = ({ props }) => {} // component has a property called props
to this:
export const Button = (props) => {}
Otherwise:
// it is not recommended
const button = shallow(<Button {...{props}}>Increment</Button>);
Edit:
This should works:
export const Button = (props) => {
return (
<button
id="inc"
className="increment"
onClick={() => props.onIncrement(props.counter)}
>
Increment
</button>
);
};
describe("Name of the group", () => {
const counter = "pc";
const props = {
onIncrement: jest.fn(),
counter,
};
it("should ", () => {
const button = shallow(<Button {...props}>Increment</Button>);
button.find(".increment").simulate("click");
expect(props.onIncrement).toHaveBeenCalledWith("pc");
});
});

Categories