State management in React apps using Redux

React is currently the most used frontend JavaScript framework. Its component-based approach has made it easier to implement views. But React can’t help us with managing the data of the application. Sure we can use its state variables to store our data, but when the data is complex and used by multiple components, the code to fetch, update, create becomes very messy. It would be a lot easier to have all the data and updates on it in one place. We can do so by using redux.

Data flow in redux application
Schematic Depiction

Redux is a state management library. We can store all our application data on the redux store and subscribe to changes on it. That way, whenever any operation updates the store, all the components using that data will re render, and we will see the changes instantly, no matter how deep the component is in the Component Tree. 

The store is a single JavaScript object, and the fields on this object are different states that we want to store. Each field has a separate reducera reducer is a function that accepts the current state of the field and the action to be performed on that state as arguments. That action determines how to update that particular field. Here is a sample reducer for an array field:

const peopleReducer = (state = [], action) => {
	switch (action.type) {
		case 'INIT_PEOPLE':
			// return state
		case 'ADD_PERSON':
			// return state
		case 'DELETE_PERSON':
			// return state
		case 'UPDATE_PERSON':
			// return state
		default:
			// return state
	}
}

export default peopleReducer;

Actions are generally defined as functions that return a type of operation and often a payload, for example:


export const initiatePeople = (data) => {
	return {
		type: 'INIT_PEOPLE',
		payload: data
	}
}

Actions are performed on the store using a dispatch function. All this seems a little too much for normal operations; I agree, to use the store, we need to write a lot of boilerplate code, but once done, it makes working with the data a whole lot easier, and also it should only be used for complex application data.

Example Application

Let’s create an example app to understand the concept better; 

Using React states

Here is the code for the people-app to fetch all the people from a rest endpoint;

First we fetch the data in useEffect and store it on the people state variable.

const App = () => {
  const [people, setPeople] = useState([]);
  useEffect(() => {
    peopleService
      .getAll()
      .then(data => setPeople(data))
      .catch(err => console.error(err));
  }, []);

  return (
    // code
  )
}

Here is the code for peopleService.getAll():

import axios from "axios";

const baseUrl = '/people';

const getAll = () => {
	return axios
		.get(`${baseUrl}`)
		.then(response => response.data);
}

Now we can use the people state to render the data on the UI. Not too complicated. Next if we add functionality to add, update and delete person records.

 const handleDelete = (person) => {
    peopleService.deletePerson(person.id)
      .then(() => setPeople(people.filter(p => p.id !== person.id)))
      .catch(err => console.error(err));
  }
  
  const handleAdd = (newPerson) => {
    peopleService.create(newPerson)
      .then((data) => setPeople([...people, data]))
      .catch(err => console.error(err));
  }
  
  const handleUpdate = (person) => {
    peopleService.update(person.id, person)
      .then((data) => 
           setPeople(people.map(p => p.id === person.id ? data : p))
      )
      .catch(err => console.error(err));
  }

We have to call the service to perform the operation and then update the state accordingly.

The respective peopleService code:

const update = (id, updatedData) => {
	return axios
		.put(`${baseUrl}/${id}`, updatedData)
		.then(response => response.data);
}

const create = (newData) => {
	return axios
		.post(`${baseUrl}`, newData)
		.then(response => response.data);
}

const deletePerson = (id) => {
	return axios
		.delete(`${baseUrl}/${id}`);
}
Using Redux

Now we do the same thing using redux.

Install redux and react-redux packages, 

npm i react-redux redux

Now we create a redux store in store.js, but first we need a reducer;

const peopleReducer = (state = [], action) => {
	switch (action.type) {
		case 'INIT_PEOPLE':
			return action.payload;
		case 'ADD_PERSON':
			return state.concat(action.payload);
		case 'DELETE_PERSON':
			return state.filter(
                              p => p.id !== action.payload.id);
		case 'UPDATE_PERSON':
			return state.map(p => 
p.id === action.payload.id ? action.payload : p);
		default:
			return state;
	}
}

export default peopleReducer;

Now we use this reducer to create our store:

import { createStore } from 'redux';
import peopleReducer from './reducer/people';

const store = createStore(peopleReducer);

export default store;

If we have to store only one state, we can directly pass our reducer. If we had multiple states we would do it like this:

import { combineReducers, createStore } from 'redux';
import peopleReducer from './reducer/people';

const reducer = combineReducers({
	people: peopleReducer,
	//another_state: its_reducer
})
const store = createStore(reducer);

export default store;

Redux team has created an extension which helps in debugging and inspecting the store on the browser. To use it we have to install a package:

npm i react-devtools-extension –save-dev

Also on your browser extensions store, search for Redux-DevTools and install it.

Then we update our createStore function call:

const store = createStore(
	reducer,
	composeWithDevTools()
);

Now we define the actions for the peopleReducer:

export const initiatePeople = (payload) => {
  return {
		type: 'INIT_PEOPLE',
	  payload
  }
}

export const addPerson = (payload) => {
	return {
		type: 'ADD_PERSON',
		payload
	}
}

export const deletePerson = (payload) => {
	return {
		type: 'DELETE_PERSON',
		payload
	}
}

export const updatePerson = (payload) => {
	return {
		type: 'UPDATE_PERSON',
		payload
	}
}
Adding redux-thunk

We can also delegate the Axios calls to actions using the redux-thunk library. This library makes it possible to return an asynchronous function from our actions. In this async function, we can make Axios calls. That way, our react components don’t have to worry about synchronizing the application state with our database. Now the actions become:

export const initiatePeople = () => {
	return async dispatch => {
		try {
			const data = await peopleService.getAll();
			dispatch({ 
                          type: 'INIT_PEOPLE', 
                          payload: data 
                        });
		} catch (e) {
			console.error(e);
		}
	}
}
export const addPerson = (data) => {
	return async dispatch => {
		try {
			const payload = await peopleService
                                              .create(data);
			dispatch({ type: 'ADD_PERSON', payload });
		} catch (e) {
			console.error(e);
		}
	}
}
export const deletePerson = (payload) => {
	return async dispatch => {
		try {
			await peopleService.deletePerson(payload.id);
			dispatch({ type: 'DELETE_PERSON', payload });
		} catch (e) {
			console.error(e);
		}
	}
}
export const updatePerson = (data) => {
	return async dispatch => {
		try {
			const payload = await peopleService
                                              .update(data);
			dispatch({ type: 'UPDATE_PERSON', payload });
		} catch (e) {
			console.error(e);
		}
	}
}

These actions return an async function which accepts the dispatch function as an argument thanks to redux-thunk. Here first, we make a request to the backend, receive the response and call the dispatch function with the response as payload. 

‘redux-thunk’ is a so-called redux middleware. So to use it, we have to call the applyMiddleware function with a thunk as an argument and pass it to the createStore function like this:

const store = createStore(
	reducer,
	composeWithDevTools(
		applyMiddleware(thunk)
	)
);

thunk is exported by the redux-thunk library.

All actions are exported so they can be used wherever needed. 

Before we can use the store in our application, we have to wrap our App component, i.e., the root component with the Provider component exported by react-redux, and pass it to our store as a prop.

import { Provider } from 'react-redux';
import store from './store';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Now the App and its children component can access the redux store using the Redux’s hooks API.

Redux Hooks

The React-Redux library provides two hooks that we can use in our React components. They are useSelector and useDispatch.

useSelector accepts a selector function, this function will be passed the current state of the store and we can return what state we want access to, in our case we need the people state so the code will be:

import { useDispatch, useSelector } from 'react-redux';

const App = () => {

  const people = useSelector(state => state.people);

// We can use this variable in our components.

useDispatch hook returns a dispatch function to which we can pass the return function or value of our desired actions. For example to initialize our store we can call dispatch on the useEffect hook with the initializePeople action, like this:

import { addPerson, deletePerson, initiatePeople, updatePerson } from './reducer/people';
import { useDispatch, useSelector } from 'react-redux';

const App = () => {

  const people = useSelector(state => state.people);
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(initiatePeople());
  }, []);

Now using the dispatch and actions we can update the rest of our code to:

  const handleDelete = (person) => {
    dispatch(deletePerson(person));
  };

  const handleUpdate = (person) => {
    dispatch(updatePerson(person));
  };

  const handleAdd = (newPerson) => {
    dispatch(addPerson(newPerson));
  };

Pretty neat, right?

Conclusion

Using redux we can make our code more clean and sophisticated. And with the help of useSelector hook we can use our state data anywhere in our application, which helps in avoiding prop drilling, and with useDispatch we can easily update our store anywhere. 

We should use react state variables only to manage the views of the components and all our application data should be stored in a store and the code to update it, defined in a single place.