Handling forms with redux

In the previous tutorial we controlled the UserForm component, which had its own state, with all input changes mapped into it. To make this form work with a Redux-friendly approach we need to create a reducer and actions for it. There’s also the awesome Redux-Form library which gives you a lot of functionality ‘out-of-the-box,’ but it is probably part of a different story. :)

Let’s start with actions. We define actions to load form data from localStorage, to change Form data if one of the inputs has changed, and to clear Form data, because we will store successful search requests from here on out, so a method for clearing will be required.

import StorageService from '../StorageService' const loadForm = (data) => ({

type: 'FORM_LOAD',

data,

}) const clearForm = () => ({

type: 'FORM_CLEAR',

}) export const loadFormAction = () => (dispatch) => {

//load form data from local storage

const data = StorageService.getSearchData() || {}

//dispatch an action

dispatch(loadForm(data))

} export const clearFormAction = () => (dispatch) => {

StorageService.removeSearchData()

dispatch(clearForm())

}

The reducer for Form will look pretty simple — just loadForm to load form data from the user input or localStorage, and clearForm to clear data.

const formReducer = (state = {}, action) => {

switch (action.type) {

case 'FORM_LOAD':

return {

...state,

...action.data,

}

case 'FORM_CLEAR':

return {}

default:

return state

}

} export default formReducer

You can see how the UserForm component is rewritten and how handlers and props are passed there from the UserContainer component.

Wrapping up — Redux main concepts in our app

To install all the discussed dependencies, just run:

npm i -s redux react-redux redux-thunk

npm i -s react-router-redux@next //if you want to use redux-router

Or use can use the package.json file from the repository.

To use all our reducers with the global state, we will use the combineReducers function from Redux. It allows us to combine pieces of state from each reducer with the appropriate alias. See the index.js file in /reducers.

const reducer = combineReducers(

{

form: formReducer,

todos: todosReducer,

users: usersReducer,

routerReducer

}

) export default reducer

After this our state object will look like:

state = {

form: {}, //form reducer part

users: {}, //users reducer part

todos: {}, //todos reducer part

routerReducer: {}, //react-router-redux part

}

Now let’s move onto main index.js file:

const history = createHistory()

const middleware = [thunk, routerMiddleware(history)] const store = createStore(

reducer,

applyMiddleware(...middleware)

); ReactDOM.render(

<Provider store={store}>

<ConnectedRouter history={history}>

<App />

</ConnectedRouter>

</Provider>,

document.getElementById('root'));

registerServiceWorker();

The first two lines create all middlewares needed — thunk for middlewares, and async actions and routerMiddleware to handle history and routing.

After that, we just init the store with our combined reducer mentioned before, and wrap our application in Provider with store passed into it.

Let’s see how the Redux container works using the UsersListContainer as an example:

import React from 'react'

import UserList from '../components/UserList'

import UserForm from '../forms/UserForm'

import { connect } from 'react-redux'

import { getUsers } from '../actions/usersActions'

import { loadForm, loadFormAction, clearFormAction } from '../actions/formActions' class UserListContainer extends React.Component { componentDidMount() {

this.props.loadForm();

this.props.search();

} render() {

//add loading and failure state

if (this.props.isLoading) {

return <span>Loading...</span>

} if (this.props.isFailure) {

return <span>Error loading users!</span>

} return <div className="user">

<UserForm data={this.props.form}

submitHandler={this.props.search}

changeHandler={this.props.changeForm}

clearHandler={this.props.clearForm} />

{this.props.users &&

<UserList users={this.props.users} />}

</div>;

}

} const mapStateToProps = ({ users, form }) => {

return {

...users,

form,

}

} const mapDispatchToProps = (dispatch) => {

return {

search: () => { dispatch(getUsers()) },

loadForm: () => { dispatch(loadFormAction()) },

changeForm: (params) => { dispatch(loadForm(params)) },

clearForm: (params) => {

dispatch(clearFormAction())

dispatch(getUsers())

},

}

} export default connect(mapStateToProps, mapDispatchToProps)(UserListContainer);

In this container we map ‘users’ and ‘form’ sections of our state to the container’s props. We also map all actions we need via mapDispatchToProps function. When the component mounts we load form data from storage, and perform a user search with the current parameters. We also pass some of the actions to UserForm as handlers.

The Users Reducer manages the user-related section of our state and handles Request/Receive/Failure action types:

const initialState = {

isLoading: false,

isFailure: false,

users: null,

} const usersReducer = (state = initialState, action) => {

switch (action.type) {

case 'USERS_REQUEST':

return {

...state,

isLoading: true,

isFailure: false,

}

case 'USERS_RECEIVE':

return {

...state,

isLoading: false,

isFailure: false,

users: action.data,

}

case 'USERS_FAILURE':

return {

...state,

isLoading: false,

isFailure: true,

}

default:

return state

}

} export default usersReducer

The User Actions file contains one middleware function which dispatches all those actions, performing an async API call and storing form data upon successful requests. Note that in this tutorial we use getState functionality in the middleware function. This function provides you with access to your current state, so you can grab, for example, form data or other bits of state without having to pass them to the action.

import ApiService from '../ApiService'

import StorageService from '../StorageService' const requestUsers = () => ({

type: 'USERS_REQUEST',

}) const receiveUsers = (data) => ({

type: 'USERS_RECEIVE',

data,

}) const failureUsers = () => ({

type: 'USERS_FAILURE',

}) export const getUsers = (params) => async (dispatch, getState) => {

try {

dispatch(requestUsers())

const params = getState().form

const data = await ApiService.getUsers(params)

if (Object.keys(params).length && data) {

//save successful request

StorageService.setSearchData(params)

}

dispatch(receiveUsers(data))

} catch (e) {

dispatch(failureUsers())

}

}

As a result, we now have our SPA, with state being fully managed by Redux. We have two ‘smart’ containers (TodosList and UsersList) and we left presentational components unchanged (aside from a bit of refactoring in UserForm component, which is not only for presentational purposes). Form data is now saved between route changes: for example, we can search for a user, look at todos, then go back to the list and it will still be filtered! Feel free to clone the repo and test the code :)

In my next stories, we will talk about handling authentication in GraphQL as well is in React-Redux, and about unit testing React-Redux apps.