How to remove a div on component unmount using react? - javascript

I want to remove a div element on component unmount using react.
I create a div with id portal in usecallback method. I want to remove it on component unmount how can I do it.
below is my code,
function Dialog () {
const [portal, setPortal] = React.useState<HTMLDivElement | null>(
(document.getElementById('portal') as HTMLDivElement) || null
);
const createPortalIfNotExists = React.useCallback(() => {
if (portal === null) {
const el = document.createElement('div');
el.id = 'portal';
document.body.appendChild(el);
setPortal(document.getElementById(
'portal'
) as HTMLDivElement);
}
}, [portal]);
createPortalIfNotExists();
if (portal === null) {
return null;
}
return ReactDOM.createPortal(
<>
<div>
{children}
</div>
</>,
portal
);
}
I have two questions here, can useEffect be instead of usecallback in this case. and how to remove the div with id portal on component unmount.
Could someone help me with this?

By using the React.useEffect internal return method, you can do it. for example:
function Dialog () {
const [portal, setPortal] = React.useState<HTMLDivElement | null>(
(document.getElementById('portal') as HTMLDivElement) || null
);
const createPortalIfNotExists = React.useCallback(() => {
if (portal === null) {
const el = document.createElement('div');
el.id = 'portal';
document.body.appendChild(el);
setPortal(document.getElementById(
'portal'
) as HTMLDivElement);
}
}, [portal]);
React.useEffect(() => {
createPortalIfNotExists();
return () => {
const portalElement = portal || document.getElementById('portal')
portal.remove();
}
}, [])
if (portal === null) {
return null;
}
return ReactDOM.createPortal(
<>
<div>
{children}
</div>
</>,
portal
);
``

Related

Input element losing it's focus after key press when it's controlled from outside the Modal (which uses Portal)

[Solved] My input component is losing focus as soon as I press any key only when its value is controlled from outside the portal
NOTE: I am sorry. While writing this, I found the problem in my code, but I decided to post this anyway
[Reason] I was inlining the close function, so the useEffect hook got triggered every time close changed when the component was rendered again due to state changes and thus calling the activeElement.blur() on each keystroke.
Portal
const root = document.getElementById('root')
const modalRoot = document.getElementById('modal-root')
const Portal = ({ children, className, drawer = false }) => {
const element = React.useMemo(() => document.createElement('div'), [])
React.useEffect(() => {
element.className = clsx('modal', className)
modalRoot.appendChild(element)
return () => {
modalRoot.removeChild(element)
}
}, [element, className])
return ReactDOM.createPortal(children, element)
}
Modal
const Modal = (props) => {
const { children, show = false, close, className } = props
const backdrop = React.useRef(null)
const handleTransitionEnd = React.useCallback(() => setActive(show), [show])
const handleBackdropClick = React.useCallback(
({ target }) => target === backdrop.current && close(),
[]
)
const handleKeyUp = React.useCallback(
({ key }) => ['Escape'].includes(key) && close(),
[]
)
React.useEffect(() => {
if (backdrop.current) {
window.addEventListener('keyup', handleKeyUp)
}
if (show) {
root.setAttribute('inert', 'true')
document.body.style.overflow = 'hidden'
document.activeElement.blur?.() // ! CULPRIT
}
return () => {
root.removeAttribute('inert')
document.body.style.overflow = 'auto'
window.removeEventListener('keyup', handleKeyUp)
}
}, [show, close])
return (
<>
{show && (
<Portal className={className}>
<div
ref={backdrop}
onClick={handleBackdropClick}
onTransitionEnd={handleTransitionEnd}
className={clsx('backdrop', show && 'active')}>
<div className="content">{children}</div>
</div>
</Portal>
)}
</>
)
}
Custom Textfield
const TextField = React.forwardRef(
({ label, className, ...props }, ref) => {
return (
<div className={clsx('textfield', className)}>
{label && <label>{label}</label>}
<input ref={ref} {...props} />
</div>
)
}
)
I was inlining the close function, so the useEffect hook got triggered every time close changed when the component was rendered again due to state changes and thus calling the activeElement.blur() on each keystroke.
In Modal.jsx
...
React.useEffect(() => {
...
if (show) {
root.setAttribute('inert', 'true')
document.body.style.overflow = 'hidden'
document.activeElement.blur?.() // ! CULPRIT
}
...
}, [show, close]) // as dependency
...
<Modal
show={show}
close={() => setShow(false)} // this was inlined
className="some-modal"
>
...
</Modal>
TAKEAWAY
Do not inline functions
Usually there is no reason to pass a function (pointer) as dependency

Cannot read property of null when extracting data from state

I'm trying to display a navigation item when a flag is true, but the problem is, when I try to get the following data from it, it returned me undefined, I created the following for that:
let navigate = useNavigate();
const userSignin = useSelector((state: RootStateOrAny) => state.userSignin);
const { userInfo } = userSignin;
const checkAdmin = useCallback(() => {
if (userInfo) {
if (typeof userInfo.user === "undefined") {
return null;
} else {
return userInfo.user.isAdmin;
}
} else {
return null;
}
}, []);
useEffect(() => {
checkAdmin();
if (!userInfo.user.isAdmin) {
navigate("/");
}
}, [checkAdmin]);
I did the checkAdmin function, because before that I had userInfo.user.isAdmin and it returned me undefined.
{checkAdmin() && (
<NavbarItem
component='li'
onMouseEnter={() => setTopMenuIndex(4)}
onMouseLeave={() => setTopMenuIndex(-1)}
>
<Box
style={{ whiteSpace: "nowrap" }}
component='a'
{...{ href: "/createItem" }}
>
{topMenuIndex === 4 && <Tippy topMenuIndex={topMenuIndex} />}
Admin Dashboard
</Box>
</NavbarItem>
)}
Now I want to make sure that if you don't have that flag, you will get redirected to the homepage, but using the userInfo.user.isAdmin is returning null now. How can I recode this logic to be better or how can I make at least this useEffect work correctly.
Firstly you are passing checkAdmin in useEffect inside an array, but it is a function. According to my knowledge you can only pass state or props to refresh the component or re-render.
I am not sure what exactly the ask was but, according to me.
let navigate = useNavigate();
const userSignin = useSelector((state: RootStateOrAny) => state.userSignin);
const { userInfo } = userSignin;
// Old Node Version
const checkAdmin = () => {
if(userInfo) {
if(userInfo.user) {
return userInfo.user.isAdmin
}
};
return false;
};
// New Node Version
const checkAdmin = () => {
if(userInfo?.user?.isAdmin) {
return userInfo.user.isAdmin
};
return false;
};
useEffect(() => {
if (!checkAdmin()) {
navigate("/");
}
}, [userInfo]);

React: Pass function with a given argument to child

My goal is to create a function in a parent component and pass it to the child component, so I can update state from child to parent.
However, I would like to determine one argument in the parent component already.
Is there any way of doing so? See my example below.
Let´s assume I have the following code
const Parent = ( props ) => {
const [counter, setCounter] = useState(0);
function updateFunc(type, id) {
let obj = {type : "", id : id}
if (type == "red"){
obj.type = 'RED_FLAG';
} else {
obj.type = 'ELSE_FLAG';
}
return obj;
}
return (
<>
// HERE I WOULD LIKE TO PASS ALSO A "RED" ARGUMENT -> IS THIS POSSIBLE?
<Child update = {update}
</>
)
}
You can do that by making an additional function:
<Child update={(id) => {
return updateFunc("red", id);
}}/>
There is a technique called currying. For example, you could have a function that takes the type as an argument and returns a function that takes the id as an argument, which finally returns the object.
const Parent = (props) => {
const [counter, setCounter] = useState(0);
function updateFunc(type) {
return (id) => {
let obj = { type: "", id: id }
if (type == "red") {
obj.type = 'RED_FLAG';
} else {
obj.type = 'ELSE_FLAG';
}
return obj;
}
}
return (
<>
<Child update={update("red")}
</>
)
}

How pass props to children in createPortal function

I created a WindowPortal to open many new external windows/tabs.
https://codesandbox.io/s/modest-goldwasser-q6lig?file=/src/App.js
How can I pass props to a props.children in createPortal function?
I want pass newWindow as a prop for handling resize of new window.
import React, { useState, useRef, useEffect, useCallback, Children, cloneElement } from "react";
import { createPortal } from "react-dom";
import { create } from "jss";
import { jssPreset, StylesProvider, CssBaseline } from "#material-ui/core";
type WindowPortalProps = {
width: number;
height: number;
close: () => void;
id: number;
title: string;
};
const WindowPortal: React.FC<WindowPortalProps> = (props) => {
const [container, setContainer] = useState<HTMLElement | null>(null);
const newWindow = useRef<Window | null>(null);
const { title } = props;
const [jss, setJss] = useState<any>(null);
useEffect(() => {
// Create container element on client-side
setContainer(document.createElement("div"));
}, []);
const close = useCallback(() => {
props.close();
}, [props]);
useEffect(() => {
// When container is ready
if (container) {
setJss(create({ ...jssPreset(), insertionPoint: container }));
// Create window
newWindow.current = window.open(
"",
"",
`width=${props.width},height=${props.height},left=200,top=200,scrollbars,resizable,menubar,toolbar,location`,
) as Window;
// Append container
newWindow.current.document.body.appendChild(container);
newWindow.current.document.title = title;
const stylesheets = Array.from(document.styleSheets);
stylesheets.forEach((stylesheet) => {
const css = stylesheet as CSSStyleSheet;
const owner = stylesheet.ownerNode as HTMLElement;
if (owner.dataset.jss !== undefined) {
// Ignore JSS stylesheets
return;
}
if (stylesheet.href) {
const newStyleElement = document.createElement("link");
newStyleElement.rel = "stylesheet";
newStyleElement.href = stylesheet.href;
newWindow.current?.document.head.appendChild(newStyleElement);
} else if (css && css.cssRules && css.cssRules.length > 0) {
const newStyleElement = document.createElement("style");
Array.from(css.cssRules).forEach((rule) => {
newStyleElement.appendChild(document.createTextNode(rule.cssText));
});
newWindow.current?.document.head.appendChild(newStyleElement);
}
});
// Save reference to window for cleanup
const curWindow = newWindow.current;
curWindow.addEventListener("beforeunload", close);
// Return cleanup function
return () => {
curWindow.close();
curWindow.removeEventListener("beforeunload", close);
};
}
}, [container]);
return (
container &&
newWindow.current &&
jss &&
createPortal(
<StylesProvider jss={jss} sheetsManager={new Map()}>
<CssBaseline />
{props.children} -> how pass newWindow.current as externalWindow props?
</StylesProvider>,
container,
)
);
};
const ExpandedComponentForWindowPortal = ({ externalWindow, ...rest }) => {
const [countOfResize, setCountOfResize] = useState(0);
const doSomething = () => setCountOfResize(countOfResize + 1);
useEffect(() => {
externalWindow.addEventListener("resize", doSomething);
return () => {
externalWindow.removeEventListener("resize", doSomething);
setCountOfResize(0)
}
}, []);
return <></>;
}
const ComponentInExternalWindow = () => {
const closeWindow = () => {}
return (
<WindowPortal
title={"title"}
width={window.innerWidth}
heigth={window.innerHeigth}
key={2}
id={1}
close={closeWindow}
>
<ExpandedComponentForWindowPortal />
</WindowPortal>
)
}
I would suggest using a context for this use case.
In WindowPortalHooks:
const WindowPortalContext = createContext(null);
export const useWindowPortalContext = () => {
return useContext(WindowPortalContext);
};
and in the end of the function wrap the children with the context provider
return (
container &&
createPortal(
<WindowPortalContext.Provider
value={{
close
}}
>
{props.children}
</WindowPortalContext.Provider>,
container
)
);
Then in any child rendered by MyWindowPortal you could use useWindowPortalContext:
function Expanded(props) {
const { close } = useWindowPortalContext();
return (
<div>
<Typography align="center">{props.title}</Typography>
<button type="button" onClick={close}>
close me
</button>
</div>
);
}

State of parent in button in child component always null

I have table with multiple columns. Each column can have modal for filtering.
I want only one modal to be visible at a time.
I pass state of component and setter of state to child component.
I can open it on click on button and I want to be able to close it by clicking same button.
const Parent = () => {
const [search, setSearch] = React.useState(null)
return
{columns.map(x => {
<...>
<Child showSearch={search} setSearch={setSearch} column={x} />
</...>
})}
}
const Child = ({ showSearch, setSearch, column }) => {
const isCurrentShowed = showSearch === column.id
const escFunction = useCallback((event) => {
if (event.keyCode === 27) {
setSearch(null);
}
}, [setSearch]);
const toggleSearch = () => {
console.log("Search variable is always null)
setSearch(showSearch === column.id ? null : column.id)
}
useEffect(() => {
document.addEventListener('keydown', escFunction, false);
return () => {
document.removeEventListener('keydown', escFunction, false);
};
}, [escFunction]);
useEffect(() => {
if (isCurrentShowed && inputRef && inputRef.current) {
inputRef.current.focus();
}
}, [isCurrentShowed]);
return (
<div>
<button onClick={toggleSearch}>
button
</button>
{isCurrentShowed && (
<div>
{column.render('Filter', { inputRef: inputRef})}
</div>
)}
</div>
);
}
In child component variable showSearch is available and I see changed value.
In toggleSearch funtion variable showSearch is always null and I'm not sure why.
You are losing this reference
Swap this line:
<button onClick={toggleSearch}>
With this:
<button onClick={toggleSearch.bind(this)}>
More about context: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this
I think column value is not properly passed to child.
const Parent = () => {
const [search, setSearch] = React.useState(null)
return
{columns.map(x => {
<...>
<Child showSearch={search} setSearch={setSearch} column={x} />
</...>
})}
}
setSearch(showSearch === column.id ? null : column.id)
Care this part. Are you sure of that column.id is not null?
If it is null then always provides the equality of null === null so ternary returns null again.

Categories