In this tutorial we will go step-by-step through building time travel debugging from scratch. We will begin by covering the core principles of Redux and how they enable building such a powerful feature. We will then build the Redux core library and time travel debugging in pure JavaScript and connect it to a simple HTML application, without using any React.

The fundamentals of time traveling with Redux

Time travel debugging refers to the ability step forward and backward through the state of you application, empowering the developer understand exactly what is happening at any point in the app’s lifecycle.

Redux is an extension of the flux pattern which enforces unidirectional data flow. In addition, Redux adds 3 principles to the flux philosophy.

A single source of truth for the state. The entire state of the application is stored in a single JavaScript object. State is read-only. This is the concept of immutability. The state is never altered, but every actions generates a brand new state object, replacing the old one. Changes are made with pure functions. This means that any time a new state is generated, it happens without triggering any other side effects.

By utilizing the fundamental concept that the state of a Redux application is generated in this linear and predictable timeline, time travel debugging extends the concept by storing a copy of the state tree generated by every action that is triggered.

The UI can be thought of as a pure function of the Redux state. Time traveling allows us to set the state of the application to a desired value and generate the exact UI that will be created under those conditions. This kind of visibility and transparency around an application is an incredibly powerful tool for a developer to fully understand what’s happening inside their application and significantly reduce the effort required to debug it.

Building a simple application with Redux and time travel debugging

We are going to build a simple HTML application that generates a random background color when clicked and uses Redux to store the RGB values. We will also create a time travel extension that will allow us to replay every state of the application and visually see the background changing at each step.

Building the Redux core library

If you are interested in building time travel debugging, I am also going to assume you have familiarity with Redux. If you are new to Redux or need a refresher on the store and reducer, refer to this article before continuing for a detailed explanation. In this tutorial, you will walk through building createStore and reducers step-by-step.

The Redux core library is the createStore function. The Redux store manages the state object which represents the entire state of the application and exposes the methods necessary to read from and update the state. Invoking createStore initializes the state and returns an object containing the getState() , subscribe() , and dispatch() methods.

The createStore function takes one required argument which is the reducer function and can optionally receives an initialState argument. The entirety of createStore is the following (crazy how small it is, right?):

Implementing time travel debugging

We will implement time traveling by subscribing a new listener to the Redux store and also extending the store’s functionality. Each change in state will be added to an array, giving us a synchronous representation of every change in state of the application. We will print the list of states to the DOM for clarity.

First we initialize the timeline and index of the active state in the history (line 1–2). We also create a saveTimeline function that adds the current state to the timeline array, prints the state to the DOM, and increments the index of the given state tree that is being rendered by the app. To ensure we capture every state change, we subscribe the saveTimeline function as a listener to the Redux store.

const timeline = [];

let activeItem = 0; const saveTimeline = () => {

timeline.push(store.getState());

timelineNode.innerHTML = timeline

.map(item => JSON.stringify(item))

.join('<br/>');

activeItem = timeline.length - 1;

}; store.subscribe(saveTimeline);

Next add a new function to the store — setState . This will allow us to inject any state into the Redux store. It will be called as we time travel between states using buttons on the DOM that we will create in the next section. The following is the implementation of the setState function on the store.

// FOR DEBUGGING PURPOSES ONLY

store.setState = desiredState => {

store.state = desiredState; // Assume the debugger is injected last. We don't want to update

// the saved states as we are debugging, so we slice it off.

const applicationListeners = store.listeners.slice(0, -1);

applicationListeners.forEach(listener => listener());

};

Keep in mind that this is for educational purposes only. You should not extend the Redux store or set the state in this manner.

When we build the full application in the following section, we will also build out the DOM. For now, all you need to know is that there will be a “previous” and a “next” button to enable time traveling. These buttons will update the active index for the timeline state being shown, allowing us to easily move forward and backward through the state changes. The follow shows how we register the event listeners to navigate the timeline:

const previous = document.getElementById('previous');

const next = document.getElementById('next'); previous.addEventListener('click', e => {

e.preventDefault();

e.stopPropagation(); let index = activeItem - 1;

index = index <= 0 ? 0 : index;

activeItem = index; const desiredState = timeline[index];

store.setState(desiredState);

}); next.addEventListener('click', e => {

e.preventDefault();

e.stopPropagation(); let index = activeItem + 1;

index = index >= timeline.length - 1 ?

timeline.length - 1 : index;

activeItem = index; const desiredState = timeline[index];

store.setState(desiredState);

});

Putting this all together yields the following code to create time travel debugging.

Building an application with time travel debugging

We will now create a visual representation to understand time travel debugging. We add an event listener to the document body that will generate three random numbers between 0–255 and save those as r , g , and b in the Redux store. A function subscribed to the store that will update the background color as well as display the current RGB value on the screen. In addition, our time travel debugger will be subscribed to the state changes and add each change to the timeline.

We begin by initializing the HTML document as follows.

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

<title></title>

</head>

<body>

<div>My background color is <span id="background"></span></div>

<div id="debugger">

<div>

<button id="previous">

previous

</button>

<button id="next">

next

</button>

</div>

<div id="timeline"></div>

</div>

<style>

html, body {

width: 100vw;

height: 100vh;

} #debugger {

margin-top: 30px;

}

</style>

<script>

// Application logic will be added here...

</script>

</body>

</html>

Notice that we also create a <div> for the debugger. There are buttons to navigate the different states and a node to list each update in the state.

Inside the script, we begin by referencing the DOM nodes and our createStore .

const textNode = document.getElementById('background');

const timelineNode = document.getElementById('timeline'); const createStore = (reducer, initialState) => {

const store = {};

store.state = initialState;

store.listeners = []; store.getState = () => store.state; store.subscribe = listener => {

store.listeners.push(listener);

}; store.dispatch = action => {

console.log('> Action', action);

store.state = reducer(store.state, action);

store.listeners.forEach(listener => listener());

}; return store;

};

Next, we create the reducer to track the RGB values and initialize the store. The initial state will be a white background.

const getInitialState = () => {

return {

r: 255,

g: 255,

b: 255,

};

}; const reducer = (state = getInitialState(), action) => {

switch (action.type) {

case 'SET_RGB':

return {

r: action.payload.r,

g: action.payload.g,

b: action.payload.b,

};

default:

return state;

}

}; const store = createStore(reducer);

Now we can subscribe a function to the store that will set the background color and add the text RGB value to the DOM. This is cause any update to the state to be represented our UI.

const setBackgroundColor = () => {

const state = store.getState();

const { r, g, b } = state;

const rgb = `rgb(${r}, ${g}, ${b})`; document.body.style.backgroundColor = rgb;

textNode.innerHTML = rgb;

}; store.subscribe(setBackgroundColor);

Finally we add a function to generate a random number 0–255 and an onClick event listener that will dispatch a new RGB value to the store.

const generateRandomColor = () => {

return Math.floor(Math.random() * 255);

}; // A simple event to dispatch changes

document.addEventListener('click', () => {

console.log('----- Previous state', store.getState());

store.dispatch({

type: 'SET_RGB',

payload: {

r: generateRandomColor(),

g: generateRandomColor(),

b: generateRandomColor(),

},

});

console.log('+++++ New state', store.getState());

});

This is the entirety of our application logic. We add the time travel code from the previous section below and at the bottom of the script tag call store.dispatch({}) to generate the initial state.

Below is the full working code of the application.

Wrap up

Our educational implementation of time travel debugging depicts the core principles of Redux. We can effortlessly track the ongoing state of our application, making it easy to debug and understand fully what is happening.