Can Redux be Used on the Server?
This article was originally written for Bit.
Why would I need Redux?
Redux bills itself on its landing page as “a predictable state container for JavaScript apps”. Redux is commonly described as a state management tool, and while it’s mostly used with React, it can be used anywhere JavaScript is used.
We’ve said Redux is for managing state. But what is this “state”, exactly? State is difficult to define, but let’s try to describe it. When we talk about state with respect to humans or things, we are trying to describe their status at this moment in time, possibly in terms of one or more parameters. For instance, when we say, “The lake is boiling hot”, or “The lake is frozen”, we’re describing the lake’s state in terms of the temperature of the water.
When you say, “I’m broke”, you’re describing your state in terms of how much money you have. Of course, in each of these examples, we’re considering only one aspect of the state of these objects. You could add more parameters, for instance, by saying, “I’m broke and I haven’t eaten, but I’m happy!” It’s key to note that state is transient. That means it changes, so when we request for the state, we understand that it may no longer be correct in a few seconds or minutes from now.
When it comes to applications, “state” takes on a few nuances. For one, it is represented by data stored somewhere. This is usually in memory (such as JavaScript objects) but could also be in files, a database or a cache mechanism like Redis. Secondly, the state of the application is usually instance-specific. So when we talk about an application’s state, we are referring to a specific instance, process, or user. An application’s state could include things like:
- is the user signed in or not? if yes, how long has the user been signed in and when is their session going to expire?
- (for a game) what is this user’s score?
- (for a media player) what timestamp did the user pause this video at?
On a lower level, the state could also include:
- what variables are set within the current environment (“environment variables”)?
- what files are currently in use by the program?
By looking at a snapshot of the application state at any point in time, we can get an idea of the conditions under which the application was running and possibly recreate them.
State can be modified by the user’s actions. For instance, making a correct move can increase the user’s score. In more complex apps, the state can be modified from multiple sources. For instance, in a multiplayer game, the user’s score could also be increased by an action his teammate takes; receiving a hit from the computer-controlled player could lead to a decrease in the score.
Let’s imagine we’re building a frontend app like the Twitter PWA (mobile.twitter.com). This app is a single page application with tabs for Home, Search, Notifications, and Messages. Each tab is its own view with its data flowing in and out. All of these comprise the state. New tweets, notifications, and messages are being broadcast into your browser window every couple of seconds. You can interact with them, too — like a tweet, retweet a tweet, delete a tweet, read a notification send a message to someone, and so on. All of these actions modify the state.
Now, the state can be modified from any one of multiple sources happening near-simultaneously. If you’re managing the state manually, you might find that it becomes difficult to keep track of what is going on. This leads to inconsistencies: a tweet might have been deleted, but it still shows up in the timeline. Or a notification or message might be read but still show up as unread. A user might like a tweet, so the heart icon is toggled immediately, but the actual network call failed, so now the UI is incorrect. Things like these are why you might need Redux.
How does Redux work?
Redux has three major concepts that aim to make state management straightforward.
- The store. The Redux store is a JavaScript object that represents the application state. It serves as the “single source of truth”. This means the entire application must rely on the store as the sole authority on the application’s state.
- Actions. The state is read-only, so it cannot be modified directly. Actions are the only way to update the state. Any component which wishes to make a change to the state must dispatch the appropriate action,
- Reducers. A reducer is a pure function that describes how the state is modified by actions. A reducer takes in the current state and the action requested, and returns the updated state.
Working with these three concepts means that the application no longer has to listen directly for all inputs (user interactions, API responses, pushed data from WebSockets) and figure out how they change the state. With the Redux model, these inputs can trigger actions that will update the state. The necessary application components can simply subscribe to the state and listen for the changes that concern them. With this, Redux aims to make state changes predictable. Sweet, eh?
Here’s a simple way we could use Redux in our fictional example:
import { createStore } from 'redux';
// our reducer
const tweets = (state = {tweets: []}, action) => {
switch (action.type) {
// we'll handle only one action: when new tweets arrive
case 'SHOW_NEW_TWEETS':
state.numberOfNewTweets = action.count;
return state.tweets.concat([action.tweets]);
default:
return state;
}
};
// a helper function to create the SHOW_NEW_TWEETS action
const newTweetsAction = (tweets) => {
return {
type: 'SHOW_NEW_TWEETS',
tweets: tweets,
count: tweets.length
};
};
const store = createStore(tweets);
twitterApi.fetchTweets()
.then(response => {
// instead of manually adding the new tweets to the view,
// we dispatch the Redux action
store.dispatch(newTweetsAction(response.data));
});
// we also use the SHOW_NEW_TWEETS action when the user tweets
// so the user's tweet is added to the state too
const postTweet = (text) => {
twitterApi.postTweet(text)
.then(response => {
store.dispatch(newTweetsAction([response.data]));
});
};
// supposing we have new tweets being pushed to the frontend via WebSockets,
// we can also dispatch the SHOW_NEW_TWEETS action
socket.on('newTweets', (tweets) => {
store.dispatch(newTweetsAction(tweets));
};
// If we're using a framework like React, our components should be connected to our store,
// and should auto-update to show the tweets
// otherwise we can manually listen for state changes
store.subscribe(() => {
const { tweets } = store.getSTate();
render(tweets);
});
From this starting point, we can add more actions and dispatch them from various points in the app without running crazy, thanks to Redux. You can read more about the Redux concepts here:
Translating these principles to the server
We’ve explored how Redux works on the client-side. Since Redux is “just JavaScript”, it could theoretically be used on the server. Let’s see what it would be like to apply these concepts to the server.
Remember when we talked about state? Well, things are somewhat different on the frontend. The frontend aims to be stateful (that is, keep track of the state between requests). If the frontend wasn’t stateful, you would have to log in every time you navigated to a new page.
The backend, however, aims to be stateless. (Note: when we say “backend”, we’re primarily referring to API-based environments that are separate from the frontend app.) This means that the state must be provided on every new invocation. For instance, the API does not keep track of whether you are logged in or not. It determines your authentication state by reading the token in your API request.
This brings us to the major reason why you wouldn’t use Redux as-is on the server: it was designed to hold transient state. But the application state stored on the backend is usually meant to be long-lived. If you used a Redux store on your Node.js server, the state would be cleared every time the node
process stops. On a PHP server, the state would be cleared on every request.
It becomes even more involved when you consider scaling. If you were to scale your application horizontally by adding more servers, you’d have multiple Node processes running concurrently, and each would have their own version of the state. This means that two identical requests to your backend at the same moment could easily get two different responses.
How then can we apply these principles to the backend? Let’s take a look at the concepts and how they are often applied:
- The store. On the backend, the single source of truth is often the database. Sometimes, in order to make it easier to get to frequently-accessed data, or for some other reason, this store might be duplicated in parts to other locations like a cache or a file. These usually tend to be read-only copies and listen to changes to the main store in order to stay updated.
- Actions and reducers: These are the only ways to change the state. In most backend applications, code is written in an imperative manner, which doesn’t lend itself well to the concepts of actions and reducers.
Let’s look at two design patterns that are similar in nature to what Redux tries to do: CQRS and event sourcing. They actually predate Redux and can be very complicated, so we’ll only take a cursory look.
CQRS and Event Sourcing
CQRS stands for “Command-Query-Responsibility-Segregation”. CQRS is a pattern where the application reads from the store and writes to it using two different models, the Query and the Command respectively.
In CQRS, the only way to change the state is to dispatch a command. Commands are akin to actions in Redux. In Redux, you’d do:
const action = { type: 'CREATE_NEW_USER', payload: ... };
store.dispatch(action);
// implement the appropriate reducer for the action
const createUser = (state = {}, action) => {
//
};
In a CQRS system, this could be:
// the base command class or interface
class Command {
handle() {
}
}
class CreateUserCommand extends Command {
constructor(user) {
super();
this.user = user;
}
handle() {
// create the user in the db
}
}
const createUser = new CreateUserCommand(user);
// dispatch the command - this calls the handle() method
dispatch(createUser);
// or you could use a CommandHandler class
commandHandler.handle(createUser);
Queries are how you read the state in CQRS. They’re the equivalent of store.getState()
. In a simple implementation, your query would talk directly to your database and retrieve records from there.
Event sourcing is designed to capture all changes to an application state as a sequence of events. Event sourcing is best suited for applications that need to know not just the current state, but its history — how it was attained. Examples are bank accounts, package tracking, e-commerce orders and transportation and logistics.
A simplified example of this:
// without event sourcing
function transferMoneyBetweenAccounts(amount, fromAccount, toAccount) {
BankAccount.where({ id: fromAccount.id })
.decrement({ amount });
BankAccount.where({ id: toAccount.id })
.increment({ amount });
}
function makeOnlinePayment(account, amount) {
BankAccount.where({ id: account.id })
.decrement({ amount });
}
// with event sourcing
function transferMoneyBetweenAccounts(amount, fromAccount, toAccount) {
dispatchEvent(new TransferFrom(fromAccount, amount, toAccount));
dispatchEvent(new TransferTo(toAccount, amount, fromAccount));
}
function makeOnlinePayment(account, amount) {
dispatchEvent(new OnlinePaymentFrom(account, amount));
}
class TransferFrom extends Event {
constructor(account, amount, toAccount) {
this.account = account;
this.amount = amount;
this.toAccount = toAccount;
}
handle() {
// store new OutwardTransfer event in database
OutwardTransfer.create({ from: this.account, to: this.toAccount, amount: this.amount, date: Date.now() });
// also update the current state of the account
BankAccount.where({ id: this.account.id })
.decrement({ amount: this.amount });
}
}
class TransferTo extends Event {
constructor(account, amount, fromAccount) {
this.account = account;
this.amount = amount;
this.fromAccount = fromAccount;
}
handle() {
// store new InwardTransfer event in database
InwardTransfer.create({ from: this.fromAccount, to: this.account, amount: this.amount, date: Date.now() });
// also update the current state of the account
BankAccount.where({ id: this.account.id })
.increment({ amount: this.amount });
}
}
class OnlinePaymentFrom extends Event {
constructor(account, amount) {
this.account = account;
this.amount = amount;
}
handle() {
// store new OnlinePayment event in database
OnlinePayment.create({ from: this.account, amount: this.amount, date: Date.now() });
// also update the current state of the account
BankAccount.where({ id: this.account.id })
.decrement({ amount: this.amount });
}
}
This is also similar to Redux’s actions. Unlike Redux, though, event-sourcing stores these changes long-term as well. This allows us to replay these changes up to any point and reproduce the application state at any point in time. For instance, if we need to figure out how much was in a bank account on a certain date, we need to only replay all events that happened to the account until we get to that date. Events in this context would include inward transfers, outward transfers, bank charges, direct debits and so on. When mistakes happen (events with bad data), we can discard the current application state, correct the event and recompute the state afresh.
CQRS and event sourcing often go hand-in-hand. (Fun fact: Redux is actually based in part on CQRS and event sourcing.) Commands can be written so that they dispatch events when fired. Then the events speak to the store (database) and update the state. In a real-time application, the Query objects could also listen for the events and fetch the updated state from the store.
Using any of these two patterns in simple apps is likely to be overkill and introduce unnecessary complexity. For applications built on a complex business domain, though, they are powerful abstractions that better model the domain and manage state.
Note that CQRS and event-sourcing can be implemented in different ways, some more complicated than others. We’ve only shown a simple implementation in our examples thus far. If you’re working with Node.js, I suggest you take a look at Wolken Kit. It provides one of the simpler interfaces for implementing CQRS and event sourcing that I’ve found.
Conclusion
Redux is a great tool for managing state and making its mutations behave in a predictable manner. In this article, we’ve looked at its core concepts and we’ve seen that, while using Redux on the server is probably not a good idea, we can apply some of its principles to the server. For more reading on CQRS and event-sourcing, check out these articles:
I write about my software engineering learnings and experiments. Stay updated with Tentacle: tntcl.app/blog.shalvah.me.