Using objects in hooks for forms - reactjs

I have just started with REACT and is looking for a valid approach when working with forms with many controls. And i dont need any validation so I skipped react-hook-forms.
Is there any recommended practices when working with forms? I have around 20 inputs so it is a semi large form. I started out having a hook for each input, but realized fast it was a little hard to maintain. So I changed to having one hook with an object like this:
https://codesandbox.io/s/bind-input-kzzd3
I can see some people recommend reducers for forms, is there anything wrong just using a hook with an object like my sample for simplicity? What will a reducer give me extra? Or would you go for a hook for each input?
import React, { useState } from "react";
import "./styles.css";
import { Row, Col, Form, FormGroup, Label, Input, Button } from "reactstrap";
export default function App() {
const onSearch = val => {
alert(val);
};
return (
<div className="App">
<GetSearchForm onSearch={onSearch} />
</div>
);
}
const GetSearchForm = props => {
const [val, setVal] = useState();
const [formData, setFormData] = useState({
accountId: 0,
firstName: "",
lastName: ""
});
const onSearch = event => {
event.preventDefault();
props.onSearch(val);
};
return (
<Form onSubmit={onSearch}>
<Row>
<Col>
<FormGroup>
<Label for="exampleEmail">Account Id</Label>
<Input
type="number"
name="account"
id="account"
placeholder="AccountId"
onChange={e =>
setFormData({ ...formData, accountId: e.target.value })
}
/>
</FormGroup>
</Col>
</Row>
<Row>
<Col>
<FormGroup>
<Label for="firstName">Firstname</Label>
<Input
type="text"
name="firstName"
id="firstName"
placeholder="Firstname"
value={val}
onChange={e => setVal(e.target.value)}
/>
</FormGroup>
</Col>
</Row>
<Row>
<Col>
<FormGroup>
<Label for="exampleEmail">Lastname</Label>
<Input
type="text"
name="lastName"
id="lastName"
placeholder="Lastname"
value={val}
onChange={e => setVal(e.target.value)}
/>
</FormGroup>
</Col>
</Row>
<Button>Submit</Button>
<div>{JSON.stringify(formData, null, 2)}</div>
</Form>
);
};

i dont need any validation so I skipped react-hook-forms.
I assume that you are using a hook means React.useState. Yes, if you don't want to use other form library, it is perfectly fine to update your state that way. Just be careful when you are updating state in useEffect, there is a problem with closure, where state is not updated properly.
any recommended practices when working with forms
As mentioned above, be careful with closure, (state is not updated properly) I will advice to use a setState((prevState)=>return your modified state).
What will a reducer give me extra? Or would you go for a hook for each input?
useReducer has more control for you to setState.
I have around 20 inputs so it is a semi large form.
Best is to use a form library instead of making each an independent state. I have used formik, it doesnt require a state management to use, pre-built validation with or without yup. (when your form grows, which in your case >20 inputs)

Related

antd how to get all values of multiple fields with one hook useWatch?

Helllo, I use antd v5. I want to get all values of multiple fields with one hook useWatch.
For example, I need to convert this code:
import {Form, Input} from 'antd';
const Test = () => {
const firstName = Form.useWatch('fist_name');
const lastName = Form.useWatch('last_name');
return (
<Form>
<Form.Item name="fist_name">
<Input />
</Form.Item>
<Form.Item name="last_name">
<Input />
</Form.Item>
</Form>
);
};
to this code (object firstAndLastNames contains values of all fields 'fist_name' and 'last_name'):
import {Form, Input} from 'antd';
const Test = () => {
const firstAndLastNames = Form.useWatch(['fist_name', 'last_name']);
return (
<Form>
<Form.Item name="fist_name">
<Input />
</Form.Item>
<Form.Item name="last_name">
<Input />
</Form.Item>
</Form>
);
};
but I don't understand how to do this, please help
My solution for this issue is to pass empty array in dependences useWatch([], form)

Inputs not updating state in functional react component

I looked around and I see similar questions, but whenever I follow the answers I can't seem to get this to work in the way that I have it written. I am starting off all four states as blank inside of an array, but I want to update the states as the user types for each input field. Why is setChanging not working to update the state for the particular name of the input field? Console logs both the x and y values as I type into each input. I must be doing something simple wrong, but I can't figure out what it is.
const ContactForm = () => {
const initialValues = {
recipientName: "",
recipientEmail: "",
fromName: "",
fromEmail: "",
};
const [changing, setChanging] = useState(initialValues);
const handleInputChange = (e) => {
let x = e.target.name;
let y = e.target.value;
console.log(x);
console.log(y);
setChanging({...changing, [e.target.name]: e.target.value});
console.log(initialValues);
}
return (
<Form>
<Form.Group className="mb-3">
<Row>
<Col>
<Form.Control
type="text"
required
name="recipientName"
placeholder="Recipient Name*"
id="form.recipientName"
onChange={handleInputChange}
/>
</Col>
<Col>
<Form.Control
type="email"
required
name="recipientEmail"
placeholder="Recipient Email*"
id="form.recipientEmail"
onChange={handleInputChange}
/>
</Col>
</Row>
</Form.Group>
<Form.Group className="mb-3">
<Row>
<Col>
<Form.Control
type="text"
required
name="fromName"
placeholder="From Name*"
aria-invalid="form.fromName"
onChange={handleInputChange}
/>
</Col>
<Col>
<Form.Control
type="email"
required
name="fromEmail"
placeholder="From Email*"
id="form.fromEmail"
onChange={handleInputChange}
/>
</Col>
</Row>
</Form.Group>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
);
}
export default ContactForm
You're logging the initialValues:
console.log(initialValues);
So you're always seeing the value of initialValues, which never changes. Nowhere are you observing state.
You can respond to state updates and log the updated state with useEffect for example:
useEffect(() => console.log(changing), [changing]);
This would log the value of changing any time that value changes.
You'd also observe updates to state in the UI if/when your UI uses state to render output. (Currently it does not.)
There are some things I suggest you to change:
<Form.Control
type="text"
required
name="fromName"
placeholder="From Name*"
aria-invalid="form.fromName"
onChange={handleInputChange}
/>
I'm not sure if those components belong to a framework like MaterialUI but would be better to have an attribute called value where you pass the state to handle a controlled component instead of an uncontrolled component:
<Form.Control
type="text"
required
name="fromName"
placeholder="From Name*"
aria-invalid="form.fromName"
onChange={handleInputChange}
value={changing.fromName} // Add this attribute
/>
Also, would be better if your initialState is outside of the function.
console.log(initialValues);
You should print the state instead of the initialValues, what you are updating is the state, not the initialValues.
setChanging({...changing, [e.target.name]: e.target.value});
this is because you didn't specify value attribute on your inputs.
in addition to this to see the changes on user type you must console.log the state (which is change here) not the initialValues.
example:
<Form.Control
type="text"
required
name="recipientName"
placeholder="Recipient Name*"
id="form.recipientName"
value={changing.fromName}
onChange={handleInputChange}
/>

Reset state without Effects using key

I came across exercise from beta.react docs concerning issue: Reset state without Effects.
You may find it in the bottom : Challenge 3 of 4: Reset state without Effects.
There's a component that receives object of person data to present it in editable form.
As for start it tells you that useEffect is redundant.
import React, { useState } from "react";
//ExportContact.jsx
export default function EditContact({ savedContact, onSave }) {
const [name, setName] = useState(savedContact.name);
const [email, setEmail] = useState(savedContact.email);
// useEffect(() => {
// setName(savedContact.name);
// setEmail(savedContact.email);
// }, [savedContact]);
return (
<section>
<label>
Name:{" "}
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
<label>
Email:{" "}
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</label>
<button
onClick={() => {
const updatedData = {
id: savedContact.id,
name: name,
email: email
};
onSave(updatedData);
}}
>
Save
</button>
<button
onClick={() => {
setName(savedContact.name);
setEmail(savedContact.email);
}}
>
Reset
</button>
</section>
);
}
Suggested solution is to split into another component that will receive key of contact id.
Like that where EditForm contains everything EditContact had so far.
export default function EditContact(props) {
return (
<EditForm
{...props}
key={props.savedContact.id}
/>
);
}
I'm just wondering how would it be different to add key prop with contact id value right into the parent component like this:
<EditContact
key={selectedContact.id}
savedContact={selectedContact}
onSave={handleSave}
/>
Instead of splitting EditContact into artificial subcomponent only to receive a key prop.
The difference is explained in the official link about spread attributes.
Below is the official summary:
Spread attributes can be useful but they also make it easy to pass unnecessary props to components that don’t care about them or to pass invalid HTML attributes to the DOM. We recommend using this syntax sparingly.

What is the best way to handle form inputs state in a controlled REACT component?

I have a basic form in React component.
For example :
export default class SimpleForm extends Component {
_isMounted = false;
state = {
firstName:'',
lastName : ''
};
changeState(data){
this.setState(data);
}
handleInput(event){
this.changeState({
[event.target.name]:event.target.value
})
}
render(){
const {
firstName,
lastName
} = this.state;
return (
<div className="">
<Form>
<FormGroup row>
<Label for="firstName" sm={3}>First Name</Label>
<Col sm={9}>
<Input type="text" value={firstName} onChange={(event)=>{this.handleInput(event)}} name="firstName" id="firstName" className="input-lg" />
</Col>
</FormGroup>
<FormGroup row>
<Label for="lastName" sm={3}>Last Name</Label>
<Col sm={9}>
<Input type="text" value={lastName} onChange={(event)=>{this.handleInput(event)}} name="lastName" id="lastName" className="input-lg" />
</Col>
</FormGroup>
</Form>
</div>
);
}
}
My concern here is each time we change an input field, the state changes. This is definitely the standard approach. I am not sure if this is the only approach to handle state. With this approach the component re renders for every character change. Isn't this expensive?
If we have lots of inputs, and lets say we need to calculate character count on some of the input fields then the count value needs some time to appear obviously because of the heavy re rendering.
You are right, re-rendering can be expensive.
You could avoid re-renders with uncontrolled inputs: https://reactjs.org/docs/uncontrolled-components.html
I would recommand to prefer uncontrolled inputs and only use controlled inputs if some values in your form depends on each others

My onChange function is not being passed down to child

I'm building a higher component where field input is grouped together with some other components. I'm trying to pass down my onChange function to the child component so it can change the state with the new input. all the other props are passed fine and I can see the using chrome dev tools, but even though I can see the onChange function if I console.log the props on the child, it is not present on the form control component which ultimately creates the input text field.
Here is the code, you can notice in the form render the FormGroup which is not using the higher InputFieldGroup component:
higher_component.js
import React from 'react'
import * as FormGroup from 'react-bootstrap/lib/FormGroup'
import * as ControlLabel from 'react-bootstrap/lib/ControlLabel'
import * as FormControl from 'react-bootstrap/lib/FormControl'
export const InputFieldGroup = (props) =>
<FormGroup
controlId={props.controlId}
validationState={props.validationState()}>
<ControlLabel>{props.controlLabel}</ControlLabel>
{console.log(props)} {/* I CAN SEE THE ONCHANGE FUNCTION HERE */}
<FormControl
type={props.type}
name={props.name}
value={props.value}
placeholder={props.placeholder}
onChange={props.handleChange}
/>
<FormControl.Feedback />
</FormGroup>
form.js
export default class RequestQuote extends Component {
...Some other class methods and stuff
handleTextChange = (e) => {
this.setState({ [e.target.name]: e.target.value })
}
render() {
return(
<form>
<InputFieldGroup
controlId='email'
validationState={this.emailValidationState}
controlLabel='email'
type='text'
name='email'
value={this.state.email}
placeholder='Enter Business Email'
onChange={this.handleTextChange}
/>
{/* When I don't use the higher component the onchange works fine */}
<FormGroup>
<ControlLabel>Name</ControlLabel>
<FormControl
type='text'
name='name'
value={this.state.name}
placeholder='Enter Name'
onChange={this.handleTextChange}
/>
<FormControl.Feedback />
</FormGroup>
</form>
)
}
}
Why isn't the onChange function being passed to the child input?
Because it is undefined! In fact you give
onChange={this.handleTextChange}
to the components as a props, if you want to call it do
props.onChange
and not
props.handleChange
otherwise, change the variable onChange={js} to handleChange={js}

Resources