Skip to main content

React-Redux: Intro To Redux-Form

Intro

Using the often maligned Redux-Form library to manage integration between forms and Redux. Will I regret this?

Redux-Form comes with a reducer that we are going to make use of. So to start we need to pull that reducer out of the library and into our reducers index file.

reducers/index.js
import { combineReducers } from "redux"
import { reducer as formReducer } from "redux-form" // highlight-line

import authReducer from "./authReducer"

export default combineReducers({
auth: authReducer,
// key "form" is required
form: formReducer, // highlight-line
})

One quick thing to note, if you do not recognize the syntax reducer as formReducer we are simply giving the alias formReducer to the reducer for our own understanding inside of this index. That way it can match the convention of our previous reducer authReducer.

Scaffolding the Form

Now let us scaffold out an actual form. We need to do a little bit of setup just like we would with Redux.

components/streams/StreamCreate.js
import React, { Component } from "react"
import { Field, reduxForm } from "redux-form"
class StreamCreate extends Component {
render() {
console.log(this.props)
return <div>Create Stream Page</div>
}
}

export default reduxForm({
form: "Create Stream",
})(StreamCreate)

And I put in the console log there just so we could see all of the new props that are now available to us in this component thanks to reduxForm.

new props available

Now we can create the actual inputs. Within the render section we create a form, and then use the new Field component. This component requires that you feed it a couple of props. The first is the name, which is a description of what the fields purpose is. Next you need to give it a "component" which is just the actual HTML for that input. As you can see we defined that with a simple helper function and fed that in.

components/streams/StreamCreate.js
import React, { Component } from "react"
import { Field, reduxForm } from "redux-form"

class StreamCreate extends Component {
renderInput() {
return <input />
}

render() {
return (
<form>
<Field name="title" component={this.renderInput} />
<Field name="description" component={this.renderInput} />
</form>
)
}
}

export default reduxForm({
form: "Create Stream",
})(StreamCreate)

two blank fields

And as you can see we get two blank default fields. Of course later on you can replace these inputs with actual components, the React way.

Turning Inputs into Controlled Inputs

We have now established how we get these inputs to appear on the screen. However currently they are not controlled inputs. As a refresher, here is our post on that subject React:Controlled vs Uncontrolled Elements. In a nutshell, for the element to be controlled we must control the value of the input using state, and the state is updated with the onChange react input property.

Now we are taking this a step further, by using Redux to store the state of the form inputs in the Store, and passing them back down as props instead of being contained inside the element.

Redux-Form has already created all of these props for us, and we can simply capture them and display them with a console log.

components/streams/StreamCreate.js
import React, { Component } from "react"
import { Field, reduxForm } from "redux-form"
class StreamCreate extends Component {
renderInput(formProps) {
// highlight-line
console.log(formProps) // highlight-line
return <input />
}

render() {
return (
<form>
<Field name="title" component={this.renderInput} />
<Field name="description" component={this.renderInput} />
</form>
)
}
}

export default reduxForm({
form: "Create Stream",
})(StreamCreate)

form props in console

And now we can see that the items we need to turn this input into a controlled element are inside those props (value and onChange). Let's go ahead and add those to the element.

components/streams/StreamCreate.js
import React, { Component } from "react"
import { Field, reduxForm } from "redux-form"
class StreamCreate extends Component {
renderInput(formProps) {
console.log(formProps)
return (
<input
value={formProps.input.value}
onChange={formProps.input.onChange}
/>
)
}

render() {
return (
<form>
<Field name="title" component={this.renderInput} />
<Field name="description" component={this.renderInput} />
</form>
)
}
}

export default reduxForm({
form: "Create Stream",
})(StreamCreate)

And now using our Redux-Form devtool we view the state of our form, and see that we are nicely tracking the state of both of these inputs inside the form named Create Stream.

state of form in console tool

Adding All Form Props to Input Element

This is a little bit tedious to be adding all the key value pairs to these form inputs manually. We could do that on our own without using Redux-Form right? Well there is a little bit of a shortcut here, which echoes what you will see in the Redux-Form documentation.

We can add all the input props into the input simultaneously with the {...formProps.input} syntax.

components/streams/StreamCreate.js
import React, { Component } from "react"
import { Field, reduxForm } from "redux-form"
class StreamCreate extends Component {
renderInput(formProps) {
console.log(formProps)
return (
<input {...formProps.input} /> // highlight-line
)
}

render() {
return (
<form>
<Field name="title" component={this.renderInput} />
<Field name="description" component={this.renderInput} />
</form>
)
}
}

export default reduxForm({
form: "Create Stream",
})(StreamCreate)

Now we can take advantage of all of the other features that redux-form provides us.

There is one further small refactor that we can make on this. We can destructure the input method off of formProps and simplify our reference like so.

components/streams/StreamCreate.js
import React, { Component } from "react"
import { Field, reduxForm } from "redux-form"
class StreamCreate extends Component {
renderInput({ input }) {
// highlight-line
return (
<input {...input} /> // highlight-line
)
}

render() {
return (
<form>
<Field name="title" component={this.renderInput} />
<Field name="description" component={this.renderInput} />
</form>
)
}
}

export default reduxForm({
form: "Create Stream",
})(StreamCreate)

Customizing Form Fields

We can easily add custom props to our form fields by adding them into the Field components. They then get passed into the Redux loop and end up in renderInput as props. If we de-structure them out we can easily reference them.

components/streams/StreamCreate.js
import React, { Component } from "react"
import { Field, reduxForm } from "redux-form"
class StreamCreate extends Component {
renderInput({ input, label }) { // highlight-line
return (
<div className="field">
<label>{label}</label> // highlight-line
<input {...input} />
</div>
)
}

render() {
return (
<form>
<Field
name="title"
component={this.renderInput}
label="Enter Title: " // highlight-line
/>
<Field
name="description"
component={this.renderInput}
label="Enter Description: " // highlight-line
/>
</form>
)
}
}

export default reduxForm({
form: "Create Stream",
})(StreamCreate)

custom label prop showing on inputs

Form Submission

Typically for form submission we would create our own helper function that prevents the default behaviour the page and then handle the submission ourselves. Redux-form has a built in function for this however called handleSubmit. We just have to go to the form element and then for the onSubmit action we pass in the redux-form method handleSubmit which takes care of preventing default behaviour and some other things, and then we pass our own onSubmit function into that, which is where we can add our submission actions. Redux-form automatically passes the values of the submitted form into this function, which here we have labelled formValues.

components/streams/StreamCreate.js
import React, { Component } from "react";
import { Field, reduxForm } from "redux-form";
class StreamCreate extends Component {
renderInput({ input, label }) {
return (
<div className="field">
<label>{label}</label>
<input {...input} />
</div>
);
}

// form values are returned to us
onSubmit(formValues) {
console.log(formValues);
}

render() {
return (
<form className="ui form" onSubmit={this.props.handleSubmit(this.onSubmit)}> // highlight-line
<Field name="title" component={this.renderInput} label="Enter Title: " />
<Field name="description" component={this.renderInput} label="Enter Description: " />
<button className="ui button primary">Submit</button> // highlight-line
</form>
);
}
}

export default reduxForm({
form: "Create Stream",
})(StreamCreate);

and we can see these form values in the console from our log.

console logged form values object

Input Validation

There is a specific pattern to redux-form validation that we are going to be repeating over and over again. There is a bit of a curve to learning this pattern, but on the flip side, once we know it, validation will be very easy. Let's start with a flow diagram.

redux-form input validation flow-chart

So the first thing to understand here is that redux-form is going to attempt to validate our inputs constantly. When the form first renders and every single time the user interacts with it. Any interaction causes the validate() function to be called. We get to define the validate function to let redux-form know what sort of input we are willing to accept. The validate function gets called with all the values of the form validate(formValues).

Let us take a look at a very simple validation function that simply checks the user entered anything into the title input.

components/streams/StreamCreate.js
import React, { Component } from "react";
import { Field, reduxForm } from "redux-form";
class StreamCreate extends Component {
renderInput({ input, label }) {
return (
<div className="field">
<label>{label}</label>
<input {...input} />
</div>
);
}

onSubmit(formValues) {
console.log(formValues);
}

render() {
return (
<form className="ui form" onSubmit={this.props.handleSubmit(this.onSubmit)}>
<Field name="title" component={this.renderInput} label="Enter Title: " />
<Field name="description" component={this.renderInput} label="Enter Description: " />
<button className="ui button primary">Submit</button>
</form>
);
}
}

const validate = (formValues) => {
if (!formValues.title) {
// user did not enter a title
}
}

export default reduxForm({
form: "Create Stream",
})(StreamCreate);

and there we can see where we would insert our object with an error message pertaining to this particular check. We can also add a quick check on the description field.

components/streams/StreamCreate.js
import React, { Component } from "react";
import { clearSubmitErrors, Field, reduxForm } from "redux-form";
class StreamCreate extends Component {
renderInput({ input, label }) {
return (
<div className="field">
<label>{label}</label>
<input {...input} />
</div>
);
}

onSubmit(formValues) {
console.log(formValues);
}

render() {
return (
<form className="ui form" onSubmit={this.props.handleSubmit(this.onSubmit)}>
<Field name="title" component={this.renderInput} label="Enter Title: " />
<Field name="description" component={this.renderInput} label="Enter Description: " />
<button className="ui button primary">Submit</button>
</form>
);
}
}

const validate = (formValues) => {
if (!formValues.title) {
errors.title = 'You must enter a title'; // highlight-line
}

if (!formValues.description) {
errors.description = 'You must enter a description'; // highlight-line
}
}

export default reduxForm({
form: "Create Stream",
})(StreamCreate);

and lastly we need to actually declare the errors object, and return it at the end of the validate function. If none of the if statements trigger it will simply return an empty object just as we declared it at the top.

components/streams/StreamCreate.js
import React, { Component } from "react";
import { clearSubmitErrors, Field, reduxForm } from "redux-form";
class StreamCreate extends Component {
renderInput({ input, label }) {
return (
<div className="field">
<label>{label}</label>
<input {...input} />
</div>
);
}

onSubmit(formValues) {
console.log(formValues);
}

render() {
return (
<form className="ui form" onSubmit={this.props.handleSubmit(this.onSubmit)}>
<Field name="title" component={this.renderInput} label="Enter Title: " />
<Field name="description" component={this.renderInput} label="Enter Description: " />
<button className="ui button primary">Submit</button>
</form>
);
}
}

const validate = (formValues) => {
const errors = {}; // highlight-line

if (!formValues.title) {
errors.title = 'You must enter a title';
}

if (!formValues.description) {
errors.description = 'You must enter a description';
}

return errors; // highlight-line
}

export default reduxForm({
form: "Create Stream",
})(StreamCreate);

This all follows our diagram perfectly so far. The next step is to wire this up to Redux-Form, as we have not actually called the validate function yet. To do this we go down to our reduxForm helper and give it a new key value pair validate: validate. The key being a fixed expected value for redux-form, and the second being the name of our function that we have just created.

components/streams/StreamCreate.js
import React, { Component } from "react";
import { clearSubmitErrors, Field, reduxForm } from "redux-form";
class StreamCreate extends Component {
renderInput({ input, label }) {
return (
<div className="field">
<label>{label}</label>
<input {...input} />
</div>
);
}

onSubmit(formValues) {
console.log(formValues);
}

render() {
return (
<form className="ui form" onSubmit={this.props.handleSubmit(this.onSubmit)}>
<Field name="title" component={this.renderInput} label="Enter Title: " />
<Field name="description" component={this.renderInput} label="Enter Description: " />
<button className="ui button primary">Submit</button>
</form>
);
}
}

const validate = (formValues) => {
const errors = {};

if (!formValues.title) {
errors.title = 'You must enter a title';
}

if (!formValues.description) {
errors.description = 'You must enter a description';
}

return errors;
}

export default reduxForm({
form: "Create Stream",
validate: validate // highlight-line
})(StreamCreate);

Which, thanks to ES6 syntax, because the key:value pair are identical, could just be shortened to validate. To me this lacks clarity in this case so I'm going to leave it as is.

Validation Messages

Here is where redux-form does a little bit of magic for us. It checks to see if there are any properties of the errors object that match the name of a name for a field. For example errors.description has a matching property to <Field name='description' />, and if so it will then pass down the error message to the renderInput function. Also note that this is a case sensitive match!

To get access to these error messages we can de-structure off a new prop called meta.

components/Streams/StreamCreate.js
import React, { Component } from "react";
import { clearSubmitErrors, Field, reduxForm } from "redux-form";
class StreamCreate extends Component {
renderInput({ input, label, meta }) {
console.log(meta);
return (
<div className="field">
<label>{label}</label>
<input {...input} />
</div>
);
}

onSubmit(formValues) {
console.log(formValues);
}

render() {
return (
<form className="ui form" onSubmit={this.props.handleSubmit(this.onSubmit)}>
<Field
name="title"
component={this.renderInput}
label="Enter Title: "
/>
<Field
name="description"
component={this.renderInput}
label="Enter Description: "
/>
<button className="ui button primary">Submit</button>
</form>
);
}
}

const validate = (formValues) => {
const errors = {};

if (!formValues.title) {
errors.title = 'You must enter a title';
}

if (!formValues.description) {
errors.description = 'You must enter a description';
}

return errors;
}

export default reduxForm({
form: "Create Stream",
validate: validate
})(StreamCreate);

which gives us all these amazing meta properties which we can use to display an error message.

form meta properties

Right away we can see that the submitFailed boolean could be helpful, as we could wait to display our error messages until this is true. Or we could harness the power of React and display the error message once touched is true, so that the user will be alerted that the field is invalid before they even submit the form. React is re-rendering the form and checking for validation after every single user input.

Let us add a new piece to the renderInput, an error message. We can create a helper function for this validation message that only returns content if the meta data shows that the input has been touched, and also that there is an error.

components/Streams/StreamCreate.js
import React, { Component } from "react";
import { clearSubmitErrors, Field, reduxForm } from "redux-form";
class StreamCreate extends Component {

renderError({ error, touched }) {
if (touched && error) {
return (
<div className="ui pointing label">
{error}
</div>
);
}
}

// must be arrow function so that context of this is bound for this.renderError
renderInput = ({ input, label, meta }) => {
return (
<div className="field">
<label>{label}</label>
<input {...input} />
{this.renderError(meta)} // highlight-line
</div>
);
}

onSubmit(formValues) {
console.log(formValues);
}

render() {
return (
<form
className="ui form"
onSubmit={this.props.handleSubmit(this.onSubmit)}
>
<Field
name="title"
component={this.renderInput}
label="Enter Title: "
/>
<Field
name="description"
component={this.renderInput}
label="Enter Description: "
/>
<button className="ui button primary">Submit</button>
</form>
);
}
}

const validate = (formValues) => {
const errors = {};

if (!formValues.title) {
errors.title = "You must enter a title";
}

if (!formValues.description) {
errors.description = "You must enter a description";
}

return errors;
};

export default reduxForm({
form: "Create Stream",
validate: validate,
})(StreamCreate);

Also note that we converted the renderInput function to an arrow function. This is because without this we will run into a context error when the this.renderError reference inside of renderInput gets passed into the render() function. Unless the context is bound with an arrow function, the context of this would be lost there.

So now we would expect that if we clicked on an input and clicked away without inputting anything in either of these fields, we should get an error message in a Semantic UI pointing box. Let's see.

error messages are working

GitHub Repo

Ncoughlin: React-Streams-Client

Comments

Recent Work

Free desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.

Learn More

BidBear

bidbear.io

Bidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.

Learn More