Must submit twice for props.history.push to take effect - javascript

I am currently messing around with client-side form validation. There is no back end or anything along those lines. I have an on submit function, and I am trying to push the user to the home page once a valid form has been submitted using props.history.push. For some reason, I have to submit the form twice for the on submit function to actually push the user into the home page. I am unsure of why this is happening. Below I am going to provide my useForm hook, my Login page, and my validation function I am using. I feel like the answer might be obvious, but I just can't find the issue.
useForm Hook:
import { useState } from "react"
const INITIAL_STATE = {
email: "",
password: ""
}
const UseForm = (ValidateLogin, props) => {
const [formData, setFormData] = useState(INITIAL_STATE)
const [errors, setErrors] = useState({})
const [user, setUser] = useState(null)
const handleChange = field => e => {
setFormData({ ...formData, [field]: e.target.value })
}
const handleSubmit = event => {
event.preventDefault()
setUser(formData)
setErrors(ValidateLogin(formData))
user && !errors.email && !errors.password && props.history.push("/")
}
return {
handleChange,
handleSubmit,
formData,
user,
errors
}
}
export default UseForm
Login Page:
import React from "react"
import UseForm from "../Login/UseForm"
import ValidateLogin from "../Login/ValidateLogin"
import { makeStyles } from "#material-ui/core/styles"
// Material UI
import TextField from "#material-ui/core/TextField"
import Button from "#material-ui/core/Button"
import Typography from "#material-ui/core/Typography"
import Container from "#material-ui/core/Container"
const useStyles = makeStyles(theme => ({
form: {
textAlign: "center",
width: "100%", // Fix IE 11 issue.
marginTop: theme.spacing(1),
position: "relative"
},
logo: {
width: 80,
margin: "20px auto 20px auto"
},
paper: {
marginTop: theme.spacing(8),
display: "flex",
flexDirection: "column",
alignItems: "center"
},
submit: {
margin: theme.spacing(3, 0, 2),
position: "relative"
},
progress: {
position: "absolute"
},
customError: {
color: "red",
fontSize: "0.8rem",
width: "100%",
position: "absolute"
}
}))
const Login = props => {
const classes = useStyles()
const { handleChange, handleSubmit, formData, user, errors } = UseForm(
ValidateLogin,
props
)
const isInvalid = !formData.email || !formData.password
return (
<div style={{ textAlign: "center" }}>
<Typography variant='h5' gutterBottom>
Welcome, Please Login
</Typography>
<Container component='main' maxWidth='xs'>
<form className={classes.form} onSubmit={handleSubmit}>
<TextField
variant='outlined'
fullWidth
margin='normal'
label='Email'
type='text'
name='email'
value={formData.email}
error={errors.email ? true : false}
helperText={errors.email}
onChange={handleChange("email")}
/>
<TextField
variant='outlined'
margin='normal'
fullWidth
label='Password'
type='password'
name='password'
value={formData.password}
error={errors.password ? true : false}
helperText={errors.password}
onChange={handleChange("password")}
/>
<br />
<Button
variant='outlined'
color='primary'
type='submit'
disabled={isInvalid}
className={classes.submit}
>
Submit
</Button>
</form>
</Container>
<br />
{user &&
!errors.email &&
!errors.password &&
JSON.stringify(user, null, 2)}
</div>
)
}
export default Login
Validate Login:
export default function ValidateLogin(formData) {
let errors = {}
if (!formData.email) {
errors.email = "Email address is required"
} else if (!/\S+#\S+\.\S+/.test(formData.email)) {
errors.email = "Email address is invalid"
}
if (!formData.password) {
errors.password = "Password is required"
} else if (formData.password.length < 6) {
errors.password = "Password must be longer than 6 characters"
}
return errors
}
Any helpful suggestions or feedback would be greatly appreciated!! I apologize for the long-winded question. Thanks for taking the time to read this.

It looks like your issue is the conditional in this block:
setUser(formData)
setErrors(ValidateLogin(formData))
user && !errors.email && !errors.password && props.history.push("/")
user and errors will not be updated by the time this condition is evaluated because state setters are async. Here is a more detailed explanation.
This is why it works on the second submit. The values have then been updated, and the condition passes.
This may not be the desired end solution, but it should solve the problem for you.
const handleSubmit = event => {
event.preventDefault()
setUser(formData)
// Use temp variable so that you can use it immediately below
const tempErrors = ValidateLogin(formData);
setErrors(tempErrors)
// Use the formData directly and the tempErrors
formData && !tempErrors.email && !tempErrors.password && props.history.push("/")
}

Related

In React , why does the input field lose focus after typing a character?

i am using react Mui for components ,not getting any errors in chrome inspector or terminal
how can i slove this
I get no errors from either eslint nor Chrome Inspector.
Submitting the form itself works as does the actual input field when it is located either in the render's return or while being imported as a separate component but not in how I have it coded below.
Why is this so?
Here is my code :
import React, { useState } from "react";
import {
Box,
Container,
Stack,
TextField,
Typography,
Button,
Divider,
} from "#mui/material";
import { styled } from "#mui/material/styles";
import { Link, useNavigate } from "react-router-dom";
const Register = () => {
const Curve = styled("div")(({ theme }) => ({
height: "35vh",
position: "absolute",
top: 0,
left: 0,
width: "100%",
background: `linear-gradient(120deg,${theme.palette.secondary.light},${theme.palette.secondary.main})`,
zIndex: -1,
borderBottomLeftRadius: "30px",
borderBottomRightRadius: "30px",
}));
const ProfileBox = styled(Box)(({ theme }) => ({
// margin: theme.spacing(18, 1),
background: theme.palette.primary.dark,
border: `solid 0.8px ${theme.palette.primary.light}`,
borderRadius: "10px",
padding: theme.spacing(3),
}));
const navigate = useNavigate();
const handleRegister = (e) => {
e.preventDefault();
navigate("/");
};
const [values, setValues] = useState({
name: "",
email: "",
password: "",
conpassword: "",
phone: "",
proffesion: "",
});
const [err, setErr] = useState({});
const [valid, setValid] = useState(false);
const handleChange = (e) => {
const { id, value } = e.target;
setValues(() => ({
[id]: value,
}));
setValid(() => true);
};
const validate = () => {
return setErr(checkErr());
function checkErr() {
const error = {};
if (values.name.length === 0) error.name = "Name nedded";
if (values.password.length === 0) error.password = "Password nedded";
if (!values.email.match(/^[^\s#]+#[^\s#]+\.[^\s#]+$/))
error.email = "Invalid email";
if (values.password.length < 6)
error.password = "Password must be longer than 6 charectors";
if (values.password !== values.conpassword)
error.conpassword = "Password doesn't match";
if (values.email.length === 0) error.email = "Email nedded";
if (values.phone.length === 0) error.phone = "Phone number nedded";
if (values.phone.length < 10) error.phone = "Invalid number";
if (values.proffesion.length === 0)
error.proffesion = "Proffesion nedded ex: lawyer ,student";
return error;
}
};
return (
<Container maxWidth="sm">
<Curve></Curve>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
height: "30vh",
alignItems: "center",
}}
>
<Typography variant="h6" algin="center">
Register
</Typography>
<Typography variant="caption" algin="center">
your data is secured with us
</Typography>
</Box>
<ProfileBox>
<Stack spacing={2}>
<TextField
variant="standard"
label="Name"
color="secondary"
value={values.name}
onChange={handleChange}
onBlur={validate}
id="name"
error={err.name}
helperText={err.name}
/>
<TextField
type="email"
variant="standard"
label="Email"
color="secondary"
id="email"
value={values.email}
onChange={handleChange}
onBlur={validate}
error={err.email}
helperText={err.email}
/>
<TextField
type="password"
variant="standard"
label="Password"
color="secondary"
value={values.password}
onChange={handleChange}
onBlur={validate}
id="password"
error={err.password}
helperText={err.password}
/>
<TextField
type="password"
variant="standard"
label="Conform password"
color="secondary"
value={values.conpassword}
onChange={handleChange}
onBlur={validate}
id="conpassword"
error={err.conpassword}
helperText={err.conpassword}
/>
<TextField
type="tel"
variant="standard"
label="Phone"
color="secondary"
value={values.phone}
onChange={handleChange}
onBlur={validate}
id="phone"
error={err.phone}
helperText={err.phone}
/>
<TextField
variant="standard"
label="Proffestion"
color="secondary"
value={values.proffesion}
onChange={handleChange}
onBlur={validate}
id="proffesion"
error={err.proffesion}
helperText={err.proffesion}
/>
<Button
variant="contained"
color="secondary"
sx={{
color: "primary.main",
}}
onClick={handleRegister}
>
Signup
</Button>
<Divider />
<Typography variant="caption" algin="center">
Allready have account{" "}
<span>
<Link to="/login" style={{ color: "var(--secondary)" }}>
Login
</Link>
</span>
</Typography>
</Stack>
</ProfileBox>
</Container>
);
};
export default Register;
Try to change your handleChange function like this:
const handleChange = (e) => {
const { id, value } = e.target;
setValues(() => (prevState => {
return {
...prevState,
[id]: value,
}
}));
setValid(true);
};
This is happening because you're creating components inside your Register component. This pattern is really bad for performance and prone to bugs exactly like your question.
On every type you're changing the state of the Register component, it re-renders itself, and re-creates Curve and ProfileBox from scratch, including their children (input fields). Which causes them to reset all their and their children's state, including focus.
You need to move Curve and ProfileBox outside of it, it will fix the issue.
const Curve = styled('div')(({ theme }) => ({
... // the same
}));
const ProfileBox = styled(Box)(({ theme }) => ({
... // the same
}));
const Register = () => {

Keyboard isn't showing on IOS emulator in react native?

I don't know why only on IOS emulator keyboard is not showing But on android it's working fine. I am trying to take user input for authentication and sending the data to the real-time database firebase.
import React, { useState, useReducer, useEffect, useCallback } from "react";
import {
KeyboardAvoidingView,
ScrollView,
View,
Button,
StyleSheet,
ActivityIndicator,
Alert,
} from "react-native";
import { useDispatch } from "react-redux";
import Input from "../../components/UI/Input";
import Card from "../../components/UI/Card";
import Colors from "../../constants/Colors";
import * as authActions from "../../store/actions/Auth";
const FORM_INPUT_UPDATE = "FORM_INPUT_UPDATE";
const formReducer = (state, action) => {
if (action.type === FORM_INPUT_UPDATE) {
const updatedValues = {
...state.inputValues,
[action.input]: action.value,
};
const updatedValidities = {
...state.inputValidities,
[action.input]: action.isValid,
};
let updatedIsFormValid = true;
for (const key in updatedIsFormValid) {
updatedIsFormValid = updatedIsFormValid && updatedValidities[key];
}
return {
formIsValid: updatedIsFormValid,
inputValidities: updatedValidities,
inputValues: updatedValues,
};
}
return state;
};
const AuthenticationScreen = (props) => {
const [Isloading, setIsloading] = useState(false);
const [error, setError] = useState();
const [IsSignup, setIsSignup] = useState(false);
const dispatch = useDispatch();
const [formState, dispatchFormState] = useReducer(formReducer, {
inputValues: {
email: "",
password: "",
},
inputValidities: {
email: false,
password: false,
},
formIsValid: false,
});
const authHandler = async () => {
let action;
if (IsSignup) {
action = authActions.signup(
formState.inputValues.email,
formState.inputValues.password
);
} else {
action = authActions.login(
formState.inputValues.email,
formState.inputValues.password
);
}
setError(null);
setIsloading(true);
try {
await dispatch(action);
props.navigation.navigate("Shop");
} catch (error) {
setError(error.message);
setIsloading(false);
}
};
useEffect(() => {
if (error) {
Alert.alert("An error occurred ", error, [{ text: "Okay" }]);
}
}, [error]);
const inputChangeHandler = useCallback(
(inputIdentifier, inputValues, inputValidities) => {
dispatchFormState({
type: FORM_INPUT_UPDATE,
value: inputValues,
isValid: inputValidities,
input: inputIdentifier,
});
},
[dispatchFormState]
);
return (
<KeyboardAvoidingView
behaviour="padding"
keyboardVerticalOffset={50}
style={styles.screen}
>
<Card style={styles.auth}>
<ScrollView>
<Input
id="email"
label="E-mail"
keyboardType="email-address"
required
email
autoCapitalize="none"
warningText="Please enter valid Email Address"
onInputChange={inputChangeHandler}
intialValue=""
/>
<Input
id="password"
label="Password"
keyboardType="default"
secureTextEntry
required
minLength={5}
autoCapitalize="none"
warningText="Please enter valid Password"
onInputChange={inputChangeHandler}
intialValue=""
/>
<View style={styles.buttonContainer}>
<View style={styles.button}>
{Isloading ? (
<ActivityIndicator size="large" color={Colors.primary} />
) : (
<Button
title={IsSignup ? "Sign up" : "Login"}
color={Colors.primary}
onPress={authHandler}
/>
)}
</View>
<View style={styles.button}>
<Button
title={`Switch to ${IsSignup ? "Login" : "Sign Up"}`}
color={Colors.secondary}
onPress={() => {
setIsSignup((prevState) => !prevState);
}}
/>
</View>
</View>
</ScrollView>
</Card>
</KeyboardAvoidingView>
);
};
AuthenticationScreen.navigationOptions = {
headerTitle: "Authenticate",
};
const styles = StyleSheet.create({
screen: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
auth: {
width: "80%",
maxWidth: 400,
maxWidth: 400,
padding: 20,
},
buttonContainer: {
marginTop: 20,
},
button: {
margin: 5,
},
});
export default AuthenticationScreen;
I can only be able to type email and password with my laptop keyboard and I have tried clicking on both fields to check whether the keyboard in the IOS emulator popup or not.
The keyboard is working fine on the android emulator.
The simulator detects that you have an external keyboard available and thus doesn't show the software keyboard. You can disable this behaviour using I/O -> Keyboard -> Connect Hardware Keyboard. Or you can Command + K to toggle the Software Keyboard manually.

Not getting pre-filled state value from redux when user try to edit the form?

I am trying to get the state from redux store and trying to fill the input field from state. If user in edit mode. In edit mode, we normally show the prefilled value in input field. But what is wrong with the below approach?
I am able to store single user successfully in reducer but in component i am not getting. Sometimes i get the value. Overall, it's very inconsistent.
import React, { useState, useEffect } from "react";
import { makeStyles } from "#material-ui/core/styles";
import TextField from "#material-ui/core/TextField";
import Button from "#material-ui/core/Button";
import { useSelector, useDispatch } from "react-redux";
import { useHistory, useParams } from "react-router-dom";
import { addUser, getSingleUser, updateUser } from "../redux/actions";
const useStyles = makeStyles((theme) => ({
root: {
marginTop: 100,
"& > *": {
margin: theme.spacing(1),
width: "45ch",
},
},
}));
const initialState = {
name: "",
address: "",
contact: "",
email: "",
};
const EditUser = () => {
let { id } = useParams();
const { user } = useSelector((state) => state.users);
console.log("user", user);
const [state, setState] = useState(user);
const [error, setError] = useState("");
const { name, address, email, contact } = state;
const classes = useStyles();
const history = useHistory();
let dispatch = useDispatch();
const onInputChange = (e) => {
let { name, value } = e.target;
setState({ ...state, [name]: value });
};
useEffect(() => {
dispatch(getSingleUser(id));
}, []);
const handlSubmit = (e) => {
e.preventDefault();
console.log("name", name);
if (!name || !email || !address || !contact) {
setError("Please fill all Input Field");
} else {
dispatch(updateUser(state, id));
setError("");
history.push("/");
}
};
return (
<>
<Button
style={{ width: "100px", marginTop: "20px" }}
variant="contained"
type="submit"
color="secondary"
onClick={() => history.push("/")}
>
Go Back
</Button>
<h2>Edit user</h2>
{error && <h3 style={{ color: "red" }}>{error}</h3>}
<form
className={classes.root}
noValidate
autoComplete="off"
onSubmit={handlSubmit}
>
<TextField
id="standard-basic"
label="Name"
value={name}
name="name"
onChange={onInputChange}
type="text"
/>
<br />
<TextField
id="standard-basic"
value={email}
name="email"
label="Email"
type="email"
onChange={onInputChange}
/>
<br />
<TextField
id="standard-basic"
value={contact}
name="contact"
label="Contact"
type="number"
onChange={onInputChange}
/>
<br />
<TextField
id="standard-basic"
label="Address"
value={address}
name="address"
type="text "
onChange={onInputChange}
/>
<br />
<Button
style={{ width: "100px" }}
variant="contained"
type="submit"
color="primary"
>
Update
</Button>
</form>
</>
);
};
export default EditUser;
Below is redux actions logic to get the single user and dispatching an action to store single user value in reducer.
export const getSingleUser = (id) => {
return function (dispatch) {
axios
.get(`${process.env.REACT_APP_API}/${id}`)
.then((resp) => {
console.log("resp", resp);
dispatch(singleUser(resp.data));
})
.catch((error) => console.log(error));
};
};

MaterialUI TextField lag issue. How to improve performance when the form has many inputs?

I encountered an annoying problem with Textfield using MateriaUI framework. I have a form with many inputs and it seems to be a bit laggy when typing or deleting values inside the fields. In other components when there are like 2 or 3 inputs there's no lag at all.
EDIT: The problem seems to be with my onChange handler.
Any help is much appreciated. Thanks in advance.
This is my custom input code:
import React, { useReducer, useEffect } from 'react';
import { validate } from '../utils/validators';
import TextField from '#material-ui/core/TextField';
import { ThemeProvider, makeStyles, createMuiTheme } from '#material-ui/core/styles';
import { green } from '#material-ui/core/colors';
const useStyles = makeStyles((theme) => ({
root: {
color: 'white'
},
input: {
margin: '10px',
'&& .MuiInput-underline:before': {
borderBottomColor: 'white'
},
},
label: {
color: 'white'
}
}));
const theme = createMuiTheme({
palette: {
primary: green,
},
});
const inputReducer = (state, action) => {
switch (action.type) {
case 'CHANGE':
return {
...state,
value: action.val,
isValid: validate(action.val, action.validators)
};
case 'TOUCH': {
return {
...state,
isTouched: true
}
}
default:
return state;
}
};
const Input = props => {
const [inputState, dispatch] = useReducer(inputReducer, {
value: props.initialValue || '',
isTouched: false,
isValid: props.initialValid || false
});
const { id, onInput } = props;
const { value, isValid } = inputState;
useEffect(() => {
onInput(id, value, isValid)
}, [id, value, isValid, onInput]);
const changeHandler = event => {
dispatch({
type: 'CHANGE',
val: event.target.value,
validators: props.validators
});
};
const touchHandler = () => {
dispatch({
type: 'TOUCH'
});
};
const classes = useStyles();
return (
<ThemeProvider theme={theme}>
<TextField
className={classes.input}
InputProps={{
className: classes.root
}}
InputLabelProps={{
className: classes.label
}}
id={props.id}
type={props.type}
label={props.label}
onChange={changeHandler}
onBlur={touchHandler}
value={inputState.value}
title={props.title}
error={!inputState.isValid && inputState.isTouched}
helperText={!inputState.isValid && inputState.isTouched && props.errorText}
/>
</ThemeProvider>
);
};
export default Input;
In addition to #jony89 's answer. You can try 1 more workaround as following.
On each keypress (onChange) update the local state.
On blur event call the parent's change handler
const Child = ({ parentInputValue, changeValue }) => {
const [localValue, setLocalValue] = React.useState(parentInputValue);
return <TextInputField
value={localValue}
onChange={(e) => setLocalValue(e.target.value)}
onBlur={() => changeValue(localValue)} />;
}
const Parent = () => {
const [valMap, setValMap] = React.useState({
child1: '',
child2: ''
});
return (<>
<Child parentInputValue={valMap.child1} changeValue={(val) => setValMap({...valMap, child1: val})}
<Child parentInputValue={valMap.child2} changeValue={(val) => setValMap({...valMap, child2: val})}
</>
);
}
This will solve your problems if you do not want to refactor the existing code.
But the actual fix would be splitting the state so that update in the state of child1 doesn't affect (change reference or mutate) state of child2.
Make sure to extract all constant values outside of the render scope.
For example, each render you are providing new object to InputLabelProps and InputProps which forces re-render of child components.
So every new object that is not must be created within the functional component, you should extract outside,
That includes :
const touchHandler = () => {
dispatch({
type: 'TOUCH'
});
};
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
flexWrap: 'wrap',
color: 'white'
},
input: {
margin: '10px',
'&& .MuiInput-underline:before': {
borderBottomColor: 'white'
},
},
label: {
color: 'white'
}
}));
const theme = createMuiTheme({
palette: {
primary: green,
},
});
Also you can use react memo for function component optimization, seems fit to your case.
I managed to get rid of this lagging effect by replacing normal <TextField /> by <Controller /> from react-hook-form.
Old code with Typing Lag
<Grid item xs={12}>
<TextField
error={descriptionError.length > 0}
helperText={descriptionError}
id="outlined-textarea"
onChange={onDescriptionChange}
required
placeholder="Nice placeholder"
value={description}
rows={4}
fullWidth
multiline
/>
</Grid>
Updated Code with react-hook-form
import { FormProvider, useForm } from 'react-hook-form';
import { yupResolver } from '#hookform/resolvers/yup';
const methods = useForm({
resolver: yupResolver(validationSchema)
});
const { handleSubmit, errors, reset } = methods;
const onSubmit = async (entry) => {
console.log(`This is the value entered in TextField ${entry.name}`);
};
<form onSubmit={handleSubmit(onSubmit)}>
<Grid item xs={12}>
<FormProvider fullWidth {...methods}>
<DescriptionFormInput
fullWidth
name="name"
placeholder="Nice placeholder here"
size={matchesXS ? 'small' : 'medium'}
bug={errors}
/>
</FormProvider>
</Grid>
<Button
type="submit"
variant="contained"
className={classes.btnSecondary}
startIcon={<LayersTwoToneIcon />}
color="secondary"
size={'small'}
sx={{ mt: 0.5 }}
>
ADD
</Button>
</form>
import React from 'react';
import PropTypes from 'prop-types';
import { Controller, useFormContext } from 'react-hook-form';
import { FormHelperText, Grid, TextField } from '#material-ui/core';
const DescriptionFormInput = ({ bug, label, name, required, ...others }) => {
const { control } = useFormContext();
let isError = false;
let errorMessage = '';
if (bug && Object.prototype.hasOwnProperty.call(bug, name)) {
isError = true;
errorMessage = bug[name].message;
}
return (
<>
<Controller
as={TextField}
name={name}
control={control}
defaultValue=""
label={label}
fullWidth
InputLabelProps={{
className: required ? 'required-label' : '',
required: required || false
}}
error={isError}
{...others}
/>
{errorMessage && (
<Grid item xs={12}>
<FormHelperText error>{errorMessage}</FormHelperText>
</Grid>
)}
</>
);
};
With this use of react-hook-form I could get the TextField more responsive than earlier.

Trigger redux form submit in componentWillReceiveProps

When a user submits a form, I'm checking to see if a certain field's value has changed. If it has, I'm displaying a confirmation modal. If the user clicks "Yes" in the modal, the modal component sets the "dismissedSubTypeChangeWarning" prop to true in redux.
Inside componentWillReceiveProps I listen for any changes to this prop. If it's value is true, I want to trigger the form's submission. I followed instructions at https://redux-form.com/6.5.0/examples/remotesubmit/ to configure this.
The dispatch call never fires, because in the console I see "detected dismiss" then nothing else. I expect to see "submitForm method" after "detected dismiss".
I condensed the code (below) to the most basic version to show the issue. When I run my code I see no errors in the console.
I found a solution using refs (see commented line), but faking a click doesn't seem like a good practice.
class ChangeEditForm extends React.Component {
componentWillReceiveProps (nextProps) {
if (nextProps.dismissedSubTypeChangeWarning) {
console.log('detected dismiss')
this.props.toggleDismissedSubTypeChangeWarning()
this.props.dispatch(submit('changeEdit'))
// this.refs.submit.click()
}
}
render () {
<form onSubmit={this.props.handleSubmit(this.props.submitForm)}>
<button type='submit' ref='submit'>Save</button>
</form>
}
}
const handlers = {
submitForm: props => (formValues) => {
console.log('submitForm method')
if ((formValues.subtype !== props.initialValues.subtype) && !props.dismissedSubTypeChangeWarning) {
console.log('show warning')
props.toggleSubTypeChangeConfirmModal()
} else {
console.log('submitting form')
}
}
}
export function mapStateToProps (state, props) {
return {
dismissedSubTypeChangeWarning: state.ui.dismissedSubTypeChangeWarning
}
}
export default compose(
pure,
reduxForm({
form: 'changeEdit',
onSubmit: handlers.submitForm
}),
connect(mapStateToProps, null),
withRouter,
withHandlers(handlers)
)(ChangeEditForm)
I still think you're over complicating your form submission.
As mentioned in the comments, the form submits once, the values are checked once, a popup spawns if values are different, confirming in the popup updates values, otherwise canceling closes modal and leaves form as is.
By creating a HOC we can control the modal and the form without having to resubmit the form on popup confirmation.
Working example: https://codesandbox.io/s/lxv9o1nxzl
container/ChangeNameHOC.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { reset } from "redux-form";
import { updateUser } from "../actions";
import { Values } from "redux-form-website-template";
import ChangeNameForm from "./ChangeNameForm";
import ShowModal from "../components/ShowModal";
import CurrentStore from "../containers/CurrentStore";
class ChangeNameHOC extends Component {
state = {
modalIsOpen: false,
formProps: {}
};
openModal = () => this.setState({ modalIsOpen: true });
closeModal = () => this.setState({ modalIsOpen: false });
validateFormValues = formProps => {
const { firstName, lastName } = this.props;
const nextFirstName = formProps.firstName;
const nextLastName = formProps.lastName;
if (firstName !== nextFirstName || lastName !== nextLastName) {
this.setState({ modalIsOpen: true, formProps });
}
};
handleSubmit = () => {
this.setState({ modalIsOpen: false }, () => {
this.props.updateUser(this.state.formProps);
this.props.reset("ChangeNameForm");
});
};
render = () => (
<div style={{ padding: "0px 20px" }}>
<ShowModal
{...this.state}
{...this.props}
afterOpenModal={this.afterOpenModal}
openModal={this.openModal}
closeModal={this.closeModal}
handleSubmit={this.handleSubmit}
/>
<ChangeNameForm validateFormValues={this.validateFormValues} />
<CurrentStore />
<Values form="ChangeNameForm" />
</div>
);
}
export default connect(
state => ({
firstName: state.user.firstName,
lastName: state.user.lastName
}),
{ updateUser, reset }
)(ChangeNameHOC);
containers/ChangeNameForm.js
import React from "react";
import { Field, reduxForm } from "redux-form";
import RenderField from "../components/RenderField";
const isRequired = value => (!value ? "Required" : undefined);
const ChangeNameForm = ({
handleSubmit,
reset,
submitting,
validateFormValues
}) => (
<form onSubmit={handleSubmit(validateFormValues)}>
<Field
className="uk-input"
label="First Name"
name="firstName"
component={RenderField}
type="text"
placeholder="First Name"
validate={[isRequired]}
/>
<Field
className="uk-input"
label="Last Name"
name="lastName"
component={RenderField}
type="text"
placeholder="Last Name"
validate={[isRequired]}
/>
<button
className="uk-button uk-button-primary"
type="submit"
disabled={submitting}
style={{ marginBottom: 20 }}
>
Submit
</button>
<div style={{ float: "right" }}>
<button
className="uk-button uk-button-danger"
type="button"
disabled={submitting}
onClick={reset}
style={{ marginBottom: 20 }}
>
Reset
</button>
</div>
</form>
);
export default reduxForm({
form: "ChangeNameForm"
})(ChangeNameForm);
components/ShowModal.js
import React, { PureComponent } from "react";
import Modal from "react-modal";
const customStyles = {
content: {
minWidth: "400px",
top: "50%",
left: "50%",
right: "auto",
bottom: "auto",
marginRight: "-50%",
transform: "translate(-50%, -50%)"
}
};
Modal.setAppElement("#root");
export default class ShowModal extends PureComponent {
showChange = (name, prevName, nextName) => (
<div>
{name}: <strong>{prevName}</strong> to <strong>{nextName}</strong>
</div>
);
render = () => {
const { firstName, lastName } = this.props;
const { firstName: nextFirstName } = this.props.formProps;
const { lastName: nextLastName } = this.props.formProps;
return (
<div>
<Modal
isOpen={this.props.modalIsOpen}
onAfterOpen={this.props.afterOpenModal}
onRequestClose={this.props.closeModal}
style={customStyles}
contentLabel="Are you sure?"
>
<h3>Are you sure you want to update?</h3>
<div style={{ marginBottom: 20 }}>
{firstName !== nextFirstName
? this.showChange("FirstName", firstName, nextFirstName)
: null}
{lastName !== nextLastName
? this.showChange("LastName", lastName, nextLastName)
: null}
</div>
<button
style={{ float: "left" }}
className="uk-button uk-button-primary"
onClick={this.props.handleSubmit}
>
confirm
</button>
<button
style={{ float: "right" }}
className="uk-button uk-button-danger"
onClick={this.props.closeModal}
>
cancel
</button>
</Modal>
</div>
);
};
}

Categories

Resources