An Introduction to Redux's Core Concepts

Dustin Byers

Redux is a predictable state container for JavaScript apps, and a very valuable tool for organizing application state. It’s a popular library to manage state in React apps, but it can be used just as well with Angular, Vue.js or just plain old vanilla JavaScript.

One thing most people find difficult about Redux is knowing when to use it. The bigger and more complex your app gets, the more likely it’s going to be that you’d benefit from using Redux. If you’re starting to work on an app and you anticipate that it’ll grow substantially, it can be a good idea to start out with Redux right off the bat so that as your app changes and scales you can easily implement those changes without refactoring a lot of your existing code.

In this brief introduction to Redux, we’ll go over the main concepts: reducers, actions, action creators and store. It can seem like a complex topic at first glance, but the core concepts are actually pretty straightforward.

What’s a Reducer?

A reducer is a pure function that takes the previous state and an action as arguments and returns a new state. Actions are an object with a type and an optional payload:

function myReducer(previousState, action) => {
  // use the action type and payload to create a new state based on
  // the previous state.
  return newState;
}

Reducers specify how the application’s state changes in response to actions that are dispatched to the store.

Since reducers are pure functions, we don’t mutate the arguments given to it, perform API calls, routing transitions or call non-pure functions like Math.random() or Date.now().

If your app has multiple pieces of state, then you can have multiple reducers. For example, each major feature inside your app can have its own reducer. Reducers are concerned only with the value of the state.

What’s an Action?

Actions are plain JavaScript objects that represent payloads of information that send data from your application to your store. Actions have a type and an optional payload.

Most changes in an application that uses Redux start off with an event that is triggered by a user either directly or indirectly. Events such as clicking on a button, selecting an item from a dropdown menu, hovering on a particular element or an AJAX request that just returned some data. Even the initial loading of a page can be an occasion to dispatch an action. Actions are often dispatched using an action creator.

What’s an Action Creator?

In Redux, an action creator is a function that returns an action object. Action creators can seem like a superfluous step, but they make things more portable and easy to test. The action object returned from an action creator is sent to all of the different reducers in the app.

Depending on what the action is, reducers can choose to return a new version of their piece of state. The newly returned piece of state then gets piped into the application state, which then gets piped back into our React app, which then causes all of our components to re-render.

So lets say a user clicks on a button, we then call an action creator which is a function that returns an action. That action has a type that describes the type of action that was just triggered.

Here’s an example action creator:

export function addTodo({ task }) {
  return {
    type: 'ADD_TODO',
    payload: {
      task,
      completed: false
    },
  }
}

// example returned value:
// {
//   type: 'ADD_TODO',
//   todo: { task: '🛒 get some milk', completed: false },
// }

And here’s a simple reducer that deals with the action of type ADD_TODO:

export default function(state = initialState, action) {
  switch (action.type) {
    case 'ADD_TODO':
      const newState = [...state, action.payload];
      return newState;

    // Deal with more cases like 'TOGGLE_TODO', 'DELETE_TODO',...

    default:
      return state;
  }
}

All the reducers processed the action. Reducers that are not interested in this specific action type just return the same state, and reducers that are interested return a new state. Now all of the components are notified of the changes to the state. Once notified, all of the components will re render with new props:

{
  currentTask: { task: '🛒 get some milk', completed: false },
  todos: [
    { task: '🛒 get some milk', completed: false },
    { task: '🎷 Practice saxophone', completed: true }
  ],
}

Combining Reducers

Redux gives us a function called combineReducers that performs to tasks:

  • It generates a function that calls our reducers with the slice of state selected according to their key.
  • It then it combines the results into a single object once again.

What is the Store?

We keep mentioning the elusive store but we have yet to talk about what the store actually is.

In Redux, the store refers to the object that brings actions (that represent what happened) and reducers (that update the state according to those actions) together. There is only a single store in a Redux application.

The store has several duties:

  • Allow access to state via getState().
  • Allow state to be updated via dispatch(action).
  • Holds the whole application state.
  • Registers listeners using subscribe(listener).
  • Unregisters listeners via the function returned by subscribe(listener).

Basically all we need in order to create a store are reducers. We mentionned combineReducers to combine several reducers into one. Now, to create a store, we will import combineReducers and pass it to createStore:

import { createStore } from 'redux';
import todoReducer from './reducers';

const store = createStore(todoReducer);

Then, we dispatch actions in our app using the store’s dispatch method like so:

store.dispatch(addTodo({ task: '📖 Read about Redux'}));
store.dispatch(addTodo({ task: '🤔 Think about meaning of life' }));
// ...

Data Flow in Redux

One of the many benefits of Redux is that all data in an application follows the same lifecycle pattern. The logic of your app is more predictable and easier to understand, because Redux architecture follows a strict unidirectional data flow.

The 4 Main Steps of the Data Lifecycle in Redux

  • An event inside your app triggers a call to store.dispatch(actionCreator(payload)).
  • The Redux store calls the root reducer with the current state and the action.
  • The root reducer combines the output of multiple reducers into a single state tree.
export default const currentTask(state = {}, action){
  // deal with this piece of state
  return newState;
};

export default const todos(state = [], action){
  // deal with this piece of state
  return newState;
};

export default const todoApp = combineReducers({
  todos,
  currentTask,
});

When an action is emitted todoApp will call both reducers and combine both sets of results into a single state tree:

return {
  todos: nextTodos,
  currentTask: nextCurrentTask,
};
  • The Redux store saves the complete state tree returned by the root reducer. The new state tree is now the nextState of your app.

Conclusion

That was a lot to go over in very little words, so don’t feel intimidated if you’re still not entirely sure how all the pieces fit together. Redux offers a very powerful pattern for managing application state, so it’s only natural that it takes a little practice to get used to the concepts.

To learn more check out these resources:

🎩 In future posts we'll explore more advanced topics like dealing with asynchronous events using Redux-Saga or Redux Thunk.

  Tweet It
✖ Clear

🕵 Search Results

🔎 Searching...