How to update the state of array of objects from form fields? - javascript

I have created a component that can be used for creating a new company record. A modal is opened with a form and the values are linked to the state values. In my situation, it will be possible to create more than one record of a company if the user chooses to add another company. A new company object will be pushed to the company state and the new empty form will be rendered.
This is what I've tried based on this answer:
import { Component } from 'react';
import { Modal, Header, Form, Button, Icon, Tab, Segment } from 'semantic-ui-react';
export default class CompanyCreate extends Component {
constructor(props) {
super(props);
this.state = {
company: [
{
name: '',
segment: ''
}
]
};
this.initialState = this.state;
this.handleChange = this.handleChange.bind(this);
this.handleCompanyChange = this.handleCompanyChange.bind(this);
}
handleChange = (e, { name, value }) => this.setState({ [name]: value });
handleCompanyChange = (e, { name, value }) => {
const index = this.state.company.findIndex((x) => {
return x[name] === value;
});
if (index === -1) {
console.log('error');
} else {
this.setState({
company: [
...this.state.company.slice(0, index),
Object.assign({}, this.state.company[index], value),
...this.state.company.slice(index + 1)
]
});
}
};
render() {
const { company } = this.state;
return (
<Segment>
{company.map((e, index) => (
<Form size="large" key={index}>
<Form.Group>
<Form.Input
width={6}
onChange={this.handleCompanyChange}
label="Nome"
placeholder="Nome"
name="name"
value={e.name}
required
/>
<Form.Input
width={6}
onChange={this.handleCompanyChange}
label="Segmento"
placeholder="Segmento"
name="segment"
value={e.segment}
required
/>
</Form.Group>
</Form>
))}
</Segment>
);
}
}
My problem is that I can't set the company state properly. How can you update the state in relation to the changes in the form fields?

Looking for answers, I found the package: immutability-helper. Based on this answer, the problem was solved simply and elegantly.
The solution:
import update from 'immutability-helper';
//...
this.state = {
company: [
{
name: '',
segment: ''
}
]
};
//...
handleCompanyChange = (e, { name, value, id }) => {
let newState = update(this.state, {
company: {
[id]: {
[name]: { $set: value }
}
}
});
this.setState(newState);
};
//...
render() {
const { company } = this.state;
return (
<Segment>
{company.map((e, index) => (
<Form size="large" key={index}>
<Form.Group>
<Form.Input
width={6}
onChange={this.handleCompanyChange}
label="Nome"
placeholder="Nome"
name="name"
value={e.name}
id={index}
required
/>
<Form.Input
width={6}
onChange={this.handleCompanyChange}
label="Segmento"
placeholder="Segmento"
name="segment"
value={e.segment}
id={index}
required
/>
</Form.Group>
</Form>
))}
</Segment>
);
}

Related

Add multiple input field dynamically in react

I have is a div that contains three input elements and a remove button. what is required is when the user clicks on the add button it will add this div dynamically and when the user clicks on the remove button it will remove this div. I was able to add one input element (without div container) dynamically with the following method.
create an array in the state variable.
assign a name to the dynamic input field with the help of array indexing like name0, name1
How can I do with these many input fields? The problem grows further when I create this whole div as a separate component. I am using a class-based component.
handleChange=(event) =>
{
this.setState({[event.target.name]:event.target.values});
}
render()
{
return(
<div className="row">
<button type="button" onClick={this.addElement}>Add</button>
<div className="col-md-12 form-group">
<input type="text" className="form-control" name="name" value={this.state.name} onChange={this.handleChange} />
<input type="text" className="form-control" name="email" value={this.state.email} onChange={this.handleChange} />
<input type="text" className="form-control" name="phone" value={this.state.phone} onChange={this.state.phone} />
<button type="button" onClick={this.removeElement}>Remove</button>
</div>
</div>
)
}
I would approach this from a configuration angle as it's a little more scalable. If you want to eventually change across to something like Formik or React Form, it makes the move a little easier.
Have an array of objects that you want to turn into input fields. Your main component should maintain state whether the <Form /> component is showing, and if it's visible pass in the config and any relevant handlers.
Your form component should maintain state for the inputs, and when you submit it, passes up the completed state to the parent.
const { Component } = React;
class Example extends Component {
constructor(props) {
super();
// The only state in the main component
// is whether the form is visible or not
this.state = { visible: false };
}
addForm = () => {
this.setState({ visible: true });
}
removeForm = () => {
this.setState({ visible: false });
}
handleSubmit = (form) => {
console.log(form);
}
render() {
const { visible } = this.state;
const { config } = this.props;
return (
<div>
<button
type="button"
onClick={this.addForm}
>Add form
</button>
{visible && (
<Form
config={config}
handleSubmit={this.handleSubmit}
handleClose={this.removeForm}
/>
)}
</div>
);
}
};
class Form extends Component {
constructor(props) {
super();
this.state = props.config.reduce((acc, c) => {
return { ...acc, [c.name]: '' };
}, {});
}
handleChange = (e) => {
const { name, value } = e.target;
this.setState({ [name]: value });
}
handleSubmit = () => {
this.props.handleSubmit(this.state);
}
render() {
const { name, email, phone } = this.state;
const { handleClose, config } = this.props;
return (
<div onChange={this.handleChange}>
{config.map(input => {
const { id, name, type, required } = input;
return (
<div>
<label>{name}</label>
<input key={id} name={name} type={type} required={required} />
</div>
)
})}
<button type="button" onClick={this.handleSubmit}>Submit form</button>
<button type="button" onClick={handleClose}>Remove form</button>
</div>
);
}
}
const config = [
{ id: 1, name: 'name', type: 'text', required: true },
{ id: 2, name: 'email', type: 'email', required: true },
{ id: 3, name: 'phone', type: 'phone', required: true }
];
ReactDOM.render(
<Example config={config} />,
document.getElementById('react')
);
input { display: block; }
label { text-transform: capitalize; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I hope this would be help for your question.
I made a child component which have three input tags.
// parent component
import React, { Component } from "react";
import TextField from "./TextField";
class App extends Component {
constructor(props) {
super(props);
this.state = {
users: [
{
key: Date.now(),
name: "",
email: "",
phone: ""
}
]
};
}
onChange = (inputUser) => {
this.setState((prevState) => {
const newUsers = prevState.users.map((element) => {
if (element.key === inputUser.key) return inputUser;
return element;
});
return { users: newUsers };
});
};
addElement = () => {
const { name, email, phone } = this.state;
this.setState((prevState) => ({
users: prevState.users.concat({
key: Date.now(),
name,
email,
phone
})
}));
};
removeElement = (id) => {
this.setState((prevState) => ({
users: prevState.users.filter((user) => user.key !== id)
}));
};
render() {
const { users } = this.state;
return (
<div className="row">
<button type="button" onClick={this.addElement}>
Add
</button>
<div className="col-md-12 form-group">
{users.map((user) => (
<React.Fragment key={user.key}>
<TextField
value={user}
onChange={(inputUser) => this.onChange(inputUser)}
/>
<button
type="button"
onClick={() => this.removeElement(user.key)}
disabled={users.length <= 1}
>
Remove
</button>
</React.Fragment>
))}
</div>
</div>
);
}
}
export default App;
// child component
import { Component } from "react";
class TextField extends Component {
handleChange = (ev) => {
const { name, value } = ev.target;
this.props.onChange({
...this.props.value,
[name]: value
});
};
render() {
const { value: user } = this.props;
return (
<>
<input
className="form-control"
name="name"
value={user.name}
onChange={this.handleChange}
placeholder="name"
type="text"
/>
<input
className="form-control"
name="email"
value={user.email}
onChange={this.handleChange}
placeholder="email"
type="text"
/>
<input
className="form-control"
name="phone"
value={user.phone}
onChange={this.handleChange}
placeholder="phone"
type="text"
/>
</>
);
}
}
export default TextField;
You can also check the code in codesandbox link below.
https://codesandbox.io/s/suspicious-heisenberg-xzchm
It was a little difficult to write down every detail of how to generate what you want. So I find it much easier to ready a stackblitz link for you to see how is it going to do this and the link is ready below:
generating a dynamic div adding and removing by handling inputs state value
const { Component } = React;
class Example extends Component {
constructor(props) {
super();
// The only state in the main component
// is whether the form is visible or not
this.state = { visible: false };
}
addForm = () => {
this.setState({ visible: true });
}
removeForm = () => {
this.setState({ visible: false });
}
handleSubmit = (form) => {
console.log(form);
}
render() {
const { visible } = this.state;
const { config } = this.props;
return (
<div>
<button
type="button"
onClick={this.addForm}
>Add form
</button>
{visible && (
<Form
config={config}
handleSubmit={this.handleSubmit}
handleClose={this.removeForm}
/>
)}
</div>
);
}
};
class Form extends Component {
constructor(props) {
super();
this.state = props.config.reduce((acc, c) => {
return { ...acc, [c.name]: '' };
}, {});
}
handleChange = (e) => {
const { name, value } = e.target;
this.setState({ [name]: value });
}
handleSubmit = () => {
this.props.handleSubmit(this.state);
}
render() {
const { name, email, phone } = this.state;
const { handleClose, config } = this.props;
return (
<div onChange={this.handleChange}>
{config.map(input => {
const { id, name, type, required } = input;
return (
<div>
<label>{name}</label>
<input key={id} name={name} type={type} required={required} />
</div>
)
})}
<button type="button" onClick={this.handleSubmit}>Submit form</button>
<button type="button" onClick={handleClose}>Remove form</button>
</div>
);
}
}
const config = [
{ id: 1, name: 'name', type: 'text', required: true },
{ id: 2, name: 'email', type: 'email', required: true },
{ id: 3, name: 'phone', type: 'phone', required: true }
];
ReactDOM.render(
<Example config={config} />,
document.getElementById('react')
);
input { display: block; }
label { text-transform: capitalize; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

How to change Antd form initialValues depends at url or id?

I got same component with Antd form for add/edit article. With pathes in router
<Route path="/add" component={ !currentUser ? Login : ArticleEditor } />
<Route path="/article/:id/edit" component={ !currentUser ? Login : ArticleEditor } />
When I click "edit" button I add initialValues to form, than if I click "Create new article" url changes to "/add", but form didn't update values. Values remains from edited article. How to update form values? Tried to set initialValues depends at path, or "id" but its not worked. How to update antd form values in that case?
const initialValues = this.props.location.pathname === '/add' ? {} : {
title: this.props?.title,
body: this.props?.body,
description: this.props?.description
};
Here you can see the component code - codesandbox link
The main issue with the code is form fields are not reset when url is changed, you can detect path change in shouldComponentUpdate and set isLoading to true and rest should work.
Updating initialValues will not work because, antd does shallow compare and once initialValues are set, you will not be able to change them.
There was an issue in the logic of componentDidUpdate which I corrected as well.
import React from "react";
import ErrorsList from "../ErrorsList/ErrorsList";
import userService from "../../services/userService";
import { connect } from "react-redux";
import { push } from "react-router-redux";
import { Form, Input, Button } from "antd";
import { store } from "../../store";
import actionCreators from "../../actionCreators";
const formItemLayout = {
labelCol: { span: 24 },
wrapperCol: { span: 24 }
};
const formSingleItemLayout = {
wrapperCol: { span: 24, offset: 0 }
};
const mapStateToProps = (state) => ({
...state.editor
});
const mapDispatchToProps = (dispatch) => ({
onLoad: (payload) => dispatch(actionCreators.doEditorLoaded(payload)),
onUnload: () => dispatch(actionCreators.doEditorUnloaded()),
onUpdateField: (key, value) =>
dispatch(actionCreators.doUpdateFieldEditor(key, value)),
onSubmit: (payload, slug) => {
dispatch(actionCreators.doArticleSubmitted(payload));
store.dispatch(push(`/`)); //article/${slug}
},
onRedirect: () => dispatch(actionCreators.doRedirect())
});
class ArticleEditor extends React.Component {
constructor(props) {
super(props);
this.id = this.props.match.params.id;
const updateFieldEvent = (key) => (e) =>
this.props.onUpdateField(key, e.target.value);
this.changeTitle = updateFieldEvent("title");
this.changeDescription = updateFieldEvent("description");
this.changeBody = updateFieldEvent("body");
this.changeTagInput = updateFieldEvent("tagInput");
this.isLoading = true;
this.submitForm = () => {
const article = {
title: this.props.title,
description: this.props.description,
body: this.props.body,
tagList: this.props.tagInput.split(",")
};
const slug = { slug: this.props.articleSlug };
const promise = this.props.articleSlug
? userService.articles.update(Object.assign(article, slug))
: userService.articles.create(article);
this.props.onSubmit(promise, this.props.articleSlug);
};
}
componentDidUpdate(prevProps, prevState) {
if (this.props.match.params.id !== prevProps.match.params.id) {
if (prevProps.match.params.id) {
this.props.onUnload();
}
this.id = this.props.match.params.id;
if (this.id) {
return this.props.onLoad(userService.articles.get(this.id));
}
this.props.onLoad(null);
}
this.isLoading = false;
}
componentDidMount() {
if (this.id) {
this.isLoading = true;
return this.props.onLoad(userService.articles.get(this.id));
}
this.isLoading = false;
this.props.onLoad(null);
}
componentWillUnmount() {
this.props.onUnload();
}
shouldComponentUpdate(newProps, newState) {
if (this.props.match.params.id !== newProps.match.params.id) {
this.isLoading = true;
}
return true;
}
render() {
const { errors } = this.props;
const initialValues = {
title: this.props?.title,
body: this.props?.body,
description: this.props?.description,
tags: this.props?.tagList
};
return this.isLoading ? (
"loading..."
) : (
<div className="editor-page">
<div className="container page">
<div className="">
<div className="">
<ErrorsList errors={errors}></ErrorsList>
<Form
{...formItemLayout}
initialValues={initialValues}
onFinish={this.submitForm}
>
<Form.Item
label="Title"
name="title"
placeholder="Article Title"
rules={[
{
required: true,
message: "Please input article title"
}
]}
>
<Input onChange={this.changeTitle} />
</Form.Item>
<Form.Item
label="Description"
name="description"
placeholder="Short description"
rules={[
{
required: true,
message: "Please input article description"
}
]}
>
<Input onChange={this.changeDescription} />
</Form.Item>
<Form.Item
name="body"
label="Article Text"
placeholder="article text"
>
<Input.TextArea onChange={this.changeBody} />
</Form.Item>
<Form.Item name="tags" label="Tags" placeholder="Enter tags">
<Input onChange={this.changeTagInput} />
</Form.Item>
<Form.Item {...formSingleItemLayout}>
<Button
className="editor-form__btn"
type="primary"
htmlType="submit"
disabled={this.props.inProgress}
>
Submit Article
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ArticleEditor);
take a look at this forked codesandbox.
You have to clean the fields before you re-use the 'ArticleEditor' component. Here you are using the same component for two different route, hence it's not changing.
You have to check if you are editing or adding a new entry to the Editor. Your editor component may look like this then,
const ArticleEditor = props => {
const [form] = Form.useForm();
useEffect(() => {
if (props.match.params.id) form.setFieldsValue({value : 'Some values'})
else form.resetFields()
}, [props?.match?.params]);
return (
<Form form={form} onFinish={yourFinishMethod}>
//...your form fields
</Form>
)
}

Form update in React : input previous data is erased if the input is left empty when submitted

So, I have a data persistence issue with my form inputs.
If I modify all inputs everything is fine.
But if an input is left empty, its previous data is erased when I submit. I need suggestions for my handleChange to keep data even when an input is not modified.
I tried this but it failed too :
handleChange = e => {
e.persist();
this.setState(prevState => ({
product: { ...prevState.product, [e.target.name]: e.target.value }
}))
}
Here is my EditForm, thanks for your help.
EditForm.js
export default class EditForm extends Component {
constructor(props) {
super(props);
this.state = { product: [] };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
};
componentDidMount = () => {
axios
.get(`/products/edit-form/${this.props.match.params.id}`)
.then(response => {
console.log(response.data.products);
this.setState({
product: response.data.products
})
});
};
handleChange(e) {
console.log(e.target.name);
this.setState({[e.target.name]: e.target.value})
}
handleSubmit(e) {
const data = {
id: this.props.match.params.id,
reference: this.state.reference,
designation: this.state.designation
}
e.preventDefault();
console.log(data);
axios
.post(`/products/${data.id}`, data )
.then(res => console.log(res))
.catch(err => console.log(err));
};
renderForm() {
return this.state.product.map((product, index) => {
const { id,reference,designation } = product
return(
<>
<Form className="post" onSubmit={this.handleSubmit}>
<Form.Row>
<Form.Group as={Col} controlId="formGridReference">
<Form.Label>Reference</Form.Label>
<Form.Control type="text" value={this.state.product.reference}
onChange={this.handleChange} name="reference" placeholder={reference}/>
</Form.Group>
<Form.Group as={Col} controlId="formGridDesignation">
<Form.Label>Designation</Form.Label>
<Form.Control type="text" value={this.state.product.designation}
onChange={this.handleChange} name="designation" placeholder={designation}/>
</Form.Group>
</Form.Row>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
</>
);
})
}
render() {
return (
<div>
<h1>Formulaire de modification</h1>
{this.renderForm()}
</div>
);
}
}```
In your EditForm.js, inside handleChange(), you have to maintain your previous state. You can do so by creating a copy of your state. Then use the copy to update your state property values and in the end, use this.setState();
Example:
const state = this.state;
state['Your property'] = 'value';
...
...
this.setState(state);

Remove Item on click in React [duplicate]

This question already has answers here:
Removing element from array in component state
(11 answers)
Closed 3 years ago.
I have a names list which contains 3 names. On clicking any 1, you can edit and save. That value updates to the list. Now I can add how many ever names I want but I am not able to delete/remove any.
This is how it looks in the beginning
On clicking a name (the whole container) it looks like this
I am able to add the new name back to the div like this
And I can add as many as I want, like this like this
Now I want to be able to click on the cross icon and remove the whole element I want. It should go be gone from the page. The other elements should take its place from top to bottom. Remove/delete functionality should be on input and the div with names in it.
Names Component (Names are extracted from this to the one below)
import Sukhdev from '../../../src/components/sukhdev';
import React from 'react';
export default { title: 'Sukhdev' };
const names = [{
firstName: "Mahatma",
lastName: "Gandhi"
}, {
firstName: "Shivaji",
lastName: "Maharaj"
}, {
firstName: "Bhagat",
lastName: "Singh"
},
]
export const sukhdev = () => {
return(
<Sukhdev names={names}/>
)
}
Parent Component
import React, { Component } from 'react';
import FirstName from './firstName';
import LastName from './lastName';
import TextArea from './textArea'
import styles from './styles';
export default class Sukhdev extends Component {
constructor(props) {
super(props);
const {names} = this.props;
const updatedNames = names.map((name) => ({...name, ...{isEditable: false}}));
this.state = {
userNames: updatedNames
}
}
inputNamesHandler = (namesIndex) => {
const updatedUserNameDetails = [...this.state.userNames];
updatedUserNameDetails[namesIndex].isEditable = true;
this.setState({userNames: updatedUserNameDetails})
}
saveButton = (inputValue, index) => {
const {userNames} = this.state;
const newNames = [...userNames];
newNames[index] = {...newNames[index], isEditable: false, firstName: inputValue, lastName: ''};
this.setState({
userNames: newNames
})
}
addChild = () => {
const createInputs = [...this.state.userNames];
createInputs.push({firstName: '', lastName: '', isEditable: true});
this.setState({
userNames: createInputs
})
}
------> // This is where the changes need to be made
deleteRow = (index) => {
const postDelete = [...this.state.userNames];
postDelete.slice(index, 1);
this.setState({
userNames: postDelete
})
}
render() {
return <div>
<h1>Names</h1>
<button onClick={this.addChild} style={styles.button}>Add New</button>
<div>
{this.state.userNames.map((nameDetails, index) => {
if(nameDetails.isEditable) {
return <div>
<TextArea clicked={(name) => this.saveButton(name, index)}/>
</div>;
} else {
return <div style={styles.namesContainer}>
<div onClick={() => this.inputNamesHandler(index)} style={styles.innerContainerComponent}>
<div style={styles.firstMargin}><FirstName firstName={nameDetails.firstName}></FirstName></div>
<div><LastName lastName={nameDetails.lastName}></LastName></div>
</div>
<img src={require('../../images/cancel.png')} style={styles.crossBtn} onClick={() => this.deleteRow(index)} />
</div>
}
})}
</div>
</div>
}
}
Textarea/Input Component
import React, { Component } from "react";
import styles from './styles'
export default class TextArea extends Component {
constructor(props) {
super(props);
this.state = {value:''}
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
render() {
return (
<div>
<div style={styles.inputContainer}>
<input type="text" style={styles.textField} value={this.state.value} onChange={this.handleChange}></input>
<button type="submit" style={styles.saveButton} onClick={() => this.props.clicked(this.state.value)}>Save</button>
<img src={require('../../images/cancel.png')} style={styles.crossBtn} />
</div>
</div>
)
}
}
First name and last name are imported by parent component in this way
import React, {Component} from 'react';
export default class FirstName extends Component {
render() {
return <div>{this.props.firstName}</div>
}
}
Last name is also like the code given above.
slice does not modify the original array, but returns a new array with the modified values. You need to assign it to a new variable or use a different method of deleting the value.
let newArray = postDelete.slice(index, index + 1);
this.setState({
userNames: newArray
})
In fact, since slice does not mutate the array, you could simplify to this:
this.setState((prevState) => ({
userNames: prevState.userNames.slice(index, index + 1)
)})
However, to accomplish this specific task, you should use a different method like filter
this.setState((prevState) => ({
userNames: prevState.userNames.filter((v,i) => i != index)
)})
This will iterate through the array and filter out all that don't meet the condition. The first argument is the current value and the second is the index. So we only want to keep the values that do not match our index variable.
class App extends React.Component {
state = {
users: [
{
firstName: "Lionel",
lastName: "Messi"
},
{
firstName: "Cristiano",
lastName: "Ronaldo"
},
{
firstName: "Neymar",
lastName: "Jr."
},
{
firstName: "Zlatan",
lastName: "Ibrahimovic"
},
{
firstName: "Ricardo",
lastName: "Kaka"
}
]
};
updateUsers = (updatedUser, index) => {
var users = [...this.state.users];
users[index] = updatedUser;
this.setState({
users
});
};
deleteUser = index => {
var users = [...this.state.users];
users.splice(index, 1);
this.setState({
users
});
};
render() {
return (
<div>
{this.state.users.map((user, index) => {
return (
<PlayerBox
user={user}
key={Math.random()}
index={index}
updateUsers={this.updateUsers}
deleteUser={this.deleteUser}
/>
);
})}
<h1> {JSON.stringify(this.state.users)} </h1>
</div>
);
}
}
class PlayerBox extends React.Component {
state = {
editMode: false,
firstName: "",
lastName: ""
};
componentDidMount() {
const { firstName, lastName } = this.props.user;
this.setState({
firstName,
lastName
});
}
updateParent = () => {
const { index, updateUsers } = this.props;
updateUsers(
{
firstName: this.state.firstName,
lastName: this.state.lastName
},
index
);
this.setState({
editMode: false
});
};
deleteUser = () => {
const { deleteUser, index } = this.props;
this.setState({
editMode: false
});
deleteUser(index);
};
render() {
const { firstName, lastName } = this.props.user;
return this.state.editMode ? (
<div>
<div>
<input
type="text"
value={this.state.firstName}
onChange={e =>
this.setState({
firstName: e.target.value
})
}
/>
<input
type="text"
value={this.state.lastName}
onChange={e =>
this.setState({
lastName: e.target.value
})
}
/>
<button type="submit" onClick={this.updateParent}>
Save
</button>
<button onClick={this.deleteUser}> Delete </button>
</div>
</div>
) : (
<div
onClick={() =>
this.setState({
editMode: true
})
}
>
{firstName} {lastName}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Use of React getDerivedStateFromProps()

I'm exploring React and am somewhat confused over lifecycle methods and parent-child communication. Specifically, I'm trying to create a component which wraps a select element and adds an input box when the "Other" option is selected. I have implemented this using getDerivedStateFromProps() but according to the documentation this lifecycle method should rarely be used. Hence my question: is there another pattern I should be aware of and use in this case?
This is my code, the value and options are passed down as props, as is the handleChange() method of the parent component. So when changes are made in the select or input elements, the parent component state is updated first and a new value is passed down through props.value.
export default class SelectOther extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
static getDerivedStateFromProps(props) {
let optionIndex = -1;
for (let i = 0; i < props.options.length; i++) {
if (props.options[i].value === props.value) {
optionIndex = i;
break;
}
}
if (optionIndex > -1) {
return {
selected: props.options[optionIndex].value,
text: "",
showInput: false
};
} else {
return {
selected: "",
text: props.value,
showInput: true
};
}
}
handleChange(e) {
this.props.handleChange({
"target": {
"name": this.props.name,
"value": e.target.value
}
});
}
render() {
return (
<div>
<label>{ this.props.label }</label>
<select name={ this.props.name } value={ this.state.selected } onChange={ this.handleChange }>
{
this.props.options.map(option => <option key={option.value} value={option.value}>{option.label}</option>)
}
<option value="">Other</option>
</select>
{
this.state.showInput &&
<div>
<label>{ this.props.label } (specify other)</label>
<input type="text" className="form-control" value={ this.state.text } onChange={ this.handleChange }></input>
</div>
}
</div>
)
}
}
You can simplify by not having SelectOther have any state, here is an example of how you can pass a function that dispatches an action to change values. Because SelectOther is a pure component it won't needlessly re render:
//make this a pure component so it won't re render
const SelectOther = React.memo(function SelectOther({
label,
name,
value,
options,
handleChange,
}) {
console.log('in render',name, value);
const showInput = !options
.map(o => o.value)
.includes(value);
return (
<div>
<label>{label}</label>
<select
name={name}
value={showInput ? '' : value}
onChange={handleChange}
>
{options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
<option value="">Other</option>
</select>
{showInput && (
<div>
<label>{label} (specify other)</label>
<input
type="text"
name={name}
className="form-control"
value={value}
onChange={handleChange}
></input>
</div>
)}
</div>
);
});
const App = () => {
//create options once during App life cycle
const options = React.useMemo(
() => [
{ value: 'one', label: 'one label' },
{ value: 'two', label: 'two label' },
],
[]
);
//create a state to hold input values and provide
// a reducer to create new state based on actions
const [state, dispatch] = React.useReducer(
(state, { type, payload }) => {
//if type of action is change then change the
// payload.name field to payload.value
if (type === 'CHANGE') {
const { name, value } = payload;
return { ...state, [name]: value };
}
return state;
},
//initial state for the inputs
{
my_name: '',
other_input: options[0].value,
}
);
//use React.useCallback to create a callback
// function that doesn't change. This would be
// harder if you used useState instead of useReducer
const handleChange = React.useCallback(
({ target: { name, value } }) => {
dispatch({
type: 'CHANGE',
payload: {
name,
value,
},
});
},
[]
);
return (
<div>
<SelectOther
label="label"
name="my_name"
value={state.my_name}
options={options}
handleChange={handleChange}
/>
<SelectOther
label="other"
name="other_input"
value={state.other_input}
options={options}
handleChange={handleChange}
/>
</div>
);
};
//render app
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I could have uses useState in App but then I have to use useEventCallback or do a useState for every input value. The following documentation comes up with the useEventCallback pattern and then immediately after states that we don’t recommend this pattern so that's why I came up with the useReducer solution instead.

Categories