I use Next.js for my react app, and i met this problem while using 'createPortal'. I spent almost a day for this problem, but still not solved... I check this code from only react app, but i can't find any problems. and it drive me crazy.(actually not really same code but almost correct!!)
plz help me guys :(
NavBar.tsx
// NavBar.tsx (to controll Modal.tsx)
const [ modalClick, setModalClick ] = useState(false);
const modalOpen = () => { setModalClick(true); }
const modalClose = () => { setModalClick(false); }
<p id = "link" onClick = { modalOpen }>Register</p>
<div>
{modalClick && <Modal component = {<LogIn/>}
closePortal = { modalClose }
selector = "#portal"/>}
</div>
_documents.tsx
// _documents.tsx
<body>
<Main/>
<NextScript/>
<div id = "portal"/>
</body>
Modal.tsx
// Modal.tsx
import React, { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
const Modal = ({ closePortal, component, selector }: any) => {
const [modalOpen, setModalOpen] = useState(false);
const ref = useRef<any>();
useEffect(() => {
setModalOpen(true);
document.body.style.overflow = "hidden";
if(document){
console.log("plz help me");
ref.current = document.querySelector(selector);
}
return () => {
setModalOpen(false);
document.body.style.overflow = "unset"
};
}, [selector]);
const exit = (e: any) => {
if(e.target === e.currentTarget){
closePortal()
}
}
if(modalOpen){
return createPortal(
<>
////////////
</>,
ref.current
)
}else return null;
};
export default Modal
Related
I am receiving an error of "HomePage.jsx:16 Uncaught TypeError: elements.map is not a function" when trying to change the boxtitle for the array component. I have tried moving around functions and such but nothing seems to work.
I basically only want to change the title for the certain array object with the same boxid. Below is my code.
HomePage.jsx:
import react from 'react';
import { useEffect, useContext } from 'react';
import '../App.css';
import Todobox from './Todobox';
import { ElementContext } from '../ElementContext';
export default function HomePage(){
const { elements, setElements, newElement, elementId } = useContext(ElementContext);
return(
<div className='page-container'>
<div className='header'>
<a className='header-title'>Trello Clone!</a>
<a className='header-button' onClick={newElement}>Create a list</a>
</div>
<div className='element-field'>
{elements.map((element) => <Todobox key={element.boxid} boxid={element.boxid} boxtitle={element.boxtitle}/>)}
</div>
</div>
)
}
Todobox.jsx:
import React from 'react';
import Item from './Item';
import { useState, useContext } from 'react';
import '../App.css';
import { ElementContext } from '../ElementContext';
export default function Todobox({ boxtitle, boxid }){
const { elements, setElements } = useContext(ElementContext);
const [boxheader, setBoxHeader] = useState();
const handleSubmit = (e) => {
const object = elements.find(obj => {
if (obj.boxid === boxid){
setBoxHeader(e.target.value)
return obj
}})
setElements({...object, boxtitle: boxheader})
}
const handleKeydown = (e) => {
if(e.keyCode == 13 && e.shiftKey == false){
e.preventDefault();
handleSubmit(e)
}
}
return(
<div className='element-box'>
<textarea className='element-title-input' placeholder={boxtitle} onKeyDown={handleKeydown}/>
<Item />
<textarea
className='element-input'
type='text'
placeholder={`Add item... ${boxid}`}
onClick={() => {console.log(boxid)}}
/>
</div>
)
}
ElementContext.js:
import React, { createContext, useState } from 'react';
import Todobox from './components/Todobox';
export const ElementContext = createContext();
export const ElementContextProvider = ({children}) => {
const [elements, setElements] = useState([]);
const [elementId, setElementId] = useState(1);
const [title, setTitle] = useState('Add title...');
const [refDict, setRefDict] = useState({});
const newElementId = (elements) =>{
setElementId(elementId + 1);
console.log(elementId)
}
const newElement = () => {
newElementId();
if (!refDict[elementId]) {
setElements(prev => [...prev, { boxtitle: title, boxid: elementId }]);
setRefDict((prev) => ({...prev, [elementId]: true}));
}
console.log(elements);
};
const value = {
elements,
setElements,
newElement,
elementId,
};
return(
<ElementContext.Provider value={value}>
{children}
</ElementContext.Provider>
)
};
Code Sandbox
Any help is appreciated since I am new and still learning! :)
Few points to handle
const newElement = () => {
newElementId(); // this wont update as react batches the state updates
// try using userRef for elementId
// create new element id here, and then set it
// const newId = elementId + 1
// setElementId(newElementId)
if (!refDict[elementId]) {
setElements(prev => [...prev, { boxtitle: title, boxid: elementId }]);
setRefDict((prev) => ({...prev, [elementId]: true}));
}
console.log(elements);
};
on submit
const object = elements.find(obj => {
if (obj.boxid === boxid){
setBoxHeader(e.target.value) // wont update the as React will likely batch the state updates
return obj
}})
setElements({...object, boxtitle: boxheader}) // is this an array ?
instead try
const object = elements?.find(obj => obj.boxid === boxid)
if (object) {
setBoxHeader(e.target.value)
setElements([ object, { boxtitle: e.target.value, boxId: elementId } ]) // what is the structure of array elements
}
you will find the new React documentation about updating arrays useful
currently I am making a navbar that only shows when you scroll up, to prevent useEffect to run everytime when the visible state get changed, I had to use both a ref and a state that is synced together to do comparison in the useEffect, using ref and a state to keep track of a same value seems extremely fishy, is there another way of doing this? one that does not involve triggering useEffect from creating the event handlers everytime the state changes?
import React, { useState, useRef, useEffect } from 'react';
import Link from 'next/link';
const NavbarLink = ({ name, href }: { name: string, href: string }) => {
return (
<Link href={href}>
<a>{ name }</a>
</Link>
);
}
const Navbar = () => {
const scrollYRef = useRef(0);
const visibleRef = useRef(true);
const [visible, setVisible] = useState(true);
useEffect(() => {
const onScroll = (event: Event) => {
event.preventDefault();
if ((window.scrollY < scrollYRef.current) != visibleRef.current) {
visibleRef.current = !visibleRef.current;
setVisible(x => !x);
}
scrollYRef.current = window.scrollY;
}
window.addEventListener('scroll', onScroll);
return () => {
window.removeEventListener('scroll', onScroll);
}
}, []);
return (
<div className={`${!visible && '-translate-y-full'} fixed flex w-full h-32 font-bold text-white transition-all`}>
<NavbarLink name="home" href='/'/>
</div>
);
}
You could use the "latest ref" pattern (here via react-use's useLatest hook), which boxes the latest value of a state atom at each component update time, so you don't need to manage it manually, and you don't need it as a dependency for the hook. (I'd also grab useEvent from react-use if you end up using it.)
import React, { useState, useRef, useEffect } from "react";
import Link from "next/link";
// via https://github.com/streamich/react-use/blob/master/src/useLatest.ts
const useLatest = (value) => {
const ref = useRef(value);
ref.current = value;
return ref;
};
const NavbarLink = ({ name, href }: { name: string; href: string }) => {
return (
<Link href={href}>
<a>{name}</a>
</Link>
);
};
function useVisibleOnScrollUp(initial = true) {
const scrollYRef = useRef(window.scrollY);
const [visible, setVisible] = useState(initial);
const latestVisibleRef = useLatest(visible);
useEffect(() => {
const onScroll = (event: Event) => {
event.preventDefault();
const currentVisible = latestVisibleRef.current;
if (window.scrollY < scrollYRef.current != currentVisible) {
setVisible((x) => !x);
}
scrollYRef.current = window.scrollY;
};
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
};
}, []);
return visible;
}
const Navbar = () => {
const visible = useVisibleOnScrollUp(true);
return (
<div
className={`${
!visible && "-translate-y-full"
} fixed flex w-full h-32 font-bold text-white transition-all`}
>
<NavbarLink name="home" href="/" />
</div>
);
};
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
My goal for this part of the app, is that when a user retrieves information, it's displayed in an external window. I had this working by using a separate component but if I opened a second window, the content in the first portal would disappear. I assumed that had something to do with state. Therefore, I rewrote the parent container with useReducer and no longer reference the child component but try to use createPortal in the parent component. While the windows pop open fine, state is updated fine, the portal never renders the content the first child. Code below.
import React, {useEffect, useState, useReducer} from "react";
import fiostv from "../api/fiostv";
import Accordion from "./Accordion";
import "./App.css";
import ReactDOM from "react-dom";
const initialState = []
function reducer(state, action) {
/*
TODO: needs a 'REMOVE' action,
TODO: not using the open property. Use it or delete it
*/
switch (action.type) {
case "ADD":
return [
...state,
{
win: null,
title: action.payload.device,
content: action.payload.data,
open: false
}
];
default:
return state;
}
}
function ConfigFetcher() {
const [state, dispatch] = useReducer(reducer, initialState);
const [device, setDevice] = useState("");
const [locations, setLocations] = useState(null );
const [selectedOption, setSelectedOption] = useState(null);
const [content, setContent] = useState(null);
const [firstDate, setFirstDate] = useState(null);
const [secondDate, setSecondDate] = useState(null);
async function fetchData(url){
let response = await fiostv.get(url);
response = await response.data;
return response;
}
function formSubmit(e) {
e.preventDefault();
if (!firstDate || !secondDate) {
fetchData(`/backups/${selectedOption}`).then(data => {
if (!state.find(o => o.title === device)) {
dispatch({type: "ADD", payload: {device, data}})
}
});
}
else {
fetchData(`/diffs/${selectedOption}/${firstDate}/${secondDate}`).then(data => {
if (data.includes("Error:")) {
alert(data)
}
else {
setContent(data)
}
});
}
}
const createPortal = () => {
if (state.length > 0 ) {
const obj = state.find(o => o.title === device)
const pre = document.createElement('pre');
const div = document.createElement('div');
div.appendChild(pre)
const container = document.body.appendChild(div);
obj.win = window.open('',
obj.title,
'width=600,height=400,left=200,top=200'
);
obj.win.document.title = obj.title;
obj.win.document.body.appendChild(container);
if (obj) {
return (
ReactDOM.createPortal(
obj.content, container.firstChild
)
);
} else {
return null
}
}
}
useEffect(() => {
createPortal();
}, [state])
const rearrange = (date_string) => {
let d = date_string.split('-');
let e = d.splice(0, 1)[0]
d.push(e)
return d.join('-');
}
function onDateChange(e) {
if (content != null) {
setContent(null);
}
e.preventDefault();
if (e.target.name === "firstDate"){
setFirstDate(rearrange(e.target.value));
}
else {
setSecondDate(rearrange(e.target.value));
}
}
const onValueChange = (e) => {
if (content != null) {
setContent(null);
}
setSelectedOption(e.target.value);
setDevice(e.target.name)
}
const Values = (objects) => {
return (
<form onSubmit={formSubmit} className={"form-group"}>
<fieldset>
<fieldset>
<div className={"datePickers"}>
<label htmlFor={"firstDate"}>First config date</label>
<input name={"firstDate"} type={"date"} onChange={onDateChange}/>
<label htmlFor={"secondDate"}>Second config date</label>
<input name={"secondDate"} type={"date"} onChange={onDateChange}/>
</div>
</fieldset>
{Object.values(objects).map(val => (
<div className={"radio"}
style={{textAlign: "left", width: "100%"}}>
<input type={"radio"}
name={val.sysname}
value={val.uuid}
checked={selectedOption === val.uuid}
onChange={onValueChange}
style={{verticalAlign: "middle"}}
/>
<label
style={{verticalAlign: "middle", textAlign: "left", width: "50%"}}>
{val.sysname}
</label>
</div>
))}
<div className={"formButtons"}>
<button type={"submit"} className={"btn-submit"}>Submit</button>
</div>
</fieldset>
</form>
);
}
useEffect(() => {
fetchData('/devicelocations').then(data => {setLocations(data)});
}, []);
This won't work the way I envisioned due to the nature of SPA's. When opening a second window, the content produced by the component will always "unmount" when the new content is displayed in the new window. The best is to have only one window open and make sure you have clean up code. For example, when a user closes a window, have a listener that cleans up the state with a dispatch. If the user doesn't close a window but another component is rendered, make sure a cleanup function exists in the useEffect hook to clean up any state and close the window.
I am trying to use react hooks to determine if a user has clicked outside an element. I am using useRef to get a reference to the element.
Can anyone see how to fix this. I am getting the following errors and following answers from here.
Property 'contains' does not exist on type 'RefObject'
This error above seems to be a typescript issue.
There is a code sandbox here with a different error.
In both cases it isn't working.
import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
const Menu = () => {
const wrapperRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(true);
// below is the same as componentDidMount and componentDidUnmount
useEffect(() => {
document.addEventListener('click', handleClickOutside, true);
return () => {
document.removeEventListener('click', handleClickOutside, true);
};
}, []);
const handleClickOutside = event => {
const domNode = ReactDOM.findDOMNode(wrapperRef);
// error is coming from below
if (!domNode || !domNode.contains(event.target)) {
setIsVisible(false);
}
}
return(
<div ref={wrapperRef}>
<p>Menu</p>
</div>
)
}
the useRef API should be used like this:
import React, { useState, useRef, useEffect } from "react";
import ReactDOM from "react-dom";
function App() {
const wrapperRef = useRef(null);
const [isVisible, setIsVisible] = useState(true);
// below is the same as componentDidMount and componentDidUnmount
useEffect(() => {
document.addEventListener("click", handleClickOutside, false);
return () => {
document.removeEventListener("click", handleClickOutside, false);
};
}, []);
const handleClickOutside = event => {
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
setIsVisible(false);
}
};
return (
isVisible && (
<div className="menu" ref={wrapperRef}>
<p>Menu</p>
</div>
)
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
I have created this common hook, which can be used for all divs which want this functionality.
import { useEffect } from 'react';
/**
*
* #param {*} ref - Ref of your parent div
* #param {*} callback - Callback which can be used to change your maintained state in your component
* #author Pranav Shinde 30-Nov-2021
*/
const useOutsideClick = (ref, callback) => {
useEffect(() => {
const handleClickOutside = (evt) => {
if (ref.current && !ref.current.contains(evt.target)) {
callback(); //Do what you want to handle in the callback
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
});
};
export default useOutsideClick;
Usage -
Import the hook in your component
Add a ref to your wrapper div and pass it to the hook
add a callback function to change your state(Hide the dropdown/modal)
import React, { useRef } from 'react';
import useOutsideClick from '../../../../hooks/useOutsideClick';
const ImpactDropDown = ({ setimpactDropDown }) => {
const impactRef = useRef();
useOutsideClick(impactRef, () => setimpactDropDown(false)); //Change my dropdown state to close when clicked outside
return (
<div ref={impactRef} className="wrapper">
{/* Your Dropdown or Modal */}
</div>
);
};
export default ImpactDropDown;
Check out this library from Andarist called use-onclickoutside.
import * as React from 'react'
import useOnClickOutside from 'use-onclickoutside'
export default function Modal({ close }) {
const ref = React.useRef(null)
useOnClickOutside(ref, close)
return <div ref={ref}>{'Modal content'}</div>
}
An alternative solution is to use a full-screen invisible box.
import React, { useState } from 'react';
const Menu = () => {
const [active, setActive] = useState(false);
return(
<div>
{/* The menu has z-index = 1, so it's always on top */}
<div className = 'Menu' onClick = {() => setActive(true)}
{active
? <p> Menu active </p>
: <p> Menu inactive </p>
}
</div>
{/* This is a full-screen box with z-index = 0 */}
{active
? <div className = 'Invisible' onClick = {() => setActive(false)}></div>
: null
}
</div>
);
}
And the CSS:
.Menu{
z-index: 1;
}
.Invisible{
height: 100vh;
left: 0;
position: fixed;
top: 0;
width: 100vw;
z-index: 0;
}