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.
Related
I would like to know why loginPassword.length and loginPasswordError is different inside and outside of loginFormPasswordHandler
import React, {useState} from 'react';
import './styles.css'
const App = () => {
const [loginPassword, setLoginPassword] = useState('');
const [loginPasswordError, setLoginPasswordError] = useState();
const [submitController, setSubmitController] = useState(false);
const loginFormSubmitHandler = (e) => {
e.preventDefault();
}
const loginFormPasswordHandler = (e) => {
setLoginPassword(e.target.value);
setLoginPasswordError(loginPassword.length < 8);
console.log('login password length is(inside):'+loginPassword.length+' and the state is '+loginPasswordError)
loginPassword.length > 8 ? setSubmitController(true) : setSubmitController(false);
}
console.log('login password length is(outside):'+loginPassword.length+' and the state is '+loginPasswordError)
return(
<React.Fragment>
<div className="form-wrapper">
<form onSubmit={loginFormSubmitHandler}>
<input className={`${loginPasswordError && 'error'}`} type="password" id="password" name="password" placeholder="Password" onChange={loginFormPasswordHandler} />
<div className={`submit-btn ${submitController ? '' : 'disable'}`}>
<input type="submit" />
</div>
</form>
</div>
</React.Fragment>
);
}
export default App;
I know useState re-runs the entire code when the state is changed. But I can't understand this behavior. I am not sure whether this is a Javascript property or React property.
setState is asynchronous, meaning your login password and error state values might not update immediately after you run setLoginPassword and setLoginPasswordError.
The other line below re-runs on every render, so it will output up to date values.
console.log('login password length is(outside):'+loginPassword.length+' and the state is '+loginPasswordError)
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).
I am writing my problem as a fresh part here.
I made a multi step form where I have on dynamic filed in 1st form, that field is to create password manually or just auto generated.
So my multi step form is working fine going to and forth is fine, but I have to pass the fields to main component so it can check for validation, and I am passing that password too
Here comes the issue
When i pass the password field also then it takes the validation even when I have click on auto generated password
I am passing fields like this fields: ["uname", "email", "password"], //to support multiple fields form
so even not check on let me create password it takes the validation.
When i click let me create password and input some values then click on next and when I comes back the input field sets to hidden again to its initial state I know why it is happening, because when I come back it takes the initial state allover again.
i am fed-up with this thing for now, I have tried many things but didn't work below is my code
import React, { useState, useEffect } from "react";
import Form1 from "./components/Form1";
import Form2 from "./components/Form2";
import Form3 from "./components/Form3";
import { useForm } from "react-hook-form";
function MainComponent() {
const { register, triggerValidation, errors, getValues } = useForm();
const [defaultValues, setDefaultValues] = useState({});
const forms = [
{
fields: ["uname", "email", "password"], //to support multiple fields form
component: (register, errors, defaultValues) => (
<Form1
register={register}
errors={errors}
defaultValues={defaultValues}
/>
)
},
{
fields: ["lname"],
component: (register, errors, defaultValues) => (
<Form2
register={register}
errors={errors}
defaultValues={defaultValues}
/>
)
},
{
fields: [""],
component: (register, errors, defaultValues) => (
<Form3
register={register}
errors={errors}
defaultValues={defaultValues}
/>
)
}
];
const [currentForm, setCurrentForm] = useState(0);
const moveToPrevious = () => {
setDefaultValues(prev => ({ ...prev, ...getValues() }));
triggerValidation(forms[currentForm].fields).then(valid => {
if (valid) setCurrentForm(currentForm - 1);
});
};
const moveToNext = () => {
setDefaultValues(prev => ({ ...prev, ...getValues() }));
triggerValidation(forms[currentForm].fields).then(valid => {
if (valid) setCurrentForm(currentForm + 1);
});
};
const prevButton = currentForm !== 0;
const nextButton = currentForm !== forms.length - 1;
const handleSubmit = e => {
console.log("whole form data - ", JSON.stringify(defaultValues));
};
return (
<div>
<div class="progress">
<div>{currentForm}</div>
</div>
{forms[currentForm].component(
register,
errors,
defaultValues[currentForm]
)}
{prevButton && (
<button
className="btn btn-primary"
type="button"
onClick={moveToPrevious}
>
back
</button>
)}
{nextButton && (
<button className="btn btn-primary" type="button" onClick={moveToNext}>
next
</button>
)}
{currentForm === 2 && (
<button
onClick={handleSubmit}
className="btn btn-primary"
type="submit"
>
Submit
</button>
)}
</div>
);
}
export default MainComponent;
please check my code sand box here you can find full working code Code sandbox
React Hook Form embrace native form validation, which means when your component is removed from the DOM and input state will be removed. We designed this to be aligned with the standard, however we start to realize more and more users used to controlled form get confused with this concept, so we are introducing a new config to retain the unmounted input state. This is still in RC and not released.
useForm({ shouldUnregister: true })
Solution for now:
break into multiple routes and store data in the global store
https://www.youtube.com/watch?v=CeAkxVwsyMU
bring your steps into multiple forms and store data in a local state
https://codesandbox.io/s/tabs-760h9
use keepAlive and keep them alive:
https://github.com/CJY0208/react-activation
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.
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;