Blog

About

Contact

Back to blog

Writing a state manager from scratch

In a POC with almost no dependencies, I had to reinvent the wheel and write a simple state manager.

Wednesday, March 1, 2023

Week Info Screenshot

Hi there! I’m so excited to share with you the amazing project I’ve been working on - creating a store manager that uses local storage to save data, and making it reactive so it can listen to local storage changes and show them in the app’s UI. It’s been a really fun experience and I’m so proud of the result!

Let’s get started on understanding how I crafted this store manager. Initially, I needed to think of a way to store the Firestore data and observe for any changes. Because I was constructing an Electron application without any external dependencies, I had to use local storage to save the data.

Once I had successfully saved the Firestore data to local storage, the next step was to ensure changes were reflected in real-time. However, by default, local storage cannot be listened for changes in the same tab or page. An event “storage” exists, and can be added to the document object with addEventListener("storage"), but this event won’t work on the same page that is making the changes. To address this issue, I created a custom store manager that uses the window object’s properties to store and access data.

This approach allowed me to create a store manager that is reactive, meaning that it listens to local storage changes and updates the data in the UI in real-time. Here’s how I implemented it:

First, I defined a StoreCallback interface that describes the signature of the functions that will be used for publishing and subscribing:

interface Window {
  store: {
    publish: StoreCallback
    subscribe: StoreCallback
  }
}

interface StoreCallback {
  (event: string, data: unknown): void
}

Then, I created a custom store manager object and assigned it to the window object’s store property. This object has two methods: publish and subscribe.

window.store = {
  publish: function (event: string, data: unknown): void {
    if (!this[event]) {
      this[event] = []
    }
    this[event].forEach((callback) => {
      callback(data)
    })
  },
  subscribe: function (event: string, callback): void {
    if (!this[event]) {
      this[event] = []
    }
    this[event].push(callback)
  }
}

The publish method takes an event string and some data. If the event has no subscribers, it creates a new empty array for that event. It then loops through all the subscribers and calls their callback function with the data.

The subscribe method takes an event string and a callback function. If the event has no subscribers, it creates a new empty array for that event. It then adds the callback function to the array of subscribers.

Finally, to make the store manager listen to local storage changes, I overrode the setItem method of the localStorage object.

localStorage.setItem = function (key: string, value: string): void {
  window.localStorage[key] = value
  window.store.publish(key, value)
}

This method first saves the key and value to the localStorage. It then calls the store manager’s publish method with the key and value as arguments, which in turn updates the UI with the new value.

To demonstrate how this works in practice, here’s an example component that subscribes to changes in the admin_global event:

window.store.subscribe('admin_global', (data) => {
      const event = JSON.parse(data) as Event
      this.querySelector<HTMLInputElement>('#event-name')!.value = event.name
      this.querySelector<HTMLInputElement>('#event-logo-title')!.value = event.logo.title

      if (event.logo.url !== '') {
        this.querySelector<HTMLImageElement>('.current-event-logo img')!.src = event.logo.url
        this.querySelector('.upload-button')!.textContent = 'Upload new'
        this.querySelector('.current-event-logo')?.classList.remove('hidden')
      }
      this.querySelector<HTMLInputElement>('#win-zone')!.value = String(event.winZoneLevel)
    })

Finally, I showed you how to create a helpful store manager that can store data in the local storage, and update the UI in real-time when the local storage is modified. By taking advantage of the setItem feature of the localStorage object, I was able to detect changes and keep the UI in sync without relying on any additional libraries or frameworks. Although this approach has some limitations, you can always modify it to get a better and more reliable solution that you can use in production.