Redux Store - Up and Running
Piotr Staniów • Posted on 02 Apr 2020 • 7 mins
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
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.
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.
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.
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));
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: