Redux without React — State Management in Vanilla JavaScript
This article was peer reviewed by Vildan Softic. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!
I am one of those developers who likes to do things from scratch and get to know how everything works. Although I am aware of the (unnecessary) work I get myself into, it definitely helps me appreciate and understand what lies behind a specific framework, library, or module.
Recently, I had one of those moments again and started working on a web application using Redux and nothing else but vanilla JavaScript. In this article I want to outline how I structured my app, examine some of my earlier (and ultimately unsuccessful) iterations, before looking at the solutions I settled on and what I learned along the way.
The Setup
You might have heard of the popular React.js and Redux combination to build fast and powerful web applications with the latest front-end technologies.
Made by Facebook, React is a component-based, open source library for building user interfaces. While React is only a view layer (not a full framework such as Angular or Ember), Redux manages the state of your application. It functions as a predictable state container, where the entire state is stored in a single object tree and can only be changed by emitting a so called action. If you’re completely new to the topic, I recommend checking out this illustrative article.
For the rest of this article it’s not required to be an expert on Redux, but it definitely helps to have at least a basic understanding of its concepts.
Redux without React — an Application from Scratch
What makes Redux great is that it forces you to think ahead and get an early picture of your application design. You start to define what should actually be stored, which data can and should change, and which components can access the store. But since Redux is only concerned with state, I found myself a bit confused as to how to structure and connect the rest of my application. React does a good job of guiding you through everything, but without it, it was down to me to figure out what works best.
The application in question is a mobile-first Tetris clone, which has a couple of different views. The actual game logic is done in Redux, while the offline capabilities are provided by localStorage
, and custom view handling. The repository can be found on GitHub, though the application is still in active development and I am writing this article as I work on it.
Defining the Application Architecture
I decided to adopt a file structure commonly found in Redux and React projects. It’s a logical structure and is applicable to many different setups. There are many variations on this theme, and most projects do things a little differently, but the overall structure is the same.
actions/
├── game.js
├── score.js
└── ...
components/
├── router.js
├── pageControls.js
├── canvas.js
└── ...
constants/
├── game.js
├── score.js
└── ...
reducers/
├── game.js
├── score.js
└── ...
store/
├── configureStore.js
├── connect.js
└── index.js
utils/
├── serviceWorker.js
├── localStorage.js
├── dom.js
└── ...
index.js
worker.js
My markup is separated into another directory and is ultimately rendered by a single index.html
file. The structure is similar to scripts/
, so as to maintain a consistent architecture throughout my code base.
layouts/
└── default.html
partials/
├── back-button.html
└── meta.html
pages/
├── about.html
├── settings.html
└── ...
index.html
Managing and Accessing the Store
To access the store, it needs to be created once and passed down to all instances of an application. Most frameworks work with some sort of dependency injection container, so we as a user of the framework don’t have to come up with our own solution. But how could I make it accessible to all of my components when rolling my own solution?
My first iteration kinda bombed. I don’t know why I thought this would be a good idea, but I put the store in its own module (scripts/store/index.js
), which could then be imported by other parts of my application. I ended up regretting this and dealing with circular dependencies real quick. The issue was that the store didn’t get properly initialized when a component tried to access it. I’ve put together a diagram to demonstrate the dependency flow I was dealing with:
The application entry point was initializing all the components, which then made internal use of the store directly or via helper functions (called connect here). But since the store wasn’t explicitly created, but only as a side effect in its own module, components ended up using the store before it has been created. There was no way to control when a component or a helper function called the store for the first time. It was chaotic.
The store module looked like this:
scripts/store/index.js
(☓ bad)
import { createStore } from 'redux'
import reducers from '../reducers'
const store = createStore(reducers)
export default store
export { getItemList } from './connect'
As mentioned above, the store was created as a side effect and then exported. Helper functions also required the store.
scripts/store/connect.js
(☓ bad)
import store from './'
export function getItemList () {
return store.getState().items.all
}
This is the exact moment when my components ended up being mutually recursive. The helper functions require the store
to function, and are at the same time exported from within store initialization file to make them accessible to other parts of my application. You see how messy that already sounds?
The Solution
What seems obvious now, took me a while to understand. I solved this problem by moving the initialization to my application entry point (scripts/index.js
) , and passing it down to all required components instead.
Again, this is very similar to how React actually makes the store accessible (check out to the source code). There is a reason they work so well together, why not learn from its concepts?
The application entry point creates the store first of all, then passes it down to all the components. Then, a component can connect with the store and dispatch actions, subscribe to changes or get specific data.
Let’s go through the changes:
scripts/store/configureStore.js (✓ good)
import { createStore } from 'redux'
import reducers from '../reducers'
export default function configureStore () {
return createStore(reducers)
}
I’ve kept the module, but instead export a function named configureStore
which creates the store somewhere else in my code base. Note that this is only the basic concept; I also make use of the Redux DevTools extension and load a persisted state via localStorage
.
scripts/store/connect.js (✓ good)
export function getItemList (store) {
return store.getState().items.all
}
The connect
helper functions are basically untouched, but now require the store to be passed as an argument. At first I was hesitant to use this solution, because I thought “what’s the point of the helper function then?”. Now I think they’re good and high-level enough, making everything more readable.
import configureStore from './store'
import { PageControls, TetrisGame } from './components'
const store = configureStore()
const pageControls = new PageControls(store)
const tetrisGame = new TetrisGame(store)
// Further initialization logic.
This is the application entry point. The store
is created, and passed down to all of the components. PageControls
adds global event listeners to specific action buttons and TetrisGame
is the actual game component. Before moving the store here, it looked basically the same but without passing down the store to all modules individually. As mentioned earlier, the components had access to the store via my failed connect
approach.
Components
I decided to work with two kinds of components: presentational and container components.
Presentational components do nothing else besides pure DOM handling; they aren’t aware of the store. Container components on the other hand can dispatch actions or subscribe for changes.
Dan Abramov has written down a great article on that for React components, but the methodology can be applied to any other component architecture as well.
For me there are exceptions though. Sometimes a component is really minimal and only does one thing. I didn’t want to split them up into one of the aforementioned patterns, so I decided to mix them. If the component grows and gets more logic, I will separate it.
scripts/components/pageControls.js
import { $$ } from '../utils'
import { startGame, endGame, addScore, openSettings } from '../actions'
export default class PageControls {
constructor ({ selector, store } = {}) {
this.$buttons = [...$$('button, [role=button]')]
this.store = store
}
onClick ({ target }) {
switch (target.getAttribute('data-action')) {
case 'endGame':
this.store.dispatch(endGame())
this.store.dispatch(addScore())
break
case 'startGame':
this.store.dispatch(startGame())
break
case 'openSettings':
this.store.dispatch(openSettings())
break
default:
break
}
target.blur()
}
addEvents () {
this.$buttons.forEach(
$btn => $btn.addEventListener('click', this.onClick.bind(this))
)
}
}
The above example is one of those components. It has a list of elements (in this case all elements with a data-action
attribute), and dispatches an action on click, depending on the attributes content. Nothing else. Other modules might then listen to changes in the store and update themselves correspondingly. As already mentioned, if the component also made DOM updates, I would separate it.
Now, let me show you a clear separation of both component types.
Updating the DOM
One of the bigger questions I had, when starting the project, was how to actually update the DOM. React uses a fast in-memory representation of the DOM called Virtual DOM to keep DOM updates to a minimum.
I was actually thinking of doing the same, and I could well switch to Virtual DOM, if my application should grow bigger and more DOM heavy, but for now I do classic DOM manipulation and that works fine with Redux.
The basic flow is as follows:
- A new instance of a container component is initialized and passed the
store
for internal use - The component subscribes to changes in the store
- And uses a different presentational component to render updates in the DOM
Note: I am a fan of the $
symbol prefix for anything DOM related in JavaScript. It is, as you might have guessed, taken from jQuery’s $
. Hence, pure presentational component filenames are prefixed with a dollar sign.
import configureStore from './store'
import { ScoreObserver } from './components'
const store = configureStore()
const scoreObserver = new ScoreObserver(store)
scoreObserver.init()
There’s nothing fancy going on here. The container component ScoreObserver
gets imported, created, and initialized. What does it actually do? It updates all score related view elements: the high score list and, during the game, the current score info.
scripts/components/scoreObserver/index.js
import { isRunning, getScoreList, getCurrentScore } from '../../store'
import ScoreBoard from './$board'
import ScoreLabel from './$label'
export default class ScoreObserver {
constructor (store) {
this.store = store
this.$board = new ScoreBoard()
this.$label = new ScoreLabel()
}
updateScore () {
if (!isRunning(this.store)) {
return
}
this.$label.updateLabel(getCurrentScore(this.store))
}
// Used in a different place.
updateScoreBoard () {
this.$board.updateBoard(getScoreList(this.store))
}
init () {
this.store.subscribe(this.updateScore.bind(this))
}
}
Bear in mind that this is a simple component; other components might have more complex logic and things to take care of. What is going on here? The ScoreObserver
component saves an internal reference to the store
and creates new instances of both presentational components for later usage. The init
method subscribes to store updates, and updates the $label
component on each store change — but only if the game is actually running.
The updateScoreBoard
method is used in a different place. It doesn’t make sense to update the list every time a change happens, as the view is not active anyway. There is also a routing component, which updates or deactivates different components on each view change. Its API looks roughly like this:
// scripts/index.js
route.onRouteChange((leave, enter) => {
if (enter === 'scoreboard') {
scoreObserver.updateScoreBoard()
}
// more logic...
})
Note: $
(and $$
) is not a jQuery reference, but a handy utility shortcut to document.querySelector
.
scripts/components/scoreObserver/$board.js
import { $ } from '../../utils'
export default class ScoreBoard {
constructor () {
this.$board = $('.tetrys-scoreboard')
}
emptyBoard () {
this.$board.innerHTML = ''
}
createListItem (txt) {
const $li = document.createElement('li')
const $span = document.createElement('span')
$span.appendChild(document.createTextNode(txt))
$li.appendChild($span)
return $li
}
updateBoard (list = []) {
const fragment = document.createDocumentFragment()
list.forEach((score) => fragment.appendChild(this.createListItem(score)))
this.emptyBoard()
this.$board.appendChild(fragment)
}
}
Again, a basic example and a basic component. The updateBoard()
method takes an array, iterates over it, and inserts its content into the score list.
scripts/components/scoreObserver/$label.js
import { $ } from '../../utils'
export default class ScoreLabel {
constructor () {
this.$label = $('.game-current-score')
this.$labelCount = this.$label.querySelector('span')
this.initScore = 0
}
updateLabel (score = this.initScore) {
this.$labelCount.innerText = score
}
}
This component does almost exactly the same as above ScoreBoard
, but only updates a single element.
Other Mistakes and Advice
Another important point is to implement a use case driven store. In my opinion it’s important to only store what is essential for the application. At the very beginning I stored almost everything: current active view, game settings, scores, hover effects, the user’s breathing pattern, and so on.
While this might be relevant for one application, it’s not for another. It can be good to store the current view, and continue at the exact same position on reload, but in my case this felt like bad user experience and more annoying than useful. You wouldn’t want to store the toggle of a menu or modal either, would you? Why should the user come back to that specific state? It might make sense in a larger web application. But in my small mobile focused game, it is rather annoying to come back to the settings screen just because I left off there.
Conclusion
I have worked on Redux projects with and without React and my main take-away is, that huge differences in application design aren’t necessary. Most methodologies used in React can actually be adapted to any other view handling setup. I took me a while to realize this, as I started off thinking I have to do things differently, but eventually I figured this is not necessary.
What is different however, is the way you initialize your modules, your store, and how much awareness a component can have of the overall application state. The concepts stay the same, but the implementation and amount of code is suited to exactly your needs.
Redux is a great tool, which helps structure your application in a more thought-out way. When used alone, without any view libraries, it can be quite tricky at first, but once you get past that initial confusion nothing can stop you.
What do you think of my approach? Have you been using Redux alone with a different view handling setup? I would love to get your feedback and discuss it in the comments.
If you’re looking for more on Redux, check out our course Rewriting and Testing Redux to Solve Design Issues mini course. In this course, you’ll build a Redux application that receives tweets, organized by topic, through a websocket connection. To give you a taster of what’s in store, check out the free lesson below.