React Function Component Form not setting state variables? - javascript

I have a React Function Component that is a form where users enter in values. The user inputs are stored using Hooks. The issue that I am running into is that it does not seem to be setting the hook correctly. When I console log one value right before the axios call, it returns as undefined. Below is the code I have so far. I think I am pretty close but am unsure where I made mistakes. Any help is appreciated!
import React, { useState, useEffect } from "react";
import axios from "axios";
function ReportOutage(){
//Use state hook to hold the values users input, passed into axios call
//setting the default values
const [formData, setFormData] = useState({
userReport: '6',
serviceType: "",
serviceName: "",
serviceStreet: "",
serviceCity: "",
serviceState: "",
serviceDescription: ""
})
const handleChange = (event) => {
event.preventDefault();
setFormData({[event.target.name]: event.target.value})
};
const handleSubmitReport = async (event) =>{
event.preventDefault();
console.log(formData.serviceName); // This is returning undefined when I am expecting the service name entered by the user.
const res = await axios.post("/outage-new", {
user_id: `${formData.userReport}`,
service_type: `${formData.serviceType}`,
service_name: `${formData.serviceName}`,
outage_street: `${formData.serviceStreet}`,
outage_city: `${formData.serviceCity}`,
outage_state: `${formData.serviceState}`,
outage_description: `${formData.serviceDescription}`,
})
};
return (
<>
<h1 id="Report-Title" class>Test Dialog box</h1>
<form onSubmit={handleSubmitReport}>
<input type="text" placeholder="Service Type"
onChange={handleChange}
value={formData.serviceType}
name="serviceType"/>
<input type="text" placeholder="Service Name"
onChange={handleChange}
value={formData.serviceName}
name="serviceName"/>
<input type="text" placeholder="Street"
onChange={handleChange}
value={formData.serviceStreet}
name="serviceStreet"/>
<input type="text" placeholder="City"
onChange={handleChange}
value={formData.serviceCity}
name="serviceCity"/>
<input type="text" placeholder="State"
onChange={handleChange}
value={formData.serviceState}
name="serviceState"/>
<input type="text" placeholder="Description"
onChange={handleChange}
value={formData.serviceDescription}
name="serviceDescription"/>
<button type="submit">Report Outage</button>
</form>
</>
);
}
export default ReportOutage;

When using the useState hook, updates are not shallowly merged like they are in the class component's this.setState. You are replacing the state with a new object with only the last field value updated.
const handleChange = (event) => {
event.preventDefault();
setFormData({ // <-- new object with only field name/value
[event.target.name]: event.target.value
});
};
You must manage this yourself, manually. Use a functional state update to access and shallow copy the previous state into the next state object.
const handleChange = (event) => {
event.preventDefault();
const { name, value } = event.target;
setFormData(data => ({
...data,
[name]: value,
}));
};

Related

How to Change pre filled input values

I new to React. I have two input fields which are filled when the pages loads via axios but when I want to change the Values I am unable to change them even I can not type anything on them.
function MainView() {
const [InputFields, setInputFields] = useState({
name: "",
fullURL: "",
});
const changeHandler = (e) => {
setInputFields({
...InputFields,
[e.target.name]: e.target.value,
});
};
const [links, setLinks] = useState([]);
const getLinks = () => {
axios.get('../sanctum/csrf-cookie').then(response => {
axios.get("/userlinks/getdata").then((res) => {
console.log(res);
setLinks(res.data);
}).catch((err) => {
console.log(err);
});
});
};
useEffect(() => {
getLinks();
}, []);
return (<div>
{links.map((link) => {
return (<div className="card" key={link.id}>
<form>
<input className="form-control" value={link.name} type="text" name="name" placeholder="name" onChange={changeHandler} />
<input className="form-control" value={link.fullURL} type="text" name="fullURL" placeholder="fullURL" onChange={changeHandler} />
</form>
</div>);
})}
</div>
);
}
export default MainView;
This happens because in your onChange method, you are changing InputFields variable, where as your getLinks method changes links variable, which is being rendered on the screen.
If you want to set an initial value, and then allow the user to change it, change your input to :
<input className="form-control" defaultValue={link.name}
value={InputFields.name} type="text" name="name" placeholder="name" onChange={changeHandler} />
Likewise change for your other input, if you do not want the user to change the value later on, it's often better to add disable in the input to avoid confusing people. 🙂
I know that this has been done so that you can create a minimal reproducible example for us, but I would have directly called setInputFields in the axios.get section to avoid this problem in the first place, however, if not possible, use the defaultValue and value as I've shown above.

Is there a way to get a React component's internal values when I click a button on the parent?

Suppose I have a component like this -
const MyForm = ({ formId }) => (
<div>
<input type="text" placeholder="Full name"></input>
<input type="text" placeholder="Email"></input>
</div>
)
export default MyForm;
And then I have my App.js like so -
import React from "react";
import MyForm from "./MyForm";
const App = () => (
<div id="app">
<MyForm formId="formOne"></MyForm>
<MyForm formId="formTwo"></MyForm>
<button onClick={
() => {
// Here, when the user clicks the button,
// I want to get values of both the textboxes,
// from both the component instances
}
}>Submit</button>
</div>
)
export default App;
So basically, what I want is - when the button is clicked, I want to be able to retrieve the values of the textboxes. One way to do this is to raise an event from inside MyForm.js so that every text change is bubbled up to the parent via a callback function prop, but that feels too cumbersome, especially if the form has a lot of fields. Is there any simple or direct way to do this? Do I need to involve global state management tools like Redux?
State inside a component is specific only to that component, the parent , children or sibling of a component have no idea of the state. The only way to communicate the value from one component to another component is via props . In your case, what we need is a state to reside at the App which can then be passed as a prop to both the MyForm Components.
App.js
const [ formState, setFormState ] = useState({ formOne: {fullName: '', Email: ''}, formTwo: '' })
const updateFormValues = (formId, key, value) => {
const stateCopy = JSON.parse(JSON.stringify(formState));
const formToUpdate = stateCopy[formId];
formToUpdate[key] = value;
setFormState(stateCopy)
}
<MyForm formId="formOne" values={formState.formOne} updateFormValues={updateFormValues}></MyForm>
<MyForm formId="formTwo" values={formState.formTwo} updateFormValues={updateFormValues}></MyForm>
MyForm.js
const MyForm = ({ formId, values, updateFormValues }) => {
const onInputChange = (e, key) => {
updateFormValues(formId, key, e.target.value)
}
return(
<div>
<input type="text" onChange={(e) => onInputChange(e, 'fullName'} value={values.fullName} placeholder="Full name"></input>
<input type="text" onChange={(e) => onInputChange(e, 'email'} value={values.email} placeholder="Email"></input>
</div>
)}
export default MyForm;
To have access to data inside children components you need to lift the state to the parent component.
One-way data flow
Identify every component that renders something based on that state.
Find a common owner component (a single component above all the components that need the state in the hierarchy).
Either the common owner or another component higher up in the hierarchy should own the state.
If you can’t find a component where it makes sense to own the state, create a new component solely for holding the state and add it somewhere in the hierarchy above the common owner component.
One way to do this:
import React, { useState } from "react";
function MyForm(props) {
const { handleChange, values } = props;
return (
<div>
<label htmlFor="name">Your name</label>
<input
type="text"
placeholder="Full name"
onChange={handleChange}
value={values.name}
id="name"
name="name"
/>
<label htmlFor="email">Your email</label>
<input
type="email"
placeholder="Email"
onChange={handleChange}
value={values.email}
id="email"
name="email"
/>
</div>
);
}
function App() {
const [values, setValues] = useState({ name: "", email: "" });
const handleChange = (event) => {
const updatedForm = { ...values, [event.target.name]: event.target.value };
setValues(updatedForm);
};
return (
<div id="app">
<MyForm
formId="formOne"
values={values}
handleChange={handleChange}
></MyForm>
<button
onClick={() => {
console.log(values);
}}
>
Submit
</button>
</div>
);
}
export default App;

using ...prev in setState prohibits userEvent.type to type into input when react testing

I want to test my Log In Component, which consists of two input fields, whose values are determined by a single react state that is an object with two parameters. However, when I try the test only the first letter appears in the value of the selected input and not the rest of the word. I determined my use of ...prev when updating the state to be the issue. If I only use a single input field with one state it works fine!
Here is my component:
import {useState} from 'react';
export function Login () {
//Login Credentials
const [loginCredentials, setLoginCredentials] = useState({ name: '' });
const handleChange = ({target}) => {
setLoginCredentials({[target.name]: target.value});
}
return (
<div className="login-container">
<h1>Log In</h1>
<div className="login-label-input">
<label htmlFor="name">Account Name
<input
type="name"
id="name"
name="name"
onChange={handleChange}
value={loginCredentials.name}
/>
</label>
<label htmlFor="name">Password
<input
type="password"
id="password"
name="password"
onChange={handleChange}
value={loginCredentials.password}
/>
</label>
</div>
State name: {loginCredentials.name}. State password: {loginCredentials.password}.
</div>
)
}
This works but if I include the password state:
export function Login () {
//Login Credentials
const [loginCredentials, setLoginCredentials] = useState({ name: '', password: '' });
const handleChange = ({target}) => {
setLoginCredentials((prev) => ({...prev, [target.name]: target.value}));
}
...
it does not pass the test. I does not throw an error but simply only adds the first letter of the string I am testing with:
test("log in with empty name input returns error message", async () => {
render(
<Login />
);
const nameField = screen.getByLabelText(/account name/i);
userEvent.type(nameField, 'test');
await waitFor(() => expect(nameField).toHaveValue('test'));
});
with the error:
expect(element).toHaveValue(test)
Expected the element to have value:
test
Received:
t
Is using ...prev bad or is this is a bug or what is going on?
It seems like you have to assign the new value to a different variable, I am not sure why this is necessary for the test but not in the app itself.
const handleChange = ({target}) => {
const newValue = target.value
setLoginCredentials((prev) => ({ ...prev, [target.name]: newValue }));
}

React controlled component error in function component

I am working with forms in React, using function components for the first time. Either I am going crazy, or this should work with no issues...
import React, {useEffect, useState} from 'react';
function ChangePasswordComponent(props) {
const {onChangePassword} = props;
const [isValid, setIsValid] = useState(true);
const [form, setForm] = useState({
password: undefined,
confirm: undefined
})
useEffect(() => {
handleValidation();
}, [form])
function handleValidation() {
setIsValid(form.password === form.confirm);
}
function onFormValueChanges(event) {
setForm({...form, [event.target.name]: event.target.value})
}
function resetFields() {
setForm({
password: undefined,
confirm: undefined
})
}
function onUpdateClick() {
onChangePassword(form.password);
resetFields();
}
return (
<div className="change-password-container">
<input
type="text"
name="password"
value={form.password}
onChange={(event) => onFormValueChanges(event)}
placeholder="new password" />
<input
type="text"
name="confirm"
value={form.confirm}
onChange={(event) => onFormValueChanges(event)}
placeholder="confirm new password" />
{!isValid ?
<span className="validation-error">passwords do not match</span> : null }
<div className="button-container">
<button onClick={() => resetFields()}>Cancel</button>
<button onClick={() => onUpdateClick()}
disabled={!form.password || !isValid}>Update</button>
</div>
</div>
);
}
export default ChangePasswordComponent;
However when I run the code I get an error in console about...
A component is changing an uncontrolled input of type text to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.
When I look back into the documentation, my pattern seems to follow the Docs just fine. Thoughts?
you should use empty strings like '' instead of undefined.
when your component has first rendered, password and confirm values are undefined. This means that there is no variable to set the value attribute of the input.
Therefore, that error occurs.

Empty value when form is submitted for the first time

I have the following code which is intended to get the input fed to ToggleForm component (which is a form) and store it in employeeData state. However, the problem is that whenever I press the submit button of ToggleForm for the first time after execution, "" value gets stored first in the employeeData state and it is only after I click the submit button for the second time that the data fed in the form comes to employeeData.
This must be a minor mistake. But I am not being able to figure it out.
import React from "react";
import ToggleForm from "./ToggleForm";
let employee = "";
class Home extends React.Component {
constructor(){
super();
this.state = {
employeeData: ""
};
}
addEmployee(e) {
e.preventDefault();
let name = e.target.name.value;
let address = e.target.address.value;
let salary = e.target.salary.value;
this.setState({
employeeData: [...this.state.employeeData, { name, address, salary }]
});
employee = [...this.state.employeeData];
console.log(employee);
}
render() {
return (
<div className="container">
<ToggleForm addEmployee={this.addEmployee.bind(this)}/>
</div>
);
}
}
export default Home;
Here is the ToggleForm component:
import React from 'react';
class ToggleForm extends React.Component {
render(){
return(<div>
<br/>
<h3>Add a new employee</h3>
<hr/>
<form className="form-group" onSubmit = {this.props.addEmployee}>
<input className="form-control" type="text" name="name" placeholder="Name of the employee"/><br/>
<input className="form-control" type="text" name="address" placeholder="Address of the employee"/><br/>
<input className="form-control" type="text" name="salary" placeholder="Salary of the employee"/><br/>
<input type="submit" className="btn btn-primary"/>
</form>
</div>)
}
}
export default ToggleForm;
setState is async and fortunately accepts an optional callback. Using the callback, you can access the most current value of state.
this.setState({
employeeData: [...this.state.employeeData, { name, address, salary }]
}, () => {
employee = [...this.state.employeeData];
});
Because setState is async so your need to setState in the component Toggle form when the text is change before ship it the parent component.
For example:
<input
onChange={this.handleChange}
className="form-control"
type="text"
name="name"
value={this.state.name}
placeholder="Name of the employee"
/>
<br />
Function handleChange:
handleChange = (e) => {
this.setState({ [e.target.name]: e.target.value });
console.log(e.target.value)
};
And then ship it to the parent:
handleSubmit = e => {
e.preventDefault();
const { name, address, salary } = this.state;
this.props.addEmployee({ name, address, salary });
};
Check my code here: https://codesandbox.io/s/ww5331jrxl
There are few basic correction in your components:
User super(); in the constructor before this.setState();
If you are not using this.state.employeeData, then don't set it in the state.
If you set the state then you will get the employeeData in the callback function as described by #Andy or you can use the following:
employee = [...this.state.employeeData, { name, address, salary }]

Categories

Resources