Check changes before routing in React / Next js - javascript

I am having a Next JS app where there are very simple two pages.
-> Home page
import Header from "../components/header";
const handleForm = () => {
console.log("trigger");
};
export default () => (
<>
<Header />
<h1>Home</h1>
<form onSubmit={handleForm}>
<input type="text" placeholder="Username" />
<input type="password" placeholder="Password" />
<button type="submit"> Login </button>
</form>
</>
);
-> About page
import Header from "../components/header";
export default () => (
<>
<Header />
<h1>About us</h1>
</>
);
Requirement:
-> Home page has a login form
-> If user started typing in any of the fields then without submitting the form, if he tries to move to About us page then a warning needs to be displayed something similar like beforeunload_event.
I am not sure how we can handle it in react as I am new to it.. Kindly please help me to handle a alert if user trying to navigate to other url while editing the form fields..

From my understanding, you can achieve your goal by listen the event routeChangeStart as then throws exception in case of rejecting to move the target url.
I forked above codesandbox and created a simple demo based on your idea which doesn't allow to switch page in case of username having value (form is dirty).
Here is the general idea:
import router from "next/router";
export default () => {
// Assume this value holds the status of your form
const [dirty, setDirty] = React.useState();
// We need to ref to it then we can access to it properly in callback properly
const ref = React.useRef(dirty);
ref.current = dirty;
React.useEffect(() => {
// We listen to this event to determine whether to redirect or not
router.events.on("routeChangeStart", handleRouteChange);
return () => {
router.events.off("routeChangeStart", handleRouteChange);
};
}, []);
const handleRouteChange = (url) => {
console.log("App is changing to: ", url, ref.current);
// In this case we don't allow to go target path like this
// we can show modal to tell user here as well
if (ref.current) {
throw Error("stop redirect since form is dirty");
}
};
return (
// ...
)
}
The link codesandbox is here https://codesandbox.io/s/react-spring-nextjs-routes-forked-sq7uj

Related

Show warning message before unmouting React component

I have a form. I want to show warning before unmounting the form component.
Component is something like this -
import React from "react";
export default function FormComp() {
const sub = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
console.log(formData.get("name"));
//API CALL HERE
};
return (
<div className="test">
<form onSubmit={sub}>
<input name="name" />
<button type="submit">Submit</button>
</form>
</div>
);
}
If the component is unmounted when user goes to a different route, how can i show a warning that form changes will not be saved (along with Yes and No option).As soon as FormComp component is unmounted, form data is cleared.
Are you using react-router? This can be easy with that.
If yes, then you can do something like this:
import { Prompt } from 'react-router'
const FormCompComponent = () => (
<>
<Prompt
when={formIsHalfFilledOut}
message='You have unsaved changes, are you sure you want to leave?'
/>
{/* Component JSX here */}
</>
)
For more details check this out: https://v5.reactrouter.com/core/api/Prompt

Render a react component in a new window

I am developing a Certificate Management System where after all the processes have been done, the user may print a certificate.
I am struggling to implement such that upon clicking the print button, a new tab will open containing the ready to print HTML certificate so that the user will only CTRL + P to have the certificate printed.
How do i render my react certificate component in a new window? Such that i would only pass the props which are the data to be put into the certificate e.g., name, date etc.. like <Certificate name={john} />
I have tried implementing the npm react-new-window but it does not work with
<Button onclick={() => {
<NewWindow>
<CertificateComponent>
</NewWindow>
}}
>
PRINT BUTTON
</Button>
I have looked into react portals but most use cases are for Modals, which is where my "PRINT" button is rendered.
Sorry for the bad english/explanation. Thank you!
New Solution based on CreatePortal
import React, { useEffect, useCallback, useMemo, useState } from "react";
import { render, createPortal } from "react-dom";
const App = () => {
const [isOpen, setOpenState] = useState(false);
const open = useCallback(() => setOpenState(true));
const close = useCallback(() => setOpenState(false));
return (
<div>
<h1>Portals in React</h1>
<button onClick={open}>Open</button>
<button onClick={close}>Close</button>
{isOpen && (
<NewWindow close={close}>
Example <button onClick={close}>Close</button>
</NewWindow>
)}
</div>
);
};
const NewWindow = ({ children, close }) => {
const newWindow = useMemo(() =>
window.open(
"about:blank",
"newWin",
`width=400,height=300,left=${window.screen.availWidth / 2 -
200},top=${window.screen.availHeight / 2 - 150}`
)
);
newWindow.onbeforeunload = () => {
close();
};
useEffect(() => () => newWindow.close());
return createPortal(children, newWindow.document.body);
};
render(<App />, document.getElementById("root"));
There can be multiple approaches for this.
Approach 1:
Create a new route and map the certificateComponent to it, make sure it doesn't have any authentication or any dependency to it.
Store the required data for certificateComponent either in session storage or local storage.
When the user clicks on print button, redirect the user to this new route using window.open("http://localhost:port/newroute").
In certificateComponent read the values stored in session/local storage and map it accordingly.
Approach 2:
Make the certificate component as an overlay which occupies the entire screen which shows up when the user click on print button.
If any UI elements need to be hidden, you can do something as shown below:
printFn = function() {
// show the certificate component
// hide the ui elements not required
// window.print()
}
This worked for me
const myWindow: any = window.open('','', 'height: 500;width:500');
ReactDOM.render(<Yourcomponent prop={propValue} /> , myWindow.document.body);
myWindow.document.close();
myWindow.focus();
myWindow.print();
myWindow.close();

Is it possible to send the state to the page destination at the time of browser back with react-router-dom?

I'm using react.js and react-router-dom to create two pages. Form.js is the page where you enter your name in the form, and Confirmation.js is the page where you confirm the name.
I want to share the state of two classes. So, when you jump to another page from the link button, you will send the state at the same time. The sent state is received in the class constructor as this.state = props.history.location.state.
Many have omitted this code.
//Form.js
import React, { Component } from 'react'
import {Link} from 'react-router-dom'
class Form extends Component {
constructor(props) {
super(props);
const histState = props.history.location.state
this.state = histState == undefined ? {name: this.state} : histState
}
render() {
return (
<div>
<input type="text" onChange={this.handleFormInputChanged} value={this.state.name}/>
<Link to={pathname: "/confirmation" , state: this.state}>Send</Link>
</div>
)
}
}
//Confirmation.js
class Confirmation extends Component {
constructor(props) {
super(props);
this.state = props.history.location.state
}
render() {
return (
<div>
<div>Your Name : <span className="name">{this.state.name}</span></div>
<Link to={pathname: "/form" , state: this.state}>Edit</Link>
</div>
)
}
}
Now I can do what I want to do. However, I noticed that when the user pressed the browser back button on the Confirmation.js page, the state was not sent because it jumped to the Form.js page without pressing the Link component.
As a solution, I added the following code to Confirmation.js.
//Confirmation.js
componentWillUnmount() {
this.props.history.push("/form", this.state)
}
However, when I do a browser back this way and receive a state in the class constructor, props.history.location.state is undefined. And strangely, after a while or reloading, props.history.location.state is set to state normally.
//Form.js
constructor(props) {
...
console.log("Form constructor", props.history.location.state)
}
I want to resolve the time it takes for state to be set as the value of props.history.location.state, is there a solution?
You can pass basic parameters as route segments, like /form/:username, or you could use a query parameter like /form?username=Hiroaki, but passing around data more structured or complex via the url or location history seems inadvisable.
I think you'd save yourself a lot of pain by using context or setting up a simple orthogonal store to keep track of it as the user navigates.
Here's a sketch of how you might do it with context. Assuming the provider is above the router in the component hierarchy, the form state will persist through navigation (though not through page reloads). (I haven't tested any of this code. This is just to give you a sense of it.)
const [formState, setFormState] = useState({});
<FormStateContext.Provider value={formState}>
<Form onChange={setFormState} />
</FormStateContext.Provider>
const Form = ({ onChange }) => {
const formState = useContext(FormStateContext);
return (
<input name="username"
value={formState.username}
onChange={(e) => setFormState({ ...formState, username: e.target.value })}
/>
);
}
const Confirmation = () => {
const formState = useContext(FormStateContext);
return (
<div>Your Name: {formState.username}</div>
);
}
If your components aren't that big, you could do something like this instead of using a different route :
import React from "react";
import { render } from "react-dom";
import "./style.css";
const App = () => {
const [state, setState] = React.useState({isConfirmationMode: false});
const handleChange = e => setState({...state, [e.target.name]: e.target.value});
const confirm = () => {
console.log('confirmed');
// Here send you data or whatever you want
// then redirect wherever you want, I just display the form again
setState({...state, isConfirmationMode: false});
}
const cancel = () => {
// juste display the form again
setState({...state, isConfirmationMode: false});
}
const displayForm = () => (
<div>
name : <input type="text" name="name" value={state.name} onChange={handleChange} />
<button onClick={() => setState({...state, isConfirmationMode: true})}>Send</button>
</div>
);
return state.isConfirmationMode ?
<Confirmation name={state.name} confirm={confirm} cancel={cancel} /> :
displayForm()
};
// Here I created 'confirm' and 'cancel', but you might only need 'cancel'
const Confirmation = ({name, confirm, cancel}) => {
return (
<div>
Are you {name} ?<br />
<button onClick={confirm}>Confirm</button>
<button onClick={cancel}>Cancel</button>
</div>
);
}
render(<App />, document.getElementById("root"));
Here is the repro on Stackblitz. The idea is just to either display the form or a confirmation depending on the state of a simple boolean (I separated the confirmation in another component but here it could be part of the first one).

Firebase Auth Network Error from event.preventDefault() and event.stopPropagation()?

I'm getting the following error when trying to login. The email address exists on Firebase Auth and I'm able to login, but the error weirdly only happens when event.preventDefault() and event.stopPropagation(). Those two lines are listed with the comment "(UNCOMMENT AND ISSUE GOES AWAY)".
Possible Issues:
Is there something else I am missing in my code or did I make a mistake somewhere else?
Error:
Error: A network error (such as timeout, interrupted connection or unreachable host) has occurred.
Login.js
// Imports: Dependencies
import React, { useState } from 'react';
import { Button, Container, Form, Row, Col } from 'react-bootstrap';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
// Imports: Redux Actions
import { loginRequest } from '../../../src/redux/actions/authActions';
// Page: Admin Login
const Login = () => {
// React Hooks: State
const [ email, setEmail ] = useState('');
const [ password, setPassword ] = useState('');
// React Hooks: Redux
const dispatch = useDispatch();
// React Hooks: Bootstrap
const [ validated, setValidated ] = useState(false);
// React Hooks: React Router DOM
let history = useHistory();
// Login To Account
const loginToAccount = (event) => {
// Form Validation Target
const form = event.currentTarget;
// Check Form Validity
if (form.checkValidity() === false) {
// Cancels Event
event.preventDefault();
// Prevents Bubbling Of Event To Parent Elements
event.stopPropagation();
}
else {
// Validate Form
setValidated(true);
// Check If Fields Are Empty
if (
email !== ''
&& password !== ''
&& email !== null
&& password !== null
&& email !== undefined
&& password !== undefined
) {
// Credentials
const credentials = {
email: email,
password: password,
};
// Redux: Login Request
dispatch(loginRequest(credentials, history));
// // Cancels Event (UNCOMMENT AND ISSUE GOES AWAY)
// event.preventDefault();
// // Prevents Bubbling Of Event To Parent Elements (UNCOMMENT AND ISSUE GOES AWAY)
// event.stopPropagation();
}
}
};
return (
<div>
{/* <NavigationBar /> */}
<Container id="login-container">
<div id="login-inner-container">
<div id="login-logo-container">
<p id="login-title">Login</p>
</div>
<Form validated={validated} onSubmit={(event) => loginToAccount(event)}>
<Form.Group controlId="login-email">
<Form.Label className="form-field-title">Email</Form.Label>
<Form.Control
type={'email'}
placeholder={'Email'}
pattern={'[a-z0-9._%+-]+#[a-z0-9.-]+\.[a-z]{2,}$'}
onChange={(event) => setEmail((event.target.value).toLowerCase())}
value={email}
maxLength={50}
required
/>
<Form.Control.Feedback type="invalid">Invalid email</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="login-password">
<Form.Label className="form-field-title">Password</Form.Label>
<Form.Control
type={'password'}
placeholder={'Password'}
onChange={(event) => setPassword(event.target.value)}
value={password}
maxLength={40}
required
/>
<Form.Control.Feedback type="invalid">Required</Form.Control.Feedback>
</Form.Group>
<Button
variant="primary"
type="submit"
id="login-button"
onClick={(event) => loginToAccount(event)}
>Login</Button>
</Form>
</div>
</Container>
</div>
)
};
// Exports
export default Login;
You are registering your `` function as the submit handler for a form:
<Form validated={validated} onSubmit={(event) => loginToAccount(event)}>
When a HTML form is submitted, its default behavior is to send the data to the server as a request that navigates away from the current page. The logic here is that the server handles the request, and send a response to the client that it then renders. That's pretty much how the web worked 20+ years ago, hence it being the default behavior for HTML forms.
So with the commented out preventDefault, your code starts signing in to Firebase and then immediately navigates away (most likely just reloading the same page). This interrupts the sign-in, which is why you see the error message.
By calling event.preventDefault() you indicate that you want to prevent the default behavior (the submitting of the form to the server), since your code is handling that itself (by calling loginRequest).
Calling stopPropagation stops the browser from giving parent HTML elements the chance to act on the event. It typically shouldn't be needed to prevent the form submission, but depends a bit on the HTML that is generated.

Run some code in React after multiple async useState setters

I have a functional React component in which I am using useState to manage state. Normally, it's just a form with two fields - code and env - which the user can manually fill out and submit. However, when the component loads, I also want to check any querystring params and if the appropriate ones exist, I want to populate and submit the form automatically. That way, users can bookmark specific form submissions.
The problem I'm having is that, as we all know, useState setters are async, just like setState in class components. As both form fields are controlled by state, setting both values will kick off multiple renders, so where should I put the code to submit the form so that I'm guaranteed that both state updates have completed?
Here is the form:
And here is a simplified, sanitized version of the code I have:
import React, { useState, useEffect } from "react";
import axios from "axios";
import queryString from "query-string";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import ToggleButtonGroup from "react-bootstrap/ToggleButtonGroup";
import ToggleButton from "react-bootstrap/ToggleButton";
import Card from "react-bootstrap/Card";
/*
* this component will show a spinner or the results from the API when complete
*/
const PortalDisplay = ({ data: portal, isLoading }) => {
if (Object.keys(portal).length === 0 && !isLoading) {
return null;
} else if (isLoading) {
return (
<div>
<p>loading…</p>
</div>
);
} else if (!!portal.id && !isLoading) {
return <div className="card-portal">data goes here</div>;
}
};
/*
* main component
*/
const PortalConfiguration = () => {
const [validated, setValidated] = useState(false);
const [code, setCode] = useState("");
const [env, setEnv] = useState("prod");
const [portalInfo, setPortalInfo] = useState({});
const [isLoading, setIsLoading] = useState(false);
const params = queryString.parse(location.search);
const onSubmitForm = (event) => {
const form = event.currentTarget;
setValidated(true);
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
return;
}
//re-initialize
setIsLoading(true);
setPortalInfo({});
axios
.get(`http://www.example.com/api`)
.then((response) => {
setIsLoading(false);
setPortalInfo({ ...response.data, ...{ baseUrl: baseUrl[env] } });
})
.catch((error) => console.log(error));
event.preventDefault();
event.stopPropagation();
};
useEffect(() => {
if (!!params && !!params.portal && !!params.env) {
if (!/^[a-zA-Z]{3}$/.test(params.portal) || (params.env !== "prod" && params.env !== "demo")) {
console.log(`Specified portal parameters {portal: ${params.portal}} and {env: ${params.env}} are incorrect`);
} else {
// this is where i want to set the portal and env states and submit the form
}
}
}, [params.portal]);
return (
<>
<h1>Your Portal Configuration</h1>
<Card>
<Card.Body>
<Form noValidate validated={validated} inline className="form-portal" onSubmit={onSubmitForm}>
<Form.Group className="form-group-code">
<label className="sr-only" htmlFor="code">
Portal Code
</label>
<Form.Control
type="text"
id="code"
value={code}
required
placeholder="Enter Portal Code (e.g. 'FWU')"
maxLength="3"
onChange={(e) => setCode(e.target.value)}
/>
<Form.Control.Feedback type="invalid">Portal Code must be three letters (a-z)</Form.Control.Feedback>
</Form.Group>
<Form.Group>
<ToggleButtonGroup type="radio" name="env" value={env} onChange={(val) => setEnv(val)}>
<ToggleButton type="radio" name="env" value="prod" variant="primary" className="btn-inline">
Production
</ToggleButton>
<ToggleButton type="radio" name="env" value="demo" variant="primary" className="btn-inline">
Demo
</ToggleButton>
</ToggleButtonGroup>
</Form.Group>
<Button variant="secondary" block="true" className="btn-inline" type="submit">
Submit
</Button>
</Form>
</Card.Body>
</Card>
<PortalDisplay data={portalInfo} isLoading={isLoading} env={env} />
</>
);
};
export default PortalConfiguration;
The line which is commented out and says "this is where i want to set the portal and env states and submit the form" is where I know I have querystring params passed and need to set both states, then submit the form.
FWIW, I have considered the usual answer to the question of how to deal with useState's asynchronicity, which is to handle it in useEffect, scoped to the particular state variable you are interested in. The two problems with that is that 1) I have two state variables that need to be updated so I don't think there's any guarantee that they will be updated in the order I called the setters, creating a possible race condition, and 2) I don't want to call this code every time that code or env updates, which can happen when the user manually interacts with the form. I only want it to be auto-submitted when the component detects the querystring upon loading.

Categories

Resources