Making React + Redux in VanillaJS (kind of)

April 17, 2018

So I wanted to make a simple prototype for a webapp thing I need to build in my thesis, and I got all excited thinking about all the cool libraries and patterns I could use to build something that is basically just Photo Booth.app... Overkill much 🤦‍♀️

After coming to the conclusion I don't need React + (insert long list of currently popular libraries that solve a problem you don't have but still need to solve for some reason) I set out to just make it in plain JS - nice and simple!

As a bit of a challenge I wanted to make my app state-driven (e.g. in React/Redux, if the state changes the relevant components/subscribers get updated). This was something I've always enjoyed about React so I thought it would be fun to roll my own version of it (don't worry, I'm not making another JS framework 😬).

This was the hacky solution I came up with:

// Global store
const store = {
	myone: 123,

	// Key containing some todos data
	todos: [
		{
			text: 'My first todo',
			completed: false,
		},
		{
			text: 'Another todo that I have done',
			completed: true,
		},
	],

	otherData: {},

	andSomeMore: '1',
	
	// Listeners to notify based on updates to particular keys
	listeners: {},
	
	/*
	*  Subscribe a function to "listen" to any changes made to the store[key] data
	*  @param keys - single string key or an array of keys to listen to
	*  @param listener - function to call when data changes
	*/
	subscribe: function(keys, listener) {
		const arrKeys = [].concat(keys)
		for (const key of arrKeys) {
			if (!(key in this.listeners)) {
				this.listeners[key] = []
			}
			this.listeners[key].push(listener)
		}
		listener()
	},
	
	/*
	*  Update data in the store and notify any relevant listeners.
	*  @param key - key to update in the store
	*  @param data - data to place at that key
	*/
	update: function(key, data) {
		if (!key in store) {
			console.warn(`Key: (${key}) not in store object`)
			return
		}
		if (store[key] === data) {
			console.log('Attempted updating store with the same state')
			return
		}

		store[key] = data
	
		// Call all listeners of this key
		for (const listener of this.listeners[key]) {
			listener()
		}
	},
}

Simple but shortsighted, I don't handle adding new keys, prevent direct mutation of the store, handle actions nicely with reducers etc. But we can update the todos data using update() and expect all listeners to be notified about it, we can use this then to re-render the "components"!

So below is an example of the very basic "component" structure that I used. Key parts namely are the reference to the node in the DOM (if it had one) and the render function which returns a HTML string.

// Component (you could do this using a class)
const TodoList = {

	// DOM element the component connects to on the html page
	elem: document.getElementById('todo_list'),
	
	// Replace element with newly rendered html
	update: function() {
		this.elem.innerHTML = this.render()
	},
	
	// Genreate HTML string representing element
	render: function() {
		return `<ul>
			${store.todos.map((t, i) => `<li>${Todo.render(t, i)}</li>`).join('')}
		</ul>`	// Hmmm. The highlighter breaks on nested template literals...
	},
}
TodoList.update = TodoList.update.bind(TodoList)

// Subscribe to the 'todos' data, call update() everytime it gets updated
store.subscribe('todos', TodoList.update)

As you can see I use store.subscribe() to listen to the 'todos' key and call the TodoList.update() function every time there is a change. Simple right!

So what does this actually mean, well anywhere else in my code I can call store.update() and expect that any re-rendering will be automatically triggered.

// Add a new todo to the store
store.update('todos', {
	...store.todos,
	{
		text: 'A new todo item',
		completed: false,
	},
})
// Magic happens and things re-render! 🎉

Anyways, the prototype was actually pretty quick to build this way. I think it was rather useful to build something like this from scratch and form your own patterns to understand the design decisions behind some of these awesome libraries/frameworks that are available now.

That's me done for now, have a wonderful day! 🌈