Skip to content

State management with the observer pattern

Olav Grønås Gjerde edited this page Oct 6, 2018 · 11 revisions

State management with the observer pattern

Introduction

One of the questions many React developers ask me, is how do I do state management? State management is hard, and has always been, it is still hard with React, especially as complexity grows. State management requires planning, you first draft will most likely fail, but the second attempt will probably be much much better. You learn as you go and with exercise you will become an excellent craftsman dealing with architecting state management. It takes time and you need to fail several times. When it comes to state management, practise outperform theory. But some theory is good to have, and how the Observer pattern work is one of them.

Adding more infrastructure

So we have come so far to implement a table, and now we want to change its state. We need a smart way to update our table when we add or update a book. Please keep in mind that you have to order the classes correctly when saving them to the js file, subclasses need to be parsed in browser after the parent class.

We start with creating an Observable class with an implementation can extend:

class Observable{

	constructor(){
		this.subscribers = [];
	}

	/**
	 * Subscribe for changes
	 * Add a function you want to be executed whenever this model changes.
	 *
	 * @params Function fn
	 * @return null
	 */
	subscribe(fn) {
		this.subscribers.push(fn);
	}

	/**
	 * Unsubscribe from being notified when this model changes.
	 *
	 * @params Function fn
	 * @return null
	 */
	unsubscribe(fn) {
		this.subscribers = this.subscribers.filter( it => it !== fn );
	}

	/**
	 * Notify subscribers
	 *
	 * @return null
	 */
	notifySubscribers() {
		this.subscribers.forEach( fn => fn() );
	}

}

As you see we have three methods, a subscribe method, unsubscribe and notifySubscriber. This is basis the Observer Pattern, you can see another example written in javascript of it here.

Refactoring books reference

We also change the books list reference in our BooksTable class. We create a new books collection class that extends our Observable class. The BooksCollections class looks like this.

class BooksCollection extends Observable{

	constructor(books){
		super();
		this.books = books;
	}

        /**
	 * Loop through all registered books and return
         * the book object that matches the id.
	 *
	 * @params Number id
	 * @return Book
	 */
	getBook(id){
		return this.books.find( it => it.id === id);
	}

        /**
	 * Register a new book to this collection.
         * Notify the subscribers.
	 *
	 * @params Book book
	 * @return void
	 */
	addBook(book){
		this.books.push(book);
		this.notifySubscribers();
	}

        /**
	 * Update a book object.
         * Notify the subscribers.
	 *
	 * @params Object obj
	 * @return void
	 */
	updateBook(obj){
		const book = this.getBook(obj.id);
		book.updateProperties(obj);
		this.notifySubscribers();
	}
}

Notice that we invoke the notifySubscribers() method when we add a book or update a book. This means any subscriber function added to this class will be invoked.

Update the code for the Books Table

The code for state management is in place, now its time to add the function that will be invoked when we make changes in our books collection.

        // We add an arrow function that will be executed
	// each time notifySubscribers() is invoked
	bookCollectionInstance.subscribe(() => {
		const bookTable = document.getElementById("book-table");
		bookTable.innerHTML = `
			<thead>
			<tr>
				<th>id</th>
				<th>title</th>
				<th>isbn</th>
				<th>author</th>
			</tr>
			</thead>
			${bookCollectionInstance.books.map(book => `
				<tr>
					<td>${book.id}</td>
					<td>${book.title}</td>
					<td>${book.isbn}</td>
					<td>${book.author.name}</td>
				</tr>
			`).join('')}
		`;
	});

	// We don't have a render method yet, so we just invoke the arrow function
	// we just added.
	bookCollectionInstance.notifySubscribers();

That's it. You are now ready to play with this. A full working example can be found in the example2 folder

Next is example3, a complete example!.