Redux Store - Up and Running


Photo of my chubby face smiling to you

Piotr Staniów • Posted on 02 Apr 20207 mins

#JavaScript
#Redux

This article is a part of Learning by implementing series.

Redux is a state management library that over the last few years has conquered the hearts of JavaScript developers throughout the entire world and there are good reasons for that. In fact, its main principles are based on various concepts known to programmers for decades, as it combines ideas from state machines, Deterministic State Automata or event-driven programming, and as such there's nothing standing against using Redux also in other programming languages.

In this article, that may serve you both as an introductory and a refresher, we will dive deeper into inner workings of the library following the idea of learning by implementing.

Lights. Camera… Action!

One of the key concepts of Redux, borrowed from Event-Driven Development paradigm, is that the flow of an application can be directed by various events, such as user actions — mouse click, keyboard typing, messages coming from other applications or APIs, or even threads.

As you may know, the browser provides us a multitude of predefined events that are emitted as instances of Event class. The event (instance) always carries some information about what type of event has occurred in the application and usually also a little bit of metadata. If we skipped all the abstraction provided by browsers, we could say that essence of this event (instance) may, for instance, boil down to such an object:

const event = {
  type: 'click',
  payload: {
    mouseX: 734,
    mouseY: 219,
  },
};

This notion of passing simple objects as a representation of events in the application prevails in Redux, and in this context such objects are called actions. We will also say about an action that it is emitted or dispatched to denote that this object is passed from a component to Redux Store.

When an action should be emitted?

This is a matter of discussion in the community, and there are as many answers to that question, as there are programmers. On a high level, there are two main views on this matter. One is that we should only emit an action to inform other components about a change in the system, assuming that some information is local and not intended ever to be shared. Another view is that all events should be visible to entire system, in case we wanted to extend the application in the future in such way, that new components would listen to these changes.

In reality, most of applications I've been dealing with are somewhere in between. Sometimes they're sharing information that could've been stored locally. Sometimes they're building weird abstractions to bypass that locality of information. It may be somehow related to the organic growth of applications — it's good idea though to make up your mind about that upfront for each project.

For more concrete examples of when an action could be emitted:

  • When an user clicks or moves the mouse (or touches the screen)
  • When something is typed on the keyboard in a form
  • When data has been retrieved from API
  • When a chunk of data arrives from WebSocket, or a notification, or a message
  • When light intensity has changed around the device (see Ambient Light API)
  • When NFC tag arrives near the device (see NFC API)

Generally speaking, an action can be emitted when something occurs that your application should respond to in some way (or may need to use that information in the future).

"

It is important however to assimilate right from the start, that in this approach emitting an action should not serve as a way to trigger some behaviour of the application. An action should always represent a real-life event that we respond to, and dispatching an action is not a fancy equivalent of calling a function.

Good actions to heaven… and those bad too.

At this point, you may wonder where these actions are actually dispatched from? Sure, Piotr, it sounds wonderful but how do I actually implement that?

As a matter of fact, what we poetically call dispatching an action actually means that we call a dispatch method with the action passed as an argument to it. The function is a method of the titular Redux Store being a central object (singleton, in fact) of the Redux architecture.

A simplistic example of that may look as follows:

import React from 'react';
import { store } from 'src/store';

export const Form = () => {
  const handleSubmit = (formData) => {
    store.dispatch({ type: '@form/clicked-submit', formData });
  };
  return (
    <Button onClick={handleSubmit} />
  );
};

We may obviously and will put some abstractions over how we actually get the instance of Store in a component rather than importing it directly but for the sake of this article we will stick to that way.

"

Did you know? The code in a file (in JavaScript) is executed exactly once and so are all the exports. Because of that, if you export an object from a file it will be a singleton reused across all files importing it.

The aforementioned Store is a class that handles all the actions that are dispatched in the project. What is the purpose of these actions? Well, their main purpose is to drive a change of an application state.

A "god" object. What is an application state?

It is a single, global object that gathers all the data an application requires at any given moment, that may be a result of previous interactions with it. This object will impact the current UI displayed to the end user, requests to the external APIs, and general behaviour of the application.

On the other hand, it will also store all data that has been received over the course of time, in particular (but not exclusively) data that is reused across multiple components. It may keep track of various APIs responses, user input, browser's URL parameters, as well as devices' input (e.g. temperature sent periodically over Bluetooth protocol straight to your product).

Frequently Asked Question: what about performance / memory usage?

You may wonder if creating a possibly huge single object that contains all the data would pose any threat to your performance?

The answer is no, and actually it may even have better performance. The object simply consolidates all those pieces of information that you would have otherwise dispersed across various places in your project. In fact, data stored in state should be deduplicated and normalized, so memory-wise this approach may prove even better.

Working out the magic parts

What we have discussed thus far has probably built up in your memory as a class resembling this one:

class Store {
  constructor(state) {
    this._state = state; // It is deemed as private
  }

  dispatch(action) {
    // Some magic
  }
}

const initialState = {};
export const store = new Store(initialState);

The application state is assumed to be immutable, and cannot be changed directly by any party. We introduce a concept of store that is a singleton having ownership of the state object, which provides API to update the state.

This way allows us to encapsulate the state and easily track, what and when caused the state to update (and the application to break 😉). You can always simply provide a wrapper around Store's methods to provide a full record of changes to the state.

Store will also have ownership of a reducer, that is a function, that based on current state and an event that occurred in the application (action) will produce a new instance of state. We can represent that in a more fancy syntax:

reducer :: (state: StateType, action: ActionType) -> nextState: StateType

Having introduced all main concepts of Redux: actions, state and the reducer, we may now need to discuss some further implementation details and finally get our hands dirty with writing some actual code.

The reducer is a function that is responsible for building up the new "snapshot" of application state, based on the previous state and an action. For the most of the new state, it will merely have most of its data shallow-copied by assigning a reference, so the overhead on creating new object is negligible. However, it is an useful practice to create a new state, to prevent issues with tracking down "what caused that change" — we can even record the history of our application states and see how it evolved with various interactions. I assure you this is a killer-feature for debugging your applications.

Since the state is usually a large object, it would be bewildering to handle all of changes to it directly in a single function. To address that, we create other "helper functions" that we also call reducers. They will be delegated with the task of updating only parts of the name.

Convention is that these partial reducers are named after parts of state they are responsible for. A reducer that updates state.users may be called usersReducer and will be called with a slice of state — state.users — and an action dispatched. They can also be stored in an object, that maps the name of the slice of state (users) to the reducer function that is responsible for that slice. That is sometimes called the reducer map.

"

Frequently Raised Issues

  1. All reducers are called when an action is dispatched, they don't need to make any updates to the state if it's not needed. In such scenario, the reducer is simply expected to return the same reference to state, as it has received and the overhead is minuscule.

  2. There is no option for these reducers to conflict with each other, as they are called synchronously one after another, and each is responsible for its exclusive subtree of the state.

  3. Asynchronous events are handled asynchronously despite the fact that Redux Store is synchronous. This is due to fact that the callback to your asynchronous event is synchronous (clicking is asynchronous, function handling the click is not).

Having all this knowledge you are now well-equipped with all necessary bits of knowledge to watch me implementing the Redux Store. The code snippet below is broken down into a few slides with my commentary.

Loading...

Can you hear me now?

Having gone through that implementation, we now have a working Redux Store that allows us to handle updates of the application state using reducers which respond to dispatched actions. The missing bit here is a publisher-subscriber pattern that would allow any component to listen to the changes of the state and respond accordingly.

In terms of React, we will be interested in updating component's state in response to the Redux state changes, so that we can rerender the component whenever an interesting part of the state changes. We'd like to be able to pass a function to the store, and rest assured that it will be called whenever the state changes — store.subscribe((nextState) => console.log(nextState));

Loading...

Redux Store has been implemented

As you can see, implementing Redux Store is relatively simple and can be done within around 40–50 lines of code. Beyond that the original redux library offers you a few helpers (e.g. createStore, combineReducers) and middleware API. The key concept however, is what you have already seen above.

Learning more

Stay tuned for next articles coming in the "Learning by Implementing" series, we'll have a look at how the store above can be connected to React, and try to implement react-redux library functional equivalent.

You can also watch my lecture at Code::Dive conference where I was explaining how Redux architecture works and also performed the live coding implementing Redux Store:

Loading...

Stay connected

Stay up to date with recent articles — subscribe to the newsletter.
Unsubscribe at any time with a link in the email.

© Piotr Staniów's Blog 2023

Opinions are my own and do not represent the views of my current or past employers.