Keep React Portal on external window displayed during re-renders - javascript

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.

Related

Next.js : Target container is not a DOM element

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

React long press that works in modern react and don't return "Rendered more hooks than during the previous render."?

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

Content disappears after refreshing the page

I have a blog app that is divided into two parts. The first part is where people can write the actual blog (title, short description, body, click on the submit button) and that blog is than displayed to the screen with all the other blogs. These blogs are clickable and can be viewed. This part works just fine. If people click on a blog they can write comments to it. It works similarly like the part where you can write the blogs (people write a comment, click on the submit button and it is displayed below the blog post). Everything is store in firebase. The problem is when I refresh the page in the comment section, everything disappears and I get no error message. If I don't refresh the comment section everything works perfect, but after refresh everything disappears, but no error message is shown.
Here are the components for the comment section:
CommentHolder is responsible for displaying the comments that are connected with the actual blog post
import React from 'react';
import { projectFirestore } from '../../firebase/config';
import DeleteComment from './DeleteComment'
class CommentHolder extends React.Component {
state = { docs: [] }
_isMounted = false;
componentDidMount = () => {
const fetchDataFromFireBase = async () => {
const getData = await projectFirestore.collection("Comments")
getData.onSnapshot((querySnapshot) => {
var documents = [];
querySnapshot.forEach((doc) => {
documents.push({ ...doc.data(), id: doc.id });
});
if (this._isMounted) {
this.setState({ docs: documents })
}
});
}
fetchDataFromFireBase()
this._isMounted = true;
}
componentWillUnmount = () => {
this._isMounted = false;
}
renderContent() {
// Delete comments
const deleteComment = async (id) => {
projectFirestore.collection('Comments').doc(id).delete().then(() => {
console.log(`Blog with id: ${id} has been successfully deleted!`)
})
}
// Build comments
let user;
if (localStorage.getItem('user') === null) {
user = [];
} else {
user = JSON.parse(localStorage.getItem('user'));
const commentArray = this.state.docs?.filter(value => value.blogID === this.props.param);
const orderedComments = commentArray.sort((a, b) => (a.time > b.time) ? -1 : (b.time > a.time) ? 1 : 0);
const renderComments = orderedComments.map(comment => {
return (
<div key={comment.id} className="card mb-3" >
<div className="card-body">
<div className="row">
<div className="col-sm">
<h6>{`${comment.name} - ${comment.time}`}</h6>
<p>{comment.comment}</p>
</div>
<div className="col-sm text-right">
{user[0].id === comment.userID ? <DeleteComment commentid={comment.id} onDeleteComment={deleteComment} /> : ''}
</div>
</div>
</div>
</div>
)
})
const updateComments = () => {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString)
const id = urlParams.get('id')
const updateComment = projectFirestore.collection('Blogs').doc(id);
return updateComment.update({
'post.comments': commentArray.length
})
}
updateComments()
return renderComments;
}
}
render() {
return (
<div>
{this.renderContent()}
</div>
)
}
}
export default CommentHolder
The AddComment contains the whole section, the text area, the submit button and the container for the comments
import React, { useState } from 'react'
import SubmitComment from './SubmitComment'
import CommentHolder from './CommentHolder';
import { useSelector, useDispatch } from 'react-redux';
const AddComment = ({ param }) => {
const [comment, setComment] = useState('');
const dispatch = useDispatch();
const state = useSelector((state) => state.state);
if(state) {
setTimeout(() => {
setComment('')
dispatch({ type: "SET_FALSE" })
}, 50)
}
return (
<div>
<div>
<div className="row">
<div className="col-sm">
<div className="form-group">
<textarea rows="4" cols="50" placeholder="Comment" className="form-control mb-3" value={comment} onChange={(e) => setComment(e.target.value)} />
</div>
</div>
</div>
</div>
<div className="mb-3">
<SubmitComment comment={comment} param={param} />
</div>
<CommentHolder param={param} />
</div>
)
}
export default AddComment
The SubmitComment is responsible for submitting the comment to the firebase
import React from 'react'
import { projectFirestore } from '../../firebase/config';
import { v4 as uuidv4 } from 'uuid';
import { useDispatch } from 'react-redux';
const SubmitComment = ({ comment, param }) => {
const dispatch = useDispatch();
const onCommentSubmit = () => {
let user;
if (localStorage.getItem('user') === null) {
user = [];
} else {
user = JSON.parse(localStorage.getItem('user'));
projectFirestore.collection('Comments').doc().set({
id: uuidv4(),
comment,
name: `${user[0].firstName} ${user[0].lastName}`,
userID: user[0].id,
blogID: param,
time: new Date().toLocaleString()
})
dispatch({ type: "SET_TRUE" });
}
}
return (
<div>
<button onClick={() => onCommentSubmit()} className='btn btn-primary'>Add comment</button>
</div>
)
}
export default SubmitComment
The DeleteComment just deletes the comment
import React from 'react'
const DeleteComment = ({ commentid, onDeleteComment }) => {
return (
<div>
<button onClick={() => onDeleteComment(commentid)} className='btn btn-outline-danger'>X</button>
</div>
)
}
export default DeleteComment
Do you guys have any suggestions on how to solve this problem? Thank you.

How to prevent rerender a component which gets props from consumer props?

import {
Consumer,
IFormConsumer
} from "react-context-form/src";
import React, { FunctionComponent, useState } from "react";
import Table from './table';
import {Modal} from './modal'
interface ListProps{
// list props
}
interface TargetProps{
// target props
}
interface ValueProps {
// value props
}
interface ReturnProps {
// return props
}
export const List: FunctionComponent<ListProps> = ({
readOnly = false
}) => {
const [formModalOpen, setFormModalOpen] = useState<boolean>(false);
const onFormCancel = () => {
setFormModalOpen(false)
};
const [targetRow, setTargetRow] = useState<TargetProps | undefined>(
undefined
);
const onCreateClick = () => {
setTargetRow(undefined);
setFormModalOpen(true);
};
const handleEdit = (row: any) => {
setTargetRow(row.original);
setFormModalOpen(true);
}
let arr: any = []; // some data
let newArr: any = []; // some data
const fieldOptions = () => {
return [...arr, ...newArr];
};
const getInitialData = (
values: ValueProps
): ReturnProps[] => {
const data = get(values, "gqlMedia");
return data && Array.isArray(data) ? data.slice() : [];
};
return (
<Consumer>
{(consumerProps: IFormConsumer) => {
const initialData = getInitialData(consumerProps.values);
return (
<>
<Modal
isOpen={formModalOpen}
onCancel={onFormCancel}
targetRow={targetRow}
data={initialData}
fieldOptions={fieldOptions}
/>
<Table
initialData={initialData}
handleEdit={handleEdit}
onCreateClick={onCreateClick}
readOnly
fieldOptions={fieldOptions}
/>
<>
)
}}
</Consumer>
);
};
When I move to a page other than the 1st page in the table and edit a row, the edit modal opens. The table is automatically rendering the 1st page in the background. The page should stay when the modal is opened. I don't want the Table component to be rerendered when the modal is opened (the state is changed). Tried using useCallback, useMemo, React.memo. Not able to find a solution.

Button handler to show preview

Iam doing one of the react assignment and I am stuck on the last part of this assignment. The question is like this: Improve on the application in the previous exercise, such that when the names of multiple countries are shown on the page there is a button next to the name of the country, which when pressed shows the view for that country. Here is my code. I tried some functions but couldnot get it so I wonder if someone can help me to cope with this last part..Thank you
import React, { useState, useEffect } from "react";
import axios from "axios";
import ReactDOM from "react-dom";
const App = () => {
const [countries, setCountries] = useState([]);
const [filter, setFilter] = useState("");
const [select, setSelected] = useState([]);
//console.log(countries);
useEffect(() => {
axios.get("https://restcountries.eu/rest/v2/all").then((response) => {
setCountries(response.data);
});
}, []);
const searchHandler = (e) => {
setFilter(e.target.value);
//console.log(filter);
const selected_countries = countries.filter((item) => {
const letter_case=item.name.toLowerCase().includes(filter.toLowerCase())
return letter_case
});
setSelected(selected_countries);
};
const countryLanguages = (languages)=>
languages.map(language => <li key={language.name}>{language.name}</li>)
const showCountries = () => {
if (select.length === 0) {
return <div></div>
} else if (select.length > 10) {
return "Find the specific filter";
}
else if(select.length>1 && select.length<10){
return (select.map(country=>
<div key={country.alpha3code}>{country.name}
<button>Show</button>//this part
</div>)
)
}
else if(select.length===1){
return(
<div>
<h1>{select[0].name}</h1>
<div>capital {select[0].capital}</div>
<div>population {select[0].population}</div>
<h2>languages</h2>
<ul>{countryLanguages(select[0].languages)}</ul>
<img src={select[0].flag} width="100px"/>
<h2>Weather in {select[0].capital}</h2>
</div>
)
}
};
return (
<div>
<h1>Countries</h1>
find countries: <input value={filter} onChange={searchHandler} />
{showCountries()}
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
Create a separate component.
const SingleCountry = ({name}) => {
const [ showDetails, setShowDetails ] = useState(false);
const toggleDetails = () => setShowDetails(!showDetails); //toggles the variable true/false
return ( <div>
<button onClick={toggleDetails}>Show Details</button>
{ /* renders the <div> only if showDetails is true */ }
{ showDetails && <div>These are the details of the country {name}</div> }
</div>)
}
Edit your showCountries component to use the new component.
const showCountries = () => {
if (select.length === 0) {
return <div></div>
} else if (select.length > 10) {
return "Find the specific filter";
}
else if(select.length>1 && select.length<10){
return (select.map(country=> <SingleCountry key={country.alpha3code} name={country.name} />
)
}

Categories