Show warning message before unmouting React component - javascript

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

Related

useState is not updating the DOM in my React todo list app

I am trying to learn the basics of React and thought that making a todo list app would be a great first project to help me learn.
I have a basic form to add todos, but, when enter is clicked, the DOM does not change. Here is my app.js code, which I think is where my error might be:
import AddTodoForm from './components/AddTodoForm.js';
import TodoList from './components/TodoList.js';
import { dataList } from './components/AddTodoForm.js';
import { useState } from 'react';
function App() {
const[list, setList] = useState([]);
function update(){
setList(dataList);
console.log(list);
console.log("update function has run.")
}
return (
<div>
<AddTodoForm update = {update} />
<h1>My Todos</h1>
<TodoList todos={list} />
</div>
);
}
export default App;
Here is the code for TodoList.js as somebody had asked for it:
import Todo from './Todo';
function TodoList(props) {
return (
<ul>
{props.todos.map((todo) => (
<Todo
key = {todo.id}
id= {todo.id}
text= {todo.text}
/>
))}
</ul>
)
}
export default TodoList;
here is the AddTodoForm.js:
import { useRef, useState } from 'react';
var idCounter = 1;
export const dataList = [];
function AddTodoForm(props){
const titleInputRef = useRef();
function submitHandler(event){
event.preventDefault();
const enteredTitle= titleInputRef.current.value;
const todoData = {
text: enteredTitle,
id: idCounter,
}
idCounter++;
console.log(todoData);
dataList.push(todoData);
}
return (
<div className="card">
<h2>Add New Todos</h2>
<form onSubmit={(event) => {submitHandler(event); }}>
<div>
<label htmlFor="text">New Todo: </label>
<input type="text" required id="text" ref={titleInputRef}></input>
</div> <br />
<div>
<button className="btn" onClick = {props.update}>Add Todo</button>
</div>
</form>
</div>
)
}
export default AddTodoForm;
I have checked the log and the update function runs. Also, if I make a slight change to my code, the todos I had entered will appear on the screen but I am not sure why the DOM does not change when the update function runs.
This is my first post on here so I wasn't sure how much of my code to include. Please ask if you need the code from my other components.
Many thanks in advance :)
Calling dataList.push(todoData) won't change dataList itself, only its content, and React doesn't check the content to update the DOM. You could use the Spread syntax to have a completely new dataList.
You could even get rid of that dataList, and use the empty array given to useState. Update your update function slightly, and it should work:
import AddTodoForm from "./components/AddTodoForm.js";
import TodoList from "./components/TodoList.js";
import { useState } from "react";
function App() {
const [list, setList] = useState([]);
function update(text) {
// this is for learning; consider using a proper id in real world
setList([...list, { text: text, id: list.length + 1 }]);
}
return (
<div>
<AddTodoForm update={update} />
<h1>My Todos</h1>
<TodoList todos={list} />
</div>
);
}
export default App;
import { useRef } from "react";
function AddTodoForm(props) {
const titleInputRef = useRef();
function submitHandler(event) {
event.preventDefault();
props.update(titleInputRef.current.value);
}
return (
<div className="card">
<h2>Add New Todos</h2>
<form onSubmit={submitHandler}>
<div>
<label htmlFor="text">New Todo: </label>
<input type="text" required id="text" ref={titleInputRef}></input>
</div>
<br />
<div>
<button className="btn">Add Todo</button>
</div>
</form>
</div>
);
}
export default AddTodoForm;
In App.js, I have removed the update function and instead sent the list and setList as props to the AddTodoForm component.
import AddTodoForm from './components/AddTodoForm.js';
import TodoList from './components/TodoList.js';
import { useState } from 'react';
function App() {
const[list, setList] = useState([]);
return (
<div>
<AddTodoForm setList = {setList} list = {list}/>
<h1>My Todos</h1>
<TodoList todos={list} />
</div>
);
}
export default App;
In ./components/AddTodoForm.js add this peice of code inside the AddTodoForm function.
const update = ()=>{
props.setList(datalist);
console.log(props.list);
console.log("The update function has run.")
}
I hope this might help.
I am going to post my opinion to help you with my knowledge about React. In your above code, you cannot render updated state(list) to reflect TodoList component.
In hook, useEffect insteads 2 component lifecycles of class component idea. I mean, you should reflect useEffect with updated states(list).
useEffect is similar to componentDidMount and componentDidUpdate.
Like this:
enter code here
useEfect(()=>{
setList(dataList);
console.log(list);
console.log("update function has run.")
},[list])

Check changes before routing in React / Next js

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

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).

React component unnecessarily re-renders when I enter input in forms

I have a react component which manage user logging in and out, when user type email and password in the login field the whole component (Navbar) re-render to Dom in every keystroke unnecessarily thus reduces speed.
How can I prevent Navbar from re-rendering when user type their credential in login fild ?
import React, { useContext,useState } from 'react';
import { Postcontext } from '../contexts/Postcontext';
import axios from 'axios';
const Navbar = () => {
const { token,setToken } = useContext(Postcontext);
const [email,setEmail] = useState(''); **state manages user email for login**
const [password,setPassword] = useState(''); **state manages user password for login**
const[log,setLog] = useState(true) **state manages if user logged in or not based on axios post request**
const login=(e)=>{
//function for login using axios
})
}
const logout=(e)=>{
//function for logout using axios
}
return (
<div className="navbar">
{log?(
<form>
<input value={email} type="text" placeholder="email" onChange={(e)=>setEmail(e.target.value)}/>
<input value={password} type="text" placeholder="password" onChange={(e)=>setPassword(e.target.value)}/>
<button onClick={login}>login</button>
</form>
):(
<button onClick={logout}>logout</button>
)}
</div>
);
}
export default Navbar;
It is because it is same component which needs re-render to reflect input text changes. If you want your email to change but not effect Navbar then create a child component and move inputs into that component, manage input values using useState() there in child component and when you finally submit and user is logged in then you can either update some global state like redux store or global auth context to reflect and rerender Navbar.
So, I had the same issue and I was able to solve it using useRef and useCallback and I will try to explain in Q&A form. Sorry if I am not that clear, this is my first StackOverFlow comment and I am a beginner in React :)
Why useRef?
React re-renders every time it sees a component has updated by checking if previous and current object are same or not. In case of useRef it checks the object Id only and not the content inside it i.e. value of current inside the Ref component. So if you change the value of current React will not consider that. (and that's what we want)
Why useCallback?
Simply because it will run only when we call it or one (or more) of the dependencies have changed. As we are using Ref so it won't be called when the current value inside it has changed.
More info: https://reactjs.org/docs/hooks-reference.html
Based on above info your code should look like this (only doing login part):
import React, { useContext, useRef } from 'react';
const App = () => {
const emailRef = useRef(null);
const passwordRef = useRef(null);
const logRef = useRef(null);
const loginUpdate = useCallback( async (event) => {
event.preventDefault();
// Your logic/code
// For value do:
// const email = emailRef.current.value;
}, [emailRef, passwordRef, logRef]);
return (
<div className="navbar">
{log?(
<form>
<input
ref={emailRef}
type="text"
placeholder="email"
/>
<input
ref={passwordRef}
type="text"
placeholder="password"
/>
<button onClick={loginUpdate}>login</button>
</form>
):(
// Not doing this part because I am lazy :)
<button onClick={logout}>logout</button>
)}
</div>
);
}
export default App;
Had a few typos. It works for me
https://codesandbox.io/s/cold-sun-s1225?file=/src/App.js:163-208
import React, { useContext,useState } from 'react';
// import { Postcontext } from '../contexts/Postcontext';
// import axios from 'axios';
const App = () => {
// const { token,setToken } = useContext();
const [email,setEmail] = useState('');
const [password,setPassword] = useState('');
const[log,setLog] = useState(true)
const login=(e)=>{
//function for login using axios
}
const logout=(e)=>{
//function for logout using axios
}
return (
<div className="navbar">
{log?(
<form>
<input value={email} type="text" placeholder="email" onChange={(e)=>setEmail(e.target.value)}/>
<input value={password} type="text" placeholder="password" onChange={(e)=>setPassword(e.target.value)}/>
<button onClick={login}>login</button>
</form>
):(
<button onClick={logout}>logout</button>
)}
</div>
);
}
export default App;

Redux forms: correct way to populate form with data from top component with axios

I'm using redux forms (just trying to learn). And axios to get my account data and populate it into form.
I've made a sandbox: https://codesandbox.io/s/mzlrv1n988
From example: https://redux-form.com/7.3.0/examples/initializefromstate/ everything seems to be clear and obvious. But it has nothing to deal with a real application: I refactored code a bit and i have top-level components for add && edit actions, who deal with server data and data from form. And I have a form component: it's goal is a form UI && validations.
How can I populate my form with data in top level component?
I mean this part of code in EditForm component:
getAccount = () =>
axios.get("https://jsonplaceholder.typicode.com/posts/1").then(Account => {
loadAccount(Account);
});
also form component:
import React from "react";
import { connect } from "react-redux";
import { Field, reduxForm } from "redux-form";
import { load as loadAccount } from "./account";
const data = {
// used to populate "account" reducer when "Load" is clicked
body: "testtest",
title: "yep!"
};
let AccountForm = props => {
const { handleSubmit, load, pristine, reset, submitting } = props;
return (
<form onSubmit={handleSubmit}>
<div>
<button type="button" onClick={() => load(data)}>
Load Account (example)
</button>
</div>
<div>
<label>body</label>
<div>
<Field name="body" component="input" type="text" placeholder="body" />
</div>
</div>
<div>
<label>title</label>
<div>
<Field
name="title"
component="input"
type="text"
placeholder="title"
/>
</div>
</div>
<div>
<button type="submit" disabled={pristine || submitting}>
Submit
</button>
<button type="button" disabled={pristine || submitting} onClick={reset}>
Undo Changes
</button>
</div>
</form>
);
};
// Decorate with reduxForm(). It will read the initialValues prop provided by connect()
AccountForm = reduxForm({
form: "initializeFromState" // a unique identifier for this form
})(AccountForm);
// You have to connect() to any reducers that you wish to connect to yourself
AccountForm = connect(
state => ({
initialValues: state.account.data // pull initial values from account reducer
}),
{ load: loadAccount } // bind account loading action creator
)(AccountForm);
export default AccountForm;
If this can be done without using redux schema - it's even better, I do not need a huge part of logic here...
Also one small question: how to disable 'submit' button, while data is loading from the server?
If you look at your sandbox with the redux devtools you can see that the state is still empty after the axios call completes. This is because you are directly calling the reducer function instead of passing via a dispatch.
I edited the pen to illustrate the solution: https://codesandbox.io/s/o94p6ono3z
For the disable button you have to handle the usual redux process yourself: set a variable in the reducer before axios runs, unset it when axios is done. Then pass down that variable as you normally do in react/redux. Then set the button as disabled if that variable is true.
Meaning you do it like in a normal redux context no matter if you have a redux-form. What redux-form offers is the submitting variable that you can use to disable the button while the form is submitting.

Categories

Resources