Cant access shallow rendered component when testing - 0 nodes found - reactjs

I am trying to unit-test my React components using JEST and enzyme.
In this case I wanna test a redux-form component, so I tried following this guide:
https://github.com/tylercollier/redux-form-test/blob/master/tests/unit/index.js#L10
But I always get the error: Method “simulate” is only meant to be run on a single node. 0 found instead.
I realize that this is because it cant find the element that I am locating in .find('someElement'), but I cant figure out how to access it correctly. I have tried changing form in subject.find('form') to MenuInputFields and inputItemList, but the result is the same.
EDIT
I found that the error is present because the component is wrapped in a redux-form with:
MenuInputFields = reduxForm({
form: 'inputItemList',
validate
})(MenuInputFields)
Removing this makes it possible to somewhat test the component, however this is (of course) not optimal. When removing the connection with redux-forms I am able to access the form, but not any of the elements within it, accessing anything else than forms always yields the same error as I got before (shown further up).
I have changed the output of subject.debug() to the result after i removed the redux-form-wrapping, and now you can see that my fields are actually in the form, I just cant access them. I also update the testcases to show what works and what doesnt.
My Test:
import React from 'react'
import configureStore from 'redux-mock-store'
import Adapter from 'enzyme-adapter-react-15'
import * as sinon from 'sinon'
import * as Enzyme from 'enzyme'
import { Provider } from 'react-redux'
import { mount, shallow, render } from 'enzyme'
import { menuList } from '../../stubs/menuList'
import { MenuInputFields } from '../../components/adminMenuInputFields'
Enzyme.configure({ adapter: new Adapter() })
describe('<MenuInputFields />', () => {
let subject
let handleSubmit, pristine, reset, submitting, submitForm, touched, error
beforeEach(() => {
submitting = false
pristine = true
error = null
reset = sinon.spy()
handleSubmit = fn => fn
})
const buildSubject = () => {
submitForm = sinon.stub()
const props = {
submitForm,
initialValues: {
ClosingTime: '14:00',
MenuItems: {
FoodItem: 'test',
Description: 'test',
Price: '66'
}
},
handleSubmit,
reset
}
return shallow(<MenuInputFields {...props}/>)
}
//This works
it('it calls submitForm once when submitting', () => {
subject = buildSubject()
subject.find('form').simulate('submit')
expect(submitForm.calledOnce).toBeTruthy()
})
//This doesnt work
it('loads initial value of ClosingTime into input-box ', () => {
subject = buildSubject()
let closingTime = subject.find('form')
expect(closingTime).toEqual('14:00')
})
//This doesnt work
it('Renders the "tilføj" button', () => {
subject = buildSubject()
let button = subject.find('.addItemRow')
expect(button.text()).toEqual('Tilføj')
})
})
My component
class MenuInputFields extends React.Component {
constructor(props){
super(props)
}
render() {
let currentDate = new Date()
let formattedDate = currentDate.toLocaleDateString('en-GB')
const renderField = ({ input, label, type, className, meta: { touched, error } }) => (
<div>
<input {...input} type={type} placeholder={label} className={className} />
{touched && error && <span>{error}</span>}
</div>
);
const renderMenuItem = ({fields, meta: {touched, error, submitFailed }}) => (
<ul className='adminInputFoodItem'>
{fields.map((MenuItem, index) => (
<li key={index}>
<p className='col-md-1'> {index+1} - </p>
<Field
name={`${MenuItem}.FoodItem`}
type='text'
component={renderField}
label='Titel'
className='col-md-2'
/>
<Field
name={`${MenuItem}.Description`}
type='text'
component={renderField}
label='Beskrivelse'
className='col-md-6'
/>
<Field
name={`${MenuItem}.Price`}
type='number'
component={renderField}
label='Pris'
className='col-md-1'
/>
<button
type='button'
title='Fjern'
onClick={() => fields.remove(index)}
className='btn btn-default btn-xs remItemRow'
>Fjern
</button>
</li>
))}
<li>
</li>
<li>
<button
type='button'
onClick={() => fields.push({})}
className='btn btn-default btn-sm addItemRow'
>Tilføj
</button>
{submitFailed && error && <span>{error}</span>}
</li>
</ul>
);
const { handleSubmit, pristine, reset, submitting, submitForm } = this.props
return (
<form className='adminInputForm' onSubmit={handleSubmit(submitForm)}>
<label className='col-md-2' >Dato: {formattedDate}</label>
<br />
<br />
<ul className='adminInputFoodItem'>
<li>
<label className='col-md-1'> Lukketid: </label>
<Field
name='ClosingTime'
type='text'
component={renderField}
label='TT:mm'
className='col-md-1'
/>
</li>
</ul>
<hr />
<div>
<label className='col-md-1'> # </label>
<label className='col-md-2'> Titel </label>
<label className='col-md-3'> Beskrivelse </label>
<label className='col-md-1'> Pris </label>
</div>
<br />
<br />
<FieldArray name='MenuItems' component={renderMenuItem} />
<br />
<div className=''>
<button
className='btn btn-default'
type='submit'
disabled={pristine || submitting}
>Gem Menu
</button>
<label>Submit besked her</label>
</div>
</form>
)
}
}
MenuInputFields = reduxForm({
form: 'inputItemList',
validate
})(MenuInputFields)
export default MenuInputFields
The container which renders the component:
const mapDispatchToProps = (dispatch) => {
return {
setMenu: (menu) => dispatch(setMenu(menu)),
submitForm: (newMenu) => dispatch(postMenuRequest(newMenu))
}
}
class AdminSetMenuView extends React.Component {
constructor(props){
super(props)
}
componentWillMount(){
this.props.setMenu(this.props.data)
}
render(){
return(
<div>
<AdminRoutes />
<br />
<br />
<div>
<MenuInputFields
initialValues={this.props.initialValues}
submitForm={this.props.submitForm} />
</div>
</div>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(AdminSetMenuView)
output from console.log(subject.debug()):
<form className="adminInputForm" onSubmit={[Function: proxy]}>
<label className="col-md-2">
Dato:
3/8/2018
</label>
<br />
<br />
<ul className="adminInputFoodItem">
<li>
<label className="col-md-1">
Lukketid:
</label>
<Field name="ClosingTime" type="text" component={[Function: renderField]} label="TT:mm" className="col-md-1" />
</li>
</ul>
<hr />
<div>
<label className="col-md-1">
#
</label>
<label className="col-md-2">
Titel
</label>
<label className="col-md-3">
Beskrivelse
</label>
<label className="col-md-1">
Pris
</label>
</div>
<br />
<br />
<FieldArray name="MenuItems" component={[Function: renderMenuItem]} />
<br />
<div className="">
<button className="btn btn-default" type="submit" disabled={[undefined]}>
Gem Menu
</button>
<label>
Submit besked her
</label>
</div>

Related

How to pass item's data to another component, edit data and save data in React.js?

I have a book table page to list all the books in the database, mapped with their book ID. If I click the book name, it will link to an edit book page, in this page, the input fields should be filled with the book detail, I can edit the book detail and save it in the database.
The problem is, how do I pass the book detail to another component?
Book table component
class BookTable extends Component {
constructor(props){
super(props)
this.state = {
books: [],
book: {}
}
render() {
return (
<div className='display-container'>
<div className="card-group-admin">
<div className="container">
<div className="row row-cols-3">
{this.state.books.map(book =>
<div className="card" style={{height:550 + 'px', width:320 + 'px'}} key={book._id}>
<img src={`http://localhost:3001/${book.bookImage}`} className="card-img-top" alt="comic book coverpage" />
<div className="card-body">
<Link to={`/books/${book._id}`} book={this.setState={book}}><h5 className="card-title">{book.bookName}</h5></Link>
<p className="card-text">{book.bookDescription}</p>
<div className="card-bottom-container">
<p className="card-status">{book.bookStatus}</p>
<button type="button" onClick={() => this.handleDeleteClick(book._id)}
className="btn btn-secondary" id="btnLength">Delete</button>
</div>
</div>
</div>)
}</div>
</div>
</div>
</div>
I am not sure whether "book={this.setState={book}}" inside the tag is correct or not, that's probably is where the problem at.
Edit component
import React, {useState} from 'react';
import { useHistory } from "react-router-dom";
import {book} from './BookTable';
function Edit ({book}) {
const [bookName, setBookName] = useState(book.bookName);
const [author, setAuthor] = useState(book.author);
const [publisher, setPublisher] = useState(book.publisher);
const [yearReleased, setYearReleased] = useState(book.yearReleased);
const [type, setType] = useState(book.type);
const [advancedBookType, setAdvancedBookType] = useState(book.advancedBookType);
const [rentalPrice, setRentalPrice] = useState(book.rentalPrice);
const [bookDescription, setBookDescription] = useState(book.bookDescription);
const handleSubmit = (e) => {
e.preventDefault();
const bookDetail = {
bookName,
author,
publisher,
yearReleased,
type,
advancedBookType,
rentalPrice,
bookDescription };
axios.put(`http://localhost:3001/app/books/${_id}`, bookDetail)
.then(response => {
console.log(response.data);
})
.catch(err => console.log(err));
}
const history = useHistory();
const handleBack = () => {
history.push("/adminDashboard");
}
return (
<div className="editPage">
<h1>Edit book page</h1>
<form onSubmit={handleSubmit} >
<label className="editLabel">Book name:</label>
<input type = 'text'
value={book.bookName}
onChange={(e) => setBookName(e.target.value)}
className="form-control form-group form-control-sm input" />
<label className="editLabel">Author:</label>
<input type = 'text'
value={book.author}
onChange={(e) => setAuthor(e.target.value)}
className="form-control form-group form-control-sm input" />
<label className="editLabel">Publisher:</label>
<input type = 'text'
value={book.publisher}
onChange={(e) => setPublisher(e.target.value)}
className="form-control form-group form-control-sm input" />
<label className="editLabel">Year released:</label>
<input type = 'number'
value={book.yearReleased}
onChange={(e) => setYearReleased(e.target.value)}
className="form-control form-group form-control-sm input" />
<label className="editLabel">Type:</label>
<select type = 'text'
value={book.type}
onChange={(e) => setType(e.target.value)}
className="form-control form-group form-control-sm input" id="exampleFormControlSelect1">
<option>Comedy</option>
<option>Love</option>
<option>Horror</option>
<option>Detecting</option>
<option>Fiction</option>
<option>Adventure</option>
<option>Action</option>
<option>Youth</option>
</select>
<label className="editLabel">Advanced book type:</label>
<select type = 'text'
value={book.advancedBookType}
onChange={(e) => setAdvancedBookType(e.target.value)}
className="form-control form-group form-control-sm input" id="exampleFormControlSelect1">
<option >None</option>
<option>Popular</option>
<option>New release</option>
</select>
<label className="editLabel">Rental price ($):</label>
<input type = 'number'
value={book.rentalPrice}
onChange={(e) => setRentalPrice(e.target.value)}
className="form-control form-group form-control-sm input" />
<label className="editLabel">Book description:</label>
<textarea value={book.bookDescription}
onChange={(e) => setBookDescription(e.target.value)}
className="form-control form-group textbox" rows="4">
</textarea>
<div className="buttonGroup">
<button onClick={() => handleBack()} className="btn btn-outline-dark">Back</button>
<input type="submit" className="btn btn-primary" id="right" value="Save" />
{/* <input type="reset" className="btn btn-outline-secondary" id="right" value="Delete" /> */}
</div>
</form>
</div>
);
}
export default Edit;
Backend book route
router.put('/books/:id', (request, response) => {
Book.findOneAndUpdate({ _id: request.params.id }, request.body, { new:true, useFindAndModify: false },
(err, book) => {
if (err) {
response.send(err);
}
response.json(book);
});
})
<Link to={{ pathname: `/books/${book._id}`, state: { data: book } }}> {book.bookName} </Link>
in Edit component
const { data } = props.location.state;

How to submit file input along with text input to backend in react?

I am trying to submit a file and text input to backend with POST request, the code looks like this,
state={
text:'',
file:''
}
handleTextChange = (e) => {
this.setState({[e.target.name]:e.target.value})
}
handleFileChange = (e) => {
this.setState({file:e.target.files[0]})
}
handleSubmit = (e) => {
const {text,file} = this.state
//POST request with text and file
}
But the file is always empty, If I console.log(e.target.files[0]), it shows a FileList in console but somehow it is not updated in state
I am quite new in react, your help is very much appreciated, thanks
EDIT: the form looks like this
<form action="" className="w-100" onSubmit={(e) => this.handleSubmit(e)}>
<div className="pt-1 pb-3">
<TextAreaAutosize
className="form-control"
placeholder="write some text here"
minRows="4"
name="text"
onChange={(e) => this.handleChange(e)}
/>
</div>
<div className="w-100 d-flex justify-content-between align-items-center">
<div>
<input
type="file"
className="form-control-file"
name="file"
onChange={(e) => this.handleFileChange(e)}
/>
</div>
<div>
<button type="submit" className="btn btn-primary">
Post
</button>
</div>
</div>
</form>
sandbox:
https://codesandbox.io/s/friendly-banzai-8ifw4?fontsize=14&hidenavigation=1&theme=dark
try this one works just fine
before install
run npm install #material-ui/core
import React, { Component } from 'react';
import './App.css';
import { TextareaAutosize } from '#material-ui/core';
class App extends Component {
constructor(props) {
super(props)
this.state = {
text: '',
file: ''
}
this.handleFileChange=this.handleFileChange.bind(this)
this.handleTextChange=this.handleTextChange.bind(this)
this.handleSubmit=this.handleSubmit.bind(this)
}
handleTextChange = (e) => {
this.setState({ [e.target.name]: e.target.value })
}
handleFileChange = (e) => {
console.log(e.target.files[0])
this.setState({ file: e.target.files[0] })
}
handleSubmit = (e) => {
const { text, file } = this.state
}
render() {
return (
<div className="App">
<form action="" className="w-100" onSubmit={(e) => this.handleSubmit(e)}>
<div className="pt-1 pb-3">
<TextareaAutosize
className="form-control"
placeholder="write some text here"
minRows="4"
name="text"
onChange={(e) => this.handleChange(e)}
/>
</div>
<div className="w-100 d-flex justify-content-between align-items-center">
<div>
<input
type="file"
className="form-control-file"
name="file"
onChange={(e) => this.handleFileChange(e)}
/>
</div>
<div>
<button type="submit" className="btn btn-primary">
Post
</button>
</div>
</div>
</form>
</div>
);
}
}
export default App;

FieldArray re-registering fields after each char-input

Hi I'm quite new to React and I'm trying to learn how to work with redux-forms (https://redux-form.com/7.3.0/) and it mostly works, but I am encountering an issue which I cant solve. The issue is the same as this one: https://github.com/erikras/redux-form/issues/2412
The issue in short:
Whenever i try to write something in an input-field, my form constantly re-registers the field, which makes the form almost impossible to work with. The bad behavior happens because of validation of inputs.
Although Marnusw seems to have solved it with this comment:
<Field ... validate={[required, minLength(6)]}/>
Every time the form was rendered a new minLength instance would be
created which caused the field to UNREGISTER_FIELD and then
REGISTER_FIELD again.
The minLength instance should be created once outside of the render
function:
const minLength6 = minLength(6)
<Field ... validate={[required, minLength6]}/>
However I am not connecting my validation to the component like he is, so I am unsure of how I can declare my validation outside the render() function in my code.
I have tried doing:
...(imports)
const validation = validate //declaring a const and assigning 'validate' to it
//instead of directly using validate.
MenuInputFields = reduxForm({
form: 'inputItemList',
validation
})(MenuInputFields)
But this disabled validation alltogether, which (of course) also solves the problem, but this is not a solution, since I need to validate inputs.
My component:
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { reduxForm, Field, FieldArray } from 'redux-form'
import { menuSelector } from '../reducers/menuViewReducer'
import { postMenuRequest } from '../sagas/adminMenuSaga'
import validate from './validateForm'
const mapDispatchToProps = (dispatch) => {
return {
submitForm: (newMenu) => dispatch(postMenuRequest(newMenu))
}
}
export class MenuInputFields extends React.Component {
constructor(props){
super(props)
}
render() {
let currentDate = new Date()
let formattedDate = currentDate.toLocaleDateString('en-GB')
const renderField = ({ input, label, type, className, meta: { touched, error } }) => (
<div>
<input {...input} type={type} placeholder={label} className={className} />
{touched && error && <span>{error}</span>}
</div>
);
const renderMenuItem = ({fields, meta: {touched, error, submitFailed }}) => (
<ul className='adminInputFoodItem'>
{fields.map((MenuItem, index) => (
<li key={index}>
<p className='col-md-1'> {index+1} - </p>
<Field
name={`${MenuItem}.FoodItem`}
type='text'
component={renderField}
label='Titel'
className='col-md-2'
/>
<Field
name={`${MenuItem}.Description`}
type='text'
component={renderField}
label='Beskrivelse'
className='col-md-6'
/>
<Field
name={`${MenuItem}.Price`}
type='number'
component={renderField}
label='Pris'
className='col-md-1'
/>
<button
type='button'
title='Fjern'
onClick={() => fields.remove(index)}
className='btn btn-default btn-xs remItemRow'
>Fjern
</button>
</li>
))}
<li>
</li>
<li>
<button
type='button'
onClick={() => fields.push({})}
className='btn btn-default btn-sm addItemRow'
>Tilføj
</button>
{submitFailed && error && <span>{error}</span>}
</li>
</ul>
);
const { handleSubmit, pristine, reset, submitting, submitForm } = this.props
return (
<form className='adminInputForm' onSubmit={handleSubmit(submitForm)}>
<label className='col-md-2' >Dato: {formattedDate}</label>
<br />
<br />
<ul className='adminInputFoodItem'>
<li>
<label className='col-md-1'> Lukketid: </label>
<Field
name='ClosingTime'
type='text'
component={renderField}
label='TT:mm'
className='col-md-1'
/>
</li>
</ul>
<hr />
<div>
<label className='col-md-1'> # </label>
<label className='col-md-2'> Titel </label>
<label className='col-md-3'> Beskrivelse </label>
<label className='col-md-1'> Pris </label>
</div>
<br />
<br />
<FieldArray name='MenuItems' component={renderMenuItem} />
<br />
<div className=''>
<button
className='btn btn-default'
type='submit'
disabled={pristine || submitting}
>Gem Menu
</button>
<label>Submit besked her</label>
</div>
</form>
)
}
}
MenuInputFields = reduxForm({
form: 'inputItemList',
validate,
})(MenuInputFields)
export default connect(mapStateToProps, mapDispatchToProps)(MenuInputFields)
validation.js
//regex patterns to match against
const timeFormat = /^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/
const specialCharsTextInput = /^[^|<>\"&]*$/
const emptyTextInput = /^$/
//Error texts
const requiredErrText = 'Required'
const timeErrText = ' - Forkert input - Format: TT:MM'
const noItemsErrText = { _error: 'Tilføj mindst 1 menu' }
const validate = values => {
const errors = {}
if (!values.ClosingTime || emptyTextInput.test(values.ClosingTime)) {
errors.ClosingTime = requiredErrText
}
if(values.ClosingTime && !timeFormat.test(values.ClosingTime)){
errors.ClosingTime = timeErrText
}
if (!values.MenuItems || !values.MenuItems.length) {
errors.MenuItems = noItemsErrText
} else {
const MenuItemsArrayErrors = []
values.MenuItems.forEach((MenuItem, MenuItemIndex) => {
const MenuItemErrors = {}
if (!MenuItem || !MenuItem.FoodItem || !specialCharsTextInput.test(MenuItem.FoodItem) || !emptyTextInput.test(MenuItem.FoodItem)) {
MenuItemErrors.FoodItem = requiredErrText
MenuItemsArrayErrors[MenuItemIndex] = MenuItemErrors
} else if (!MenuItem || !MenuItem.Description || !specialCharsTextInput.test(MenuItem.Description) || !emptyTextInput.test(MenuItem.Description)) {
MenuItemErrors.Description = requiredErrText
MenuItemsArrayErrors[MenuItemIndex] = MenuItemErrors
} else if (!MenuItem || !MenuItem.Price || !specialCharsTextInput.test(MenuItem.Price) || !emptyTextInput.test(MenuItem.Price)) {
MenuItemErrors.Price = requiredErrText
MenuItemsArrayErrors[MenuItemIndex] = MenuItemErrors
}
})
if (MenuItemsArrayErrors.length) {
errors.MenuItems = MenuItemsArrayErrors
}
}
return errors
}
export default validate

Unable to type in input after adding value in React Js

I have tried with the following code with react js
import React, { Component } from 'react';
import Sidebar from './../Sidebar';
import Header from './../Header';
import {get_profile} from './../services/Services';
import $ from 'jquery';
export default class Profile extends Component {
constructor(props) {
super(props);
this.state = { userDetails: '' } ;
}
componentWillMount() {
$(window).scrollTop(0)
console.log('sdfds')
get_profile().then((response) => {
var userResults = $.parseJSON(response.text);
this.setState({ userDetails: userResults.response });
console.log(userResults);
}).catch((error) => {
//alert(error)
console.log(error);
});
}
handleChange(event) {
console.log(event.target.id)
this.setState({[event.target.name]: event.target.value});
}
render() {
return (
<div className="admin-panel">
<Sidebar />
<Header />
<div className="main-content">
<div className="contents">
<div className="container-fluid">
<div className="row">
<div className="col-sm-6">
<h2 className="page-title">Profile</h2>
<div className="form-group">
<label >First Name</label>
<input type="text" id="firstName" className="form-control" value={this.state.userDetails.firstName} />
</div>
<div className="form-group">
<label >Last Name</label>
<input type="text" id="lastName" className="form-control" value={this.state.userDetails.lastName} />
</div>
<div className="form-group">
<label >Email</label>
<input type="text" id="email" name="email" className="form-control" value={this.state.userDetails.email} />
</div>
<button type="button" className="btn btn-squared btn-success margin-inline" onClick={(e)=> {this.profileUpdate()}}>Update</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
}
After fetching the results i'm unable to edit the input box value. I have tried with defaultValue but that also not working for me. I'm struck with this issue, Kindly help me to solve the issue.
Just pass an onChange handler to the input which will update the value of the corresponding key of userDetails like this:
<input
...
value={this.state.userDetails.firstName}
onChange={this.onChange}
/>
onChange(e) {
this.setState((previousState) => {
const userDetails = previousState.userDetails
return { userDetails: {...userDetails, firstName: e.target.value} }
})
}
or you can write a common onChange function like this:
<input
...
value={this.state.userDetails.firstName}
onChange={(e) => {this.onChange(e.target.value, 'firstName')}}
/>
<input
...
value={this.state.userDetails.lastName}
onChange={(e) => {this.onChange(e.target.value, 'lastName')}}
/>
<input
...
value={this.state.userDetails.email}
onChange={(e) => {this.onChange(e.target.value, 'email')}}
/>
onChange(value, key) {
this.setState((previousState) => {
const userDetails = previousState.userDetails
return { userDetails: {...userDetails, [key]: value} }
})
}

Redux form with self creating inputs

I'm trying to create a redux form (using redux-form) that can dynamically create it's own inputs. I am having trouble figuring out how to make redux-form aware of the new fields that have been created. Is it possible to dynamically change the fields prop that redux-form passes in within the form component itself? Am I thinking about this wrong? Here is what I am working with.
class AddCustomer extends Component {
render() {
class Form extends Component {
constructor(props, context) {
super(props, context)
this.state = {
inputsToAdd: []
};
}
handleAddInput() {
let inputsToAdd = this.state.inputsToAdd.slice();
inputsToAdd.push(this.state.inputsToAdd.length);
this.setState({ inputsToAdd });
}
submitForm(data) {
console.log(data)
this.setState({inputsToAdd: []});
this.props.dispatch(initialize('addCustomer', {}))
}
render() {
const { fields, handleSubmit } = this.props;
return (
<div>
<form onSubmit={handleSubmit(this.submitForm.bind(this))}>
<Input type='text' label='Company name' {...fields['companyName']}/>
<Input type='email' label='Admin user email' {...fields['adminEmail']}/>
</form>
{this.state.inputsToAdd.map((element, index) => {
return (
<Input key={index} type='text' />
)
})}
<Button onClick={() => this.handleAddInput()}>Add Input</Button>
<Button onClick={handleSubmit(this.submitForm.bind(this))}>Submit</Button>
</div>
)
}
}
Form = connectReduxForm({
form: 'addCustomer',
fields: ['companyName', 'adminEmail']
})(Form);
return (
<div>
<h1>Add Customer</h1>
<Form />
</div>
)
}
}
As of Redux Form 6.* you can achieve what you are trying to do using <FieldArray/>
See the example below (taken from Redux documentation and slightly simplified)
import React from 'react'
import { Field, FieldArray, reduxForm } from 'redux-form'
import validate from './validate'
const renderMembers = ({ fields, meta: { touched, error } }) => (
<ul>
<li>
<button type="button" onClick={() => fields.push({})}>Add Member</button>
{touched && error && <span>{error}</span>}
</li>
{fields.map((member, index) =>
<li key={index}>
<button
type="button"
title="Remove Member"
onClick={() => fields.remove(index)}/>
<h4>Member #{index + 1}</h4>
<Field
name={`${member}.firstName`}
type="text"
component={renderField}
label="First Name"/>
<Field
name={`${member}.lastName`}
type="text"
component={renderField}
label="Last Name"/>
</li>
)}
</ul>
)
const FieldArraysForm = (props) => {
const { handleSubmit, submitting } = props
return (
<form onSubmit={handleSubmit}>
<FieldArray name="members" component={renderMembers}/>
<div>
<button type="submit" disabled={submitting}>Submit</button>
</div>
</form>
)
}
export default reduxForm({
form: 'fieldArrays', // a unique identifier for this form
validate
})(FieldArraysForm)
For more info checkout the documentation
http://redux-form.com/6.1.1/examples/fieldArrays/

Resources