Custom Form Validation in React with Redux Middleware
Redux provides a clean architecture for state management. So why do we continue to muddy our components with complex validation logic? Instead, let’s leverage Redux, with the help of some custom middleware!
Redux and State Management
Redux provides a centralized state management system for our React apps. We subscribe our component tree to a central store and state changes are enacted via a data-down-actions-up pattern. Actions are dispatched to the store, the store uses a reducer to change state and broadcast the new state to our components, and the components then re-render.
Letting Redux manage our React application’s state means taking (most) of that responsibility away from individual components–– even our big meaty container components. We don’t let our components establish complex internal state and we don’t weigh these components down with complex logic to update such state. Instead, we use the Redux store to shape our application’s state; action creator functions to communicate the need for state changes; reducers to make state changes. So why should we treat our form validation and error handling any differently?
Despite the adherence of so many React developers to the Redux architecture, it’s still common to see complex form components that handle their own validations and errors. Let’s allow Redux to do what it does best and manage such interactions for us!
The App
Note: You can check out the complete code for this project on GitHub here, and you can play around with a live demo here. Keep in mind that this is simple dummy app and as such does not have a persistence layer. Sorry, we’re not really saving your form responses :(
You may have heard that we can travel to space now. Elon Musk is looking to staff a mission to Mars. All of the world’s top astronauts and nerds are competing for a spot on the ship. In order to apply for a position, you have to fill out a pretty complicated, rigorous application form. As the developers behind this form, we need to implement a complex set of form validations.
Here’s a look at the behavior we’re going for:
Our form validations range from the standard:
- Without the required fields of name and email, the form cannot be submitted.
- Email must be a properly formatted email address.
To the more complicated:
- The email a user supplies must be their official SpaceEx email address––
name@space.ex
––as only registered SpaceEx members can apply for this mission. - If an applicant checks that they do have experience terraforming other planets, they must fill out the “which planets have you terraformed?” text field.
- The “which planets have you terraformed?” text field cannot contain “Mars”––this is a mission to Mars, we know you didn’t terraform it already!
We can imagine that the list of complex form validations could go on and on. Trying to manage all of this in one component, let’s say a FormContainer
component, will get really messy, really fast. Instead, we’ll offload the form validation and the population of error messages to Redux.
Application State
Our app is pretty simple–it displays an astronaut application form and submits that form. Our initial state looks like this:
// client/src/store/initialStates/astronaut.js
{
astronaut: {
id: null,
name: "",
email: "",
terraform_experience: false,
terraform_planets: ""
}
}
The Component Tree
Our component architecture is also simple. We have a top-level container component: AstronautForm
that contains some child components, each of which represent a section of the form.
Here’s a simplified look:
client/src/components/AstronautForm.js
:
import React from 'react';
import { Form, Button} from 'react-bootstrap'
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as astronautActions from '../actions/astronautActions';
import AstronautName from './form/AstronautName';
import AstronautEmail from './form/AstronautEmail';
import TerraformExperience from './form/TerraformExperience';
import TerraformPlanets from './form/TerraformPlanets';
class AstronautForm extends React.Component {
...
render() {
const {
id,
name,
email,
terraform_planets,
terraform_experience
} = this.props.astronaut;
return (
<Form key="astronaut-form" onSubmit={this.submitForm}>
<AstronautName
name={name}
onAttributeUpdate={this.updateAstronautAttributes}/>
<AstronautEmail
email={email}
onAttributeUpdate={this.updateAstronautAttributes}/>
<TerraformExperience
terraformExperience={terraform_experience}
onAttributeUpdate={this.updateAstronautAttributes}/>
<TerraformPlanets
terraformExperience={terraform_experience}
terraformPlanets={terraform_planets}
onAttributeUpdate={this.updateAstronautAttributes}/>
<Button type="submit">
Submit
</Button>
<Button onClick={this.clearForm}>
Clear
</Button
</Form>
)
}
}
function mapStateToProps(storeState, componentProps) {
const { astronaut } = storeState;
return { astronaut };
}
function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(astronautActions, dispatch) }
};
export default connect(mapStateToProps, mapDispatchToProps)(AstronautForm);
Our AstronautForm
component is the container component. It is connected to Redux and aware of state changes. It uses mapStateToProps
to pluck astronaut
out of state and make it available as part of the component’s props
. It contains (get it?) the child components that make up our form:
AstronautName
: the name field on our formAstronautEmail
: the email field on our formTerraformExperience
: the terraforming experience checkboxTerraformPlanets
: the terraformed planets text field
Managing State with Actions and Reducers
Our Redux architecture handles updates to the astronaut’s attributes in state: name, email, terraform experience and terraform planets.
When a user is done filling out a particular form field, we use the onBlur
event to dispatch an action that updates the corresponding attribute in state.
Let’s take a look at the AstronautName
component as an example:
client/src/components/form/AstronautName.js
:
import React from 'react';
class AstronautName extends React.Component {
state = {
name: ""
};
componentWillReceiveProps(nextProps) {
this.setState({name: nextProps.name});
};
onChange = (e) => {
this.setState({name: e.target.value});
};
onBlur = (e) => {
this.props.onAttributeUpdate(
{ name: this.state.name }
)
};
render() {
const { name } = this.state;
return (
<div>
<label>Name</label>
<input
type="text"
onBlur={this.onBlur}
onChange={this.onChange}
value={name}/>
</div>
)
}
};
export default AstronautName;
We passed in name
as a prop from the AstronautForm
parent component. We use componentWillReceiveProps
to put that in AstronautName
’s internal state.
We use the onChange
event to update AstronautName
’s state with the updated name. We use the onBlur
event to call the onAttributeUpdate
function.
This function is passed in as part of props
from AstronautForm
. AstronautForm
defines the function like this:
client/src/components/AstronautForm.js
:
...
updateAstronautAttributes = (newAttributes) => {
this.props.actions.updateAstronautAttributes(newAttributes)
};
We dispatch an action creator function updateAstronautAttributes
. Our action looks like this:
client/src/actions/astronautActions.js
:
export function updateAstronautAttributes(newAttributes) {
return {
type: "UPDATE_ASTRONAUT_ATTRIBUTES",
newAttributes
}
}
This action is handled by our astronautReducer
like this:
client/src/reducers/astronautReducer.js
:
import defaultState from '../store/initialStates/astronaut.js'
export default function astronautReducer(state=defaultState, action) {
switch(action.type) {
case "UPDATE_ASTRONAUT_ATTRIBUTES":
return {...state, ...action.newAttributes}
...
}
}
This creates a new version of our application’s central state, updating our components accordingly.
Submitting the Form
When a user clicks the “submit” button on our form, we fire the submitForm
function, defined in the AstronautForm
container component:
client/src/components/AstronautForm.js
:
...
submitForm = (e) => {
e.preventDefault();
this.props.actions.saveAstronaut(this.props.astronaut);
};
As described in the previous section, every time a user triggers the onBlur
event of a particular form field (name, email, terraforming experience, terraforming planets), we dispatch an action to update the corresponding attribute in the application’s state. Since the AstronautForm
component is connected to Redux via the connect
function, every time such a state change occurs, the component will re-render, and call mapStateToProps
. Thus ensuring that at any given point in time, when the user hits “submit” the astronaut in this.props.astronaut
is up-to-date with the latest changes.
So, our submitForm
function just needs to dispatch the saveAstronaut
action creator function with an argument of this.props.astronaut
.
Our saveAstronaut
action needs to send a web request to our API to submit the form. We know that we can’t just plop some async code into the middle of an action creator function without the help of middleware. So, we have some custom API middleware that will send the web request for us. If you’re unfamiliar with custom async middleware, I strongly recommend checking out the official Redux Middleware documentation, along with this excellent post written by my TuneCore teammate, Charlie Massry.
Our action looks like this:
client/src/actions/astronautActions.js
:
export function saveAstronaut(astronaut) {
return {
type: "API",
astronaut
};
}
And our middleware looks like this:
client/src/middleware/apiMiddleware.js
:
import {
saveAstronautSuccess,
saveAstronautFailure
} from '../actions/astronautActions';
const apiMiddleware = ({ dispatch, getState}) => next => action => {
if (action.type !== "API") {
return next(action)
}
fetch('/api/astronauts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
astronaut: action.astronaut
})
}).then((response) => {
return response.json();
}).catch((error) => {
dispatch(saveAstronautFailure(error));
}).then((data) => {
dispatch(saveAstronautSuccess(data));
});
};
export default apiMiddleware;
Our middleware gets called by the store before sending an action creator function’s return value along to the reducer. If the action has a type of "API"
, we will use fetch
to send our API request. Then, when the promise resolves, we will dispatch another action. For the purpose of this post, we won’t worry about our success and failure functions. Suffice it to say that the success
function updates state with the saved astronaut and the failure
function updates state with some error message.
Now that we understand the overall structure of our React + Redux app, we’re ready to tackle our form validations.
Form Validation
There are three categories of form validations we have to deal with for our app to work as expected.
- Required fields (like name and email)
- Custom validations that need to run when the form is submitted
- Custom validations that need to run when an attribute is updated in state
Let’s start with the low-hanging fruit: required fields.
Required Fields: Easy HTML5 Validations
Making a field required, and therefore preventing the user from submitting the form without it, is super easy to do with just HTML. We simply add required
to the input tag.
client/src/components/form/AstronautName.js
:
...
render() {
const { name } = this.state;
return (
<div>
<label>Name</label>
<input
required
type="text"
onBlur={this.onBlur}
onChange={this.onChange}
value={name}/>
</div>
)
}
Now, when a user clicks “submit” without filling out this field, we’ll see this behavior:
Blammo.
We can do the same for our email field for the same effect.
Validate on Submission
Let’s move on to some more complex form validations. If a user clicks the checkbox indicating that they do have experience terraforming other planets, we want to require them to fill out the “which planets have you terraformed?” text field.
We can’t validate for the presence of terraformed_planets
on the blur of the terraformed_experience
checkbox. That would cause the error to show up for the terraformed planets field right after they click the checkbox, before the user has a chance to interact with the terraform_planets
text field.
We can (and should) validate the terraform_planets
text field on the blur of that text field. But what if the user never clicks into that field at all? What if they check the terraform_experience
checkbox and then immediately click “submit”. We don’t want to actually submit the form to the API under those circumstances. We want to perform this validation before we send the web request.
Why We Shouldn’t Validate In the Component
We could handle this directly in the component by adding code to our submitForm
function in AstronautForm
:
Bad Example, Don’t Do This:
submitForm = (e) => {
e.preventDefault();
if (this.props.astronaut.terraform_experience && !this.props.astronaut_planets {
this.props.actions.saveAstronaut(this.props.astronaut);
} else {
this.setState({
errors:
...this.state.errors,
terraform_planets: true
}
}
}
};
This approach has a few drawbacks.
- It requires us to store
errors
in theAstronautForm
component’s state. While there isn’t anything inherently wrong with this, storing complex state within individual components is exactly what Redux allows us to avoid. - We are starting to add complex logic to our component. Currently, we’re only looking at just two attributes. But if we really want our component to handle this validation, this code will have to grow to validate every astronaut attribute. Not only is that messy, but it forces the form component’s submit function to explod its responsibilities. No longer can it simply submit a form, now it validates the astronaut object stored in props and decides whether it should submit the form or update the component’s internal state. Think of your form submission function like a younger sibling that you don’t entirely trust to do anything right and wouldn’t give a lot of responsibility to (no offense Zoe). Our form submission function should do exactly that––submit a form. It shouldn’t be responsible for validating the astronaut or updating state.
Let’s let Redux handle both validating the astronaut and tracking astronaut errors.
Tracking Errors in Redux’s State
When we first established our state, we established an object that looks like this:
client/src/store/initialStates/astronaut.js
:
{
astronaut: {
id: null,
name: "",
email: "",
terraform_experience: false,
terraform_planets: ""
}
}
Let’s expand the astronaut
key of state to include errors, tracking an error for each attribute that we want to validate:
{
astronaut: {
id: null,
name: "",
email: "",
terraform_experience: false,
terraform_planets: "",
errors: {
name: null,
email: null,
terraform_planets: null
}
}
}
Now that the astronaut
key in Redux’s state contains its own errors, we can rely on our astronautReducer
to update these errors appropriately. When will we tell our reducer to update the astronaut’s errors? Let’s return to our use-case: “validating on submit”.
Custom Validation Middleware
According to our earlier example, we know that we want to validate the presence of terraform_planets
when a user submit the form, if they have checked the terraform_experience
box.
We want to perform this validation after the user hits submit, not inside our component, and we want to do the validation before the API request gets sent. If the astronaut is not valid, we don’t want to send the API request. Instead, we’ll dispatch an action that will tell our reducer to update the appropriate error in state.
How on earth can we plug into the moment in time after the form is submitted and the saveAstronaut
action is dispatched, but before the API request is sent? Custom middleware of course!
We’ll define some custom validation middleware and we’ll add it to our middleware stack before the custom API middleware. That way it will get called before the API middleware gets called, i.e. before the API request is sent.
This diagram illustrates where in the Redux lifecycle our middleware fits in.
Defining the Middleware
We’ll define our form validation middleware:
client/src/middleware/formValidationMiddleware.js
:
const formValidationMiddleware = ({ dispatch, getState}) => next => action => {
// validations coming soon!
};
export default formValidationMiddleware;
Adding to the Middleware Stack
We’ll add it to the stack before our custom apiMiddleware
.
client/src/store/configureStore.js
:
import {
createStore,
applyMiddleware } from 'redux'
import rootReducer from '../reducers'
import initialState from './initialState';
import apiMiddleware from '../middleware/apiMiddleware';
import formValidationMiddleware from '../middleware/formValidationMiddleware';
export default function configureStore() {
return createStore(
rootReducer,
initialState,
applyMiddleware(
formValidationMiddleware,
apiMiddleware
)
)
}
Now we’re ready to code our validation middleware!
Performing the Validations
First things first. We only want to do this validation work if the action that was dispatched is the saveAstronaut
action. This is the action that will send the web request, courtesy of our apiMiddleware
. So, we’ll add an if
statement that checks for the "API"
action type. If the action does not have that type, we’ll return next(action)
so that the action will proceed to the reducer.
client/src/middleware/formValidationMiddleware.js
:
const formValidationMiddleware = ({ dispatch, getState}) => next => action => {
if (action.type !== "API") {
return next(action)
}
// validations coming soon!
};
export default formValidationMiddleware;
Okay, on to our validations. We’ll run the validations for every astronaut attribute that requires validation. By taking the validation logic out of the component, we are taking the responsibility of deciding whether to not to send the form submission API request out of the component too. We are allowing the component to dispatch the saveAstronaut
action, regardless of the presence of any errors. So, we always want to validate all attributes in this middleware.
client/src/middleware/formValidationMiddleware.js
:
import { astronautValidationError } from '../actions/astronautActions';
import astronautValidationErrors from '../utils/astronautValidationErrors';
import astronautIsValid from '../utils/astronautIsValid';
const formValidationMiddleware = ({ dispatch, getState}) => next => action => {
if (action.type != = "API") {
return next(action)
}
const { astronaut } = action;
let errors = astronautValidationErrors(astronaut)
if (!astronautIsValid(errors)) {
dispatch(astronautValidationError(errors))
} else {
next(action)
};
};
export default formValidationMiddleware;
Let’s break this down and take a look at some of the helper functions being called here.
First, we grab the astronaut from the action:
const { astronaut } = action;
Then, we build the errors object with the help of a function, astronautValidationErrors
.
let errors = astronautValidationErrors(astronaut)
Our goal is to generate an object that looks exactly like the errors
sub-key of state’s astronaut
key, with the values properly reflecting the presence of an error. We want to generate such an object so that we can send it along to the reducer which will use it to update the astronaut’s errors in the application’s state.
For example, the following errors object would indicate that there is an error with the name
attribute, but not the email
or terraform_planets
attributes.
{
name: true,
email: false,
terraform_planets: false
}
Let’s take a look at the astronautValidationErrors
function defined in client/src/utils/astronautValidationErrors.js
:
import { attributeValidators } from './attributeValidators';
export default function astronautValidationErrors(astronaut) {
Object.keys(attributeValidators).reduce((errors, validator) => {
errors[validator] = !attributeValidators[validator](astronaut)
}, {})
}
This function relies on an object we’ve imported from another utils/
file, attributeValidators
:
export const attributeValidators = {
name: nameValid,
email: emailValid,
terraform_planets: terraformPlanetValid
}
function nameValid(astronaut){
return astronaut.name && astronaut.name.length > 0;
}
function emailValid(astronaut) {
return astronaut.email && astronaut.email.split("@")[1] === "space.ex"
}
function terraformPlanetValid(astronaut) {
const { terraform_experience, terraform_planets } = astronaut;
if (terraform_experience) {
return terraform_planets &&
terraform_planets.length > 0 &&
!terraform_planets.toLocaleLowerCase().includes("mars");
} else {
return true
}
}
Here we have an object attributeValidators
, with keys corresponding to each of the astronaut attribute names and values pointing to our custom validation helper functions.
We use this object in our astronautValidationErrors
function to:
- Look up the validation function by the name of the attribute, call that function,
- Set that same key in the
errors
object we are building tofalse
if the validator returns true (indicating that there isn’t an error for this attribute) ortrue
if the validator returned false (indicating that there is an error for this attribute).
errors[validator] = !attributeValidators[validator](astronaut)
Super clean and dynamic.
Returning to our middleware, we’ve produced an object, errors
, that contains the keys of the attribute names and the values of true
to indicate an invalid attribute or false
to indicate no such error.
Now we need to implement some logic. If the errors
object contains any true values (i.e. if any of the attributes are invalid), we should not allow our action to proceed to the next middleware––the API middleware. We should instead dispatch a new action that will tell the reducer to update the astronaut’s errors in state.
// client/src/middleware/formValidationMiddleware.js
...
if (!astronautIsValid(errors)) {
dispatch(astronautValidationError(errors))
} else {
next(action)
}
Here we use another helper function, astronautIsValid
. If the astronaut is not valid, we will dispatch the astronautValidtionError
action. Otherwise, we will call next(action)
and let Redux proceed to pass our action to the API middleware.
Let’s take a look at our helper function, astronautIsValid
:
// client/src/utils/astronautIsValid.js
export default function astronautIsValid(errors) {
return !Object.values(errors).some(err => err)
}
It simply returns true if the errors
object has no keys with a value of true
(which indicates an invalid attribute) and false
if the errors
object contains any true
values.
Back in our middleware, if the errors
object does in fact contain true
values, we dispatch the astronautValidtionError
action with a payload of the errors
object we built.
Updating State
The astronautValidtionError
action looks like this:
// client/src/actions/astronautActions.js
...
export function astronautValidationError(errors) {
return {
type: "ASTRONAUT_VALIDATION_ERROR",
errors
}
}
And is handled by the astronautReducer
which uses the object contained in action.errors
to update the astronaut in state with the appropriate errors:
// client/client/src/reducers/astronautReducer.js
...
case "ASTRONAUT_VALIDATION_ERROR":
return {
...state,
errors: {
...state.errors,
...action.errors
}
}
Lastly, we’ll update each component to display an error message if the given attribute has an error.
Let’s look at the AstronautEmail
component as an example.
Notice that the container component, AstronautForm
now passes in the this.props.astronaut.errors.email
as a prop.
// client/client/client/src/components/AstronautForm.js
...
render() {
const { email, errors } = this.props.astronaut;
...
<AstronautEmail
email={email}
emailError={errors.email}
onAttributeUpdate={this.updateAstronautAttributes} />
...
}
And our AstronautEmail
component implements some display logic based on the presence of emailError
in props:
// client/client/src/components/form/AstronautEmail.js
...
render() {
...
{emailError &&
<div>please provide a valid SpaceEx email.</div>
}
}
We’ve successfully validated our form after the user clicked submit, taught Redux to manage errors in application state, prevented the web request from being sent to the API when the astronaut is not valid, and displayed errors in our components––all without adding complicated view logic or state management to our components! Good job us.
Validate on State Change
Now that we’ve looked at the scenario in which we want to preform validations when we submit the form, let’s discuss our last validation use-case. Some validations should occur as the user edits the form––updating the component to display certain errors as soon as the user finishes editing a particular form field.
Our email and “which planets have you terraformed?” fields are good examples of this desired behavior. As soon as a user focuses off of one of these form fields, we should display or remove the appropriate errors. In the case of email, we should show them an error message if they provided a non “@space.ex” email. In the case of terraformed planets, we should show them an error if (1) they clicked “terraforming experience” but left this field blank, or (2) they included “Mars” in their list of planets.
We can see this behavior below:
<iframe width=”640” height=”360” client/src=”https://www.youtube.com/embed/DrXLJI30IPo?rel=0&showinfo=0” frameborder=”0” allow=”autoplay; encrypted-media” allowfullscreen></iframe>
So, how do we hook into the point in time when we’re blurring away from a form field and updating the astronaut’s attributes in Redux’s state? We already have an action that gets dispatched onBlur
of each form field: updateAstronautAttributes
. This action sends the new attributes to the reducer where the astronaut is updated in state.
Let’s write custom middleware to intercept this action, validate the astronaut against its new attributes, and add errors to the action for the reducer to include in any state changes.
We’ll define our middleware and add it to the middleware stack:
client/src/middleware/validateAttributeUpdateMiddleware.js
:
const validateAttributeUpdateMiddleware = ({ dispatch, getState}) => next => action => {
// validations coming soon!
};
export default validateAttributeUpdateMiddleware;
// client/src/store/configureStore.js
import {
createStore,
applyMiddleware } from 'redux'
import rootReducer from '../reducers'
import initialState from './initialState';
import apiMiddleware from '../middleware/apiMiddleware';
import formValidationMiddleware from '../middleware/formValidationMiddleware';
import validateAttributeUpdateMiddleware from '../middleware/ValidateAttributeUpdateMiddleware';
export default function configureStore() {
return createStore(
rootReducer,
initialState,
applyMiddleware(
formValidationMiddleware,
validateAttributeUpdateMiddleware,
apiMiddleware
)
)
}
Now we’re ready to code our validations!
client/src/middleware/validateAttributeUpdateMiddleware.js
:
import astronautAttribueIsValid from '../utils/astronautAttributeIsValid'
const ValidateAttributeUpdateMiddleware = ({ dispatch, getState}) => next => action => {
if (action.type !== "UPDATE_ASTRONAUT_ATTRIBUTES") {
return next(action)
}
const { newAttributes } = action;
const { astronaut } = getState();
let updatedAstronaut = {...astronaut, ...newAttributes}
const attrName = Object.keys(newAttributes)[0]
action.errors = {
[attrName]: !astronautAttribueIsValid(updatedAstronaut, attrName)
}
next(action)
};
export default ValidateAttributeUpdateMiddleware;
Let’s break this down:
First, we grab our hash of new attributes from the action:
const { newAttributes } = action;
Then, we build a copy of the astronaut object that is currently in state, with the new attributes:
const { astronaut } = getState();
let updatedAstronaut = {...astronaut, ...newAttributes}
Next up, we need to grab the name of the attribute we are currently updating, so that we know which validation helper function to call:
const attrName = Object.keys(newAttributes)[0]
Lastly, we dynamically populate action.errors
with a key of the name of the attribute we are updating/validating and a true/false
value. We populate this value with the help of another helper function, astronautAttribueIsValid
. Let’s take a look at that function now:
client/src/utils/astronautAttribueIsValid.js
:
import { attributeValidators } from './attributeValidators';
export default function astronautAttributeIsValid(astronaut, attribute) {
if (attributeValidators[attribute]) {
return attributeValidators[attribute](astronaut);
} else {
return true;
}
}
This function takes in arguments of the astronaut object we are validating and the name of the attribute to validate.
Once again we utilize our attributeValidators
object and the helper functions it stores. We look up the validation function by its attribute name, if it exists, we call the function with an argument of our astronaut. This will return true
for a valid attribute and false
for an invalid one.
If our attempts to lookup a validation function in the attributeValidators
object returns undefined
, then this is an attribute that we don’t have a validator for. It doesn’t need to be validated and we should just return true
to indicate that the attribute is valid (by virtue of not requiring validation, it can’t be invalid).
So, in the case in which the astronaut’s newAttributes
look like this:
{email: "sophie@gmail.com"}
We set action.errors
to:
{
email: true
}
Thereby indicating that the email
attribute is invalid.
Updating State
Once we’ve built our errors object and attached it to action
, we return next(action)
. This will send our action to the reducer in the following state:
{
type: "UPDATE_ASTRONAUT_ATTRIBUTES",
newAttributes: {email: "sophie@email.com"},
errors: {email: true}
}
Lastly, we’ll teach our astronautReducer
to handle this action correctly by updating not just the astronaut’s top-level attributes, but also by updating the astronaut’s errors.
// client/src/reducers/astronautReducer.js
...
case "UPDATE_ASTRONAUT_ATTRIBUTES":
return {
...state,
...action.newAttributes,
errors: {
...state.errors,
...action.errors
}
}
...
This will cause the components to re-render with the appropriately updated astronaut
mapped into props
from state. Our components already contain logic to display any errors found in astronaut.errors
so our app should just work!
Conclusion
The code shared here represents just a handful of (contrived and simplified) example use-cases for custom validation middleware. The main take away here is not the particular validation functions for our fictitious astronaut form, but rather the manner in which we leveraged Redux to handle these validations. We avoided creating a bloated container component that was responsible for validations and making decisions about which actions to dispatch under which circumstances. Instead, we let Redux’s centralized state management system maintain error states and hooked into the dispatch of different actions to perform custom and complex validations. We kept our components clean, and we let Redux do what it does best.