Redux in C++: Reducer and Action
Building Redux reducer and action for C++ application.
Published on 9 May 2020. Posted under category: Development

Redux describes itself as “A predictable state container for JS apps”. JavaScript does make it easy to implement Redux pattern, especially with ES2015 spread syntax.
Surprisingly, it’s also quite easy to use Redux pattern in a C++ application. In fact, this pattern is really powerful in C++ because we can enforce immutability.
In this series, we will see how to use Redux pattern in a todo-list application.
Let’s start with preparing a state model reducer and some actions to demonstrate the concept using modern C++ features such as variant
and optional
.
Goal
We want a reducer function that, given an appropriate action, returns a new state.
// main.cpp
int main() {
// As a user, given an empty initial state.
auto initialState = State{};
// When I add a new task using addTask action.
auto addAction = action::addTask(1u, "Check turnip price.");
// Then the reducer returns a new state with my new task.
auto newState = reduce(initialState, addAction);
assert(newState.tasks().size() == 1);
auto v1Task = newState.tasks().at(1u);
assert(v1Task.description() == "Check turnip price.");
// When I edit my task using editTask action.
auto tomorrow = app::tomorrow();
auto editAction =
Action{ActionType::EditTask,
Payload{
{"taskID", 1u},
{"dueTime", tomorrow},
{"note", "I bought those turnips at 133 Bells."},
}};
// Then the reducer returns a new state with updated task.
newState = reduce(newState, editAction);
assert(newState.tasks().size() == 1);
auto v2Task = newState.tasks().at(1u);
assert(v2Task.dueTime() == tomorrow);
assert(v2Task.note() == "I bought those turnips at 133 Bells.");
// And the reducer does not alter previous state.
assert(v1Task.dueTime() == app::zero());
assert(v1Task.note() == "");
return 0;
}
That’s not too bad, isn’t it? At this point the program won’t even compile, let alone check those assertions. Let’s start by making them compile-able.
In a real project I would start by describing requirements using BDD tools such as Catch2. And probably separate the test data from the actual test.
State model
At this point the state model is trivial.
It only contains a map of tasks and each task has properties such as task()
, dueTime()
and note()
.
Let’s declare those first.
// state.h
#ifndef STATE_H // Include guard, will omit those in next snippets
#define STATE_H
#include <map>
using id_t = unsigned int;
class Task;
class State {
std::map<id_t, Task> _tasks = {};
public:
// constant property getter:
auto tasks() const -> std::map<id_t, Task>;
// constant property setter:
auto tasks(const std::map<id_t, Task>& newTasks) const -> State;
};
#endif
I started by declaring an useful type alias for ID.
In this case I’m using unsigned int
to disable negative numbers.
I don’t recommend using too much type alias, but in this case it’s useful because (much) later on we might consider to use different type for ID, such as a hash string.
Having this alias early enables me to change everything at once later.
I did not define any constructor for the State
.
Instead, I opted to use constant setter pattern popularized by D3 and Swift to configure the State
.
This constant setter does not alter member and return a new object with updated member instead.
This is especially useful in Redux pattern because it explicitly says that all methods are constant, enforcing an immutable state model.
The implementation of the getter and setter looks like this:
// state.cpp
auto State::tasks() const -> map<id_t, Task> {
return _tasks;
}
auto State:tasks(const map<id_t, Task>& newTasks) const -> State {
auto newState = State{*this}; // copy
newState._tasks = newTasks;
return newState;
}
This pattern repeats all over the place and it is very tempting to hide it behind macro or template. I’d suggest to just stick with a keyboard snippet.
Don’t hide simplicity.
The State
class serves as the trunk of the state model.
It will become busier in a complete application, but for now that’s all.
Let’s move on and declare the actual Task
class.
// state.h
class Task {
std::string _description = "";
app::time_point_t _dueTime = app::zero();
std::string _note = "";
public:
auto description() const -> std::string;
auto description(const std::string& description) const -> Task;
auto dueTime() const -> app::time_point_t;
auto dueTime(const app::time_point_t& dueTime) const -> Task;
auto note() const -> std::string;
auto note(const std::string& note) const -> Task;
};
The Task
class use the same constant getter and setter pattern.
Compared to JavaScript, it’s a lot of boilerplate code to write a state model.
The prize, however, is a rock solid immutable structure with almost no cognitive burden to implement (without snippet I only spent about 5 minutes typing the whole definition).
What if we just use plain struct?
The benefit is not visible now, but the boilerplate actually makes the code more maintainable. For example, later on we can have a property to show when the task is updated. Without a setter, we must move the logic that handles timestamp update to reducer. This can easily get out of control once we have other side effects, something that is really common in real application.
Action
A central idea of the Redux pattern is using action and only action to perform state update. This gives us a clear understanding on what happened in the application.
There are several ways to implement action, the easiest one is to use a JSON library, such as Niels Lohmann’s. I prefer to use enumerations to categorize action type and plain map to store the payload.
// action.h
using payload_t =
std::variant<id_t, std::string, app::time_point_t, int, double>;
struct Payload : public std::map<std::string, payload_t> {
using std::map<std::string, payload_t>::map;
auto get(const std::string& key) const -> std::optional<payload_t>;
};
enum class ActionType {
AddTask = 1,
EditTask,
};
class Action {
ActionType _type;
Payload _payload;
public:
Action() = delete;
Action(ActionType type, const Payload& payload);
auto type() const -> ActionType;
auto payload() const -> const Payload&;
};
The payload_t
type alias defines what are the acceptable data to store into action payload.
I mentioned earlier that this should be a plain data type, so the variant only accepts numbers and string type.
The type time::time_point_t
(see Addendum) falls into this category because it’s essentially a plain integer with extra methods.
Rather than sending number or date string around to specify time, using the type system to explicitly say that a variable is a time would help in the long run.
Same reasoning applies with id_t
.
As a rule of thumb, avoid putting in type with multiple member variables into payload variant.
The Payload
type itself is a derivation of std::map
and inherits all of its constructors.
In general I dislike extending standard type like this since it’s hiding the underlying type.
However I found the Payload::get
method really convenient (and in the actual project I ended up putting in even more quality-of-life functions).
The Payload::get
function returns an optional value.
Prior to C++17, and in language such as Go, I used to use pointer for something that may or may not exist.
This leads to many edge case scenario and crashes due to trivial mistake.
Now we can explicitly tell compiler that a variable is optional using <optional>
library.
In many other language, this feature becomes a language feature (not just library) so we can put ?
after a type to mark that as an optional.
C++ is not there yet, but glad to have this feature at all.
// action.cpp
auto Payload::get(const std::string& key) const -> std::optional<payload_t> {
auto it = find(key);
if (it == end())
return {};
return std::make_optional(it->second);
}
I observe that I don’t need that std::make_optional
when compiling with Visual Studio, which is just what I expect.
However that raises error on Mac with Clang 11 compiler, so just to be safe I added std::make_optional
there.
Keen reader would point out that we can use
<unordered_map>
for faster random access lookup because we don’t need to store payload in a particular order (same reasoning also applies toState
).
The action itself does not actually do anything to change application state, it only stores necessary information to do the change. We need a reducer to actually create a new state from given action.
Reducer
Reducer is a function that, given previous state and an action, returns a new state.
Reducer essentially maps every actions into handler functions.
To put things clean, I put the handler functions inside reducer
namespace.
// reducer.h
auto reduce(const State& state, const Action& action) -> State;
namespace reducer {
auto tasks(const std::map<id_t, Task>& state, const Action& action)
-> std::map<id_t, Task>;
auto addTask(const std::map<id_t, Task>& state, const Action& action)
-> std::map<id_t, Task>;
auto editTask(const std::map<id_t, Task>& state, const Action& action)
-> std::map<id_t, Task>;
}
The first level of mapping happens in the reduce
function.
This function is essentially composing all reducers into a complete state.
// reducer.cpp
auto reduce(const State& state, const Action& action) -> State {
return state
.tasks(reducer::tasks(state.tasks(), action));
}
The second, and final, level of mapping happens in noun reducers.
// reducer.cpp
auto reducer::tasks(const std::map<id_t, Task>& state, const Action& action)
-> std::map<id_t, Task> {
// check if payload has taskID
auto taskID$ = action.payload().get("taskID");
if (!taskID$.has_value())
return state;
switch (action.type()) {
case ActionType::AddTask:
return reducer::addTask(state, action);
case ActionType::EditTask:
return reducer::editTask(state, action);
}
return state;
}
Ideally I’d like to use taskID?
as a variable name for optional values, but that’s not doable.
I’m borrowing $
symbol for now, at least until I start using observables.
Finally, the action handler itself happens in verb reducers.
auto reducer::editTask(const std::map<id_t, Task>& state, const Action& action)
-> std::map<id_t, Task> {
auto taskID = get<id_t>(action.payload().at("taskID"));
// check if task does not exist
// here you can see why I prefer to extend map with get method...
auto taskIter = state.find(taskID);
if (taskIter == state.end())
return state;
auto task = taskIter->second;
auto description = get<string>(
action.payload().get("description").value_or(task.description()));
auto note = get<string>(action.payload().get("note").value_or(task.note()));
auto dueTime = get<app::time_point_t>(action.payload().get("dueTime").value_or(task.dueTime()));
auto newState = map<id_t, Task>{state};
newState[taskID] = task
.description(description)
.note(note)
.dueTime(dueTime);
return newState;
}
The payload extraction part looks really verbose. Let’s dissect it a bit.
optional<payload_t> descriptionOrNull = action.payload().get("description");
payload_t descriptionVariant = descriptionOrNull.value_or(task.description());
string description = get<string>(descriptionVariant);
I generally try to use as little template as possible.
In this case, however, the compiler should recognize the type of description
from the fallback value we provided.
We just need to tell it where to look.
// action.h
struct Payload : public std::map<std::string, payload_t> {
template<class T>
auto get(const std::string& key, const T& fallback) const -> T {
return std::get<T>(get(key).value_or(fallback));
}
};
And the action handler can be rewritten as:
// reducer.cpp
auto reducer::editTask(const std::map<id_t, Task>& state, const Action& action) {
...
auto description = action.payload().get("description", task.description());
auto note = action.payload().get("note", task.note());
auto dueTime = action.payload().get("dueTime", task.dueTime());
...
}
Final thoughts
I have to admit that I was sceptical at first if Redux in C++ is doable at all.
To my surprise, C++17 surpasses my expectations on all fronts.
I especially like how new library such as <optional>
and <variant>
can be used together to make a pleasant developer experience while retaining correctness.
Having those as libraries instead of language features has some drawbacks, but to be honest a language bloated with feature is not that pleasant to work with either. I prefer to keep things simple and only go to the exotic places every now and then.
Some recommendations when working with Redux
To keep things manageable as the application grows, always keep the state structure flat and refer to other data using ID. Also, don’t make an action that requires two separate reducers to handle. Instead, compose multiple actions to get the desired effect.
In later series we will see how to put that into practice by having users in the state model. Before going there, the next article will complete the trinity by implementing Redux store and have a look at how to implement undo. Stay tuned!
Further reading and source files
- Redux Core Concept
- Everything You Need to Know About std::variant from C++17
- std::optional: How, when, and why
Get the source files here.
Addendum: Working with time
C++ Standard Library is great, but sometime it’s just way too powerful for its own right.
This is especially true for more powerful part of the library like <chrono>
and <random>
.
To work with those kind of libraries, I’d prefer to pick a good enough defaults and wrap them in a sane helper. That way I can adjust the actual workhorse later while keeping the code that matters readable.
In this project, for example, I chose a steady_clock
as my clock, gave it a simple type alias, then worked around that type alias.
Here’s how the helper might look like.
// time.h
namespace app {
using clock_t = std::chrono::steady_clock;
using time_point_t = app::clock_t::time_point;
auto zero() -> time_point_t;
auto now() -> time_point_t;
auto tomorrow() -> time_point_t;
}
// time.cpp
#include "time.h"
using namespace app;
auto app::zero() -> time_point_t {
return time_point_t{};
}
auto app::now() -> time_point_t {
return clock_t::now();
}
auto app::tomorrow() -> time_point_t {
return app::now() + std::chrono::hours(24);
}
Time support in C++ is particularly heading towards an exciting future with locale aware time provided by tzdb
coming to C++20.
The Web already provide this for years, and it’s such a life-saver.
I’m glad that it’s part of the standard library (along with other number and money locale support), but looking at the documentation I can see the comittee are going with power first ease-of-use second, just like the situation we have now with <chrono>
and <random>
.
As a bonus exercise, please add more helper to time.h
with function that, given a time point, returns a string such as “Tomorrow”, “In 6 hours”, etc.
We can compare answer on the next article where we build a Redux store.