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>
);
}
Related
I've been making a game which at the end, requires the user to type their guess. To avoid confusion in my actual project, I created something in codesandbox which demonstrates the problem I'm having. I should add that the game in codesandbox isn't suppose to make much sense. But essentially you just click any box 5 times which generates a random number and when the component mounts, it also creates an array with 5 random number. At the end, you type a number and it checks if both arrays contain the key entered and colors them accordingly. The problem I'm having is that once the guess component is shown, all the hooks states return to their initial states.
Main.tsx
import { Guess } from "./Guess";
import { useHook } from "./Hook";
import { Loading } from "./Loading";
import "./styles.css";
export const Main = () => {
const {loading, count, handleClick, randArr} = useHook()
return (
<div className="main">
{!loading && count < 5 &&
<div className='click-container'>
{Array.from({length: 5}).fill('').map((_, i: number) =>
<div onClick={handleClick} className='box' key={i}>Click</div>
)}
</div>
}
{loading && <Loading count={count} />}
{!loading && count >= 5 && <Guess arr={randArr} />}
</div>
);
}
Hook.tsx
import { useEffect, useState } from 'react'
export const useHook = () => {
type guessType = {
keyNum: number
isContain: boolean
}
const [disable, setDisable] = useState(true)
const [randArr, setRandArr] = useState<number[]>([])
const [initialArr, setInitialArr] = useState<number[]>([])
const [count, setCount] = useState<number>(0)
const [loading, setLoading] = useState(true)
const [guess, setGuess] = useState<guessType[]>([])
const randomNum = () => {
return Math.floor(Math.random() * (9 - 0 + 1) + 0);
}
useEffect(() => {
const handleInitialArr = () => {
for (let i = 0; i < 5; i++) {
let num = randomNum()
setInitialArr((prev) => [...prev, num])
}
}
handleInitialArr()
}, [])
const handleClick = () => {
if (!disable) {
let num = randomNum()
setRandArr((prev)=> [...prev, num])
setCount((prev) => prev + 1)
setDisable(true)
setLoading(true)
}
}
useEffect(()=> {
const handleLoading = () => {
setTimeout(() => {
setLoading(false)
}, 500)
}
const handleRound = () => {
setDisable(false)
}
handleLoading()
handleRound()
}, [count])
const handleKeyUp = ({key}) => {
const isNumber = /^[0-9]$/i.test(key)
if (isNumber) {
if (randArr.includes(key) && initialArr.includes(key)) {
setGuess((prev) => [...prev, {keyNum: key, isContain: true}])
console.log(' they both have this number')
} else {
setGuess((prev) => [...prev, {keyNum: key, isContain: false}])
console.log(' they both do not contain this number ')
}
}
}
console.log(count)
console.log(randArr, ' this is rand arr')
console.log(initialArr, ' this is initial arr')
return {
count,
loading,
handleClick,
randArr,
handleKeyUp,
guess
}
}
Guess.tsx
import React, { useEffect } from "react";
import { useHook } from "./Hook";
import "./styles.css";
type props = {
arr: number[];
};
export const Guess: React.FC<props> = (props) => {
const { handleKeyUp, guess } = useHook();
useEffect(() => {
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keyup", handleKeyUp);
};
}, [handleKeyUp]);
console.log(props.arr, " this is props arr ");
return (
<div className="content">
<div>
<p>Guesses: </p>
<div className="guess-list">
{guess.map((item: any, i: number) =>
<p key={i} className={guess[i].isContain ? 'guess-num-true': 'guess-num-false'} >{item.keyNum}</p>
)}
</div>
</div>
</div>
);
};
Also, here is the codesandbox if you want to take a look for yourself: https://codesandbox.io/s/guess-numbers-70fss9
Any help would be deeply appreciated!!!
Fixed demo: https://codesandbox.io/s/guess-numbers-fixed-kz3qmw?file=/src/my-context.tsx:1582-2047
You're under the misconception that hooks share state across components. The hook will have a new state for every call of useHook(). To share state you need to use a Context.
type guessType = {
keyNum: number;
isContain: boolean;
};
type MyContextType = {
count: number;
loading: boolean;
handleClick: () => void;
randArr: number[];
handleKeyUp: ({ key: string }) => void;
guess: guessType[];
};
export const MyContext = createContext<MyContextType>(null as any);
export const MyContextProvider: FC<PropsWithChildren<{}>> = ({ children }) => {
// Same stuff as your hook goes here
return (
<MyContext.Provider
value={{ count, loading, handleClick, randArr, handleKeyUp, guess }}
>
{children}
</MyContext.Provider>
);
};
export const App = () => {
return (
<div className="App">
<MyContextProvider>
<Page />
</MyContextProvider>
</div>
);
};
export const Main = () => {
const { loading, count, handleClick, randArr } = useContext(MyContext);
...
}
export const Guess: React.FC<props> = (props) => {
const { handleKeyUp, guess } = useContext(MyContext);
...
}
Your handleKeyUp function is also bugged, a good example of why you need to type your parameters. key is a string, not a number. So the condition will always be false.
const handleKeyUp = ({ key }: {key: string}) => {
const num = parseInt(key);
if (!isNaN(num)) {
if (randArr.includes(num) && initialArr.includes(num)) {
setGuess((prev) => [...prev, { keyNum: num, isContain: true }]);
console.log(" they both have this number");
} else {
setGuess((prev) => [...prev, { keyNum: num, isContain: false }]);
console.log(" they both do not contain this number ");
}
}
};
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>
);
}
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!
There's a solution here... Several, actually - but none of them work for React 17.0.2. They all result in
Error: Rendered more hooks than during the previous render.
Even with fixes listed in the comments (Using useref() instead of useState, for instance).
So my question is - how can I have long click/press/tap in React 17.0.2 and newer?
My attempt at fixing it:
//https://stackoverflow.com/questions/48048957/react-long-press-event
import {useCallback, useRef, useState} from "react";
const useLongPress = (
onLongPress,
onClick,
{shouldPreventDefault = true, delay = 300} = {}
) => {
//const [longPressTriggered, setLongPressTriggered] = useState(false);
const longPressTriggered = useRef(false);
const timeout = useRef();
const target = useRef();
const start = useCallback(
event => {
if (shouldPreventDefault && event.target) {
event.target.addEventListener("touchend", preventDefault, {
passive: false
});
target.current = event.target;
}
timeout.current = setTimeout(() => {
onLongPress(event);
//setLongPressTriggered(true);
longPressTriggered.current = true;
}, delay);
},
[onLongPress, delay, shouldPreventDefault]
);
const clear = useCallback(
(event, shouldTriggerClick = true) => {
timeout.current && clearTimeout(timeout.current);
shouldTriggerClick && !longPressTriggered && onClick(event);
//setLongPressTriggered(false);
longPressTriggered.current = false;
if (shouldPreventDefault && target.current) {
target.current.removeEventListener("touchend", preventDefault);
}
},
[shouldPreventDefault, onClick, longPressTriggered]
);
return {
onMouseDown: e => start(e),
onTouchStart: e => start(e),
onMouseUp: e => clear(e),
onMouseLeave: e => clear(e, false),
onTouchEnd: e => clear(e)
};
};
const isTouchEvent = event => {
return "touches" in event;
};
const preventDefault = event => {
if (!isTouchEvent(event)) return;
if (event.touches.length < 2 && event.preventDefault) {
event.preventDefault();
}
};
export default useLongPress;
RandomItem.js:
import React, {useEffect, useState} from 'react';
import Item from "../components/Item";
import Loader from "../../shared/components/UI/Loader";
import {useAxiosGet} from "../../shared/hooks/HttpRequest";
import useLongPress from '../../shared/hooks/useLongPress';
function RandomItem() {
let content = null;
let item = useAxiosGet('collection');
if (item.error === true) {
content = <p>There was an error retrieving a random item.</p>
}
if (item.loading === true) {
content = <Loader/>
}
if (item.data) {
const onLongPress = useLongPress();
return (
content =
<div>
<h1 className="text-6xl font-normal leading-normal mt-0 mb-2">{item.data.name}</h1>
<Item name={item.data.name} image={item.data.filename} description={item.data.description}/>
</div>
)
}
return (
<div>
{content}
</div>
);
}
export default RandomItem;
The (unedited) useLongPress function should be used similar to the following example:
import React, { useState } from "react";
import "./styles.css";
import useLongPress from "./useLongPress";
export default function App() {
const [longPressCount, setlongPressCount] = useState(0)
const [clickCount, setClickCount] = useState(0)
const onLongPress = () => {
console.log('longpress is triggered');
setlongPressCount(longPressCount + 1)
};
const onClick = () => {
console.log('click is triggered')
setClickCount(clickCount + 1)
}
const defaultOptions = {
shouldPreventDefault: true,
delay: 500,
};
const longPressEvent = useLongPress(onLongPress, onClick, defaultOptions);
return (
<div className="App">
<button {...longPressEvent}>use Loooong Press</button>
<span>Long press count: {longPressCount}</span>
<span>Click count: {clickCount}</span>
</div>
);
}
Be sure to pass in the onLongPress function, onClick function, and the options object.
Here is a codesandbox with React 17.0.2 with a working example of useLongPress: https://codesandbox.io/s/uselongpress-forked-zmtem?file=/src/App.js
I need to make a click on the button in one component and on this click call a function in the adjacent one. What's the easiest way?
I implemented like this. https://stackblitz.com/edit/react-l5beyi But I think you can do it much easier. React is new to me, and this construction looks strange ...
const App = () => {
const [isAdded, setIsAdded] = useState(false);
function handleClick(status) {
setIsAdded(status)
}
return (
<div>
<ComponentFirst
HandleClick={handleClick}
/>
<ComponentSecond
isAdded={isAdded}
handleCreate={handleClick}
/>
</div>
);
}
const ComponentFirst = ({ HandleClick }) => {
return (
<button
onClick={HandleClick}
>button</button>
)
}
const ComponentSecond = (props) => {
let { isAdded, handleCreate } = props;
const result = () => {
alert('work')
console.log('work')
}
React.useEffect(() => {
if (isAdded) {
result()
handleCreate(false);
}
}, [isAdded, handleCreate]);
return (
<></>
)
}
In your (contrived, I suppose) example the second component doesn't render anything, so it doesn't exist. The work should be done by the parent component:
const App = () => {
const handleClick = React.useCallback((status) => {
alert(`work ${status}`);
// or maybe trigger some asynchronous work?
}, []);
return (
<div>
<ComponentFirst handleClick={handleClick} />
</div>
);
};
const ComponentFirst = ({ handleClick }) => {
return <button onClick={() => handleClick("First!")}>button</button>;
};
You can also use a CustomEvent to which any component can listen to.
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
return (
<div>
<ComponentFirst />
<ComponentSecond />
</div>
);
}
const ComponentFirst = () => {
const handleClick = (e) => {
// Create the event.
const event = new CustomEvent("myCustomEventName", {
detail: "Some information"
});
// target can be any Element or other EventTarget.
window.dispatchEvent(event);
};
return <button onClick={handleClick}>button</button>;
};
const ComponentSecond = (props) => {
function eventHandler(e) {
console.log("Dispatched Detail: " + e.detail);
}
//Listen for this event on the window object
useEffect(() => {
window.addEventListener("myCustomEventName", eventHandler);
return () => {
window.removeEventListener("myCustomEventName", eventHandler);
};
});
return <></>;
};