loading...

Writing your own Redux-like state library

talentlessguy profile image v 1 r t l ・4 min read

Intro

Recently I wrote an article about implementing vDOM:

Today we will have something more simple, state management library. As last time, I remind you that there are tons of articles about this topic, I just tell my experience so please don't say "you stole it" in comments :)

Concepts

It is ok if you don't know Redux, honestly I didn't use it much before trying implementing state by myself. I wanted to do something similar to hooks but no succeed. Even though, Redux is much easier to remake.

So now let's run through basic concepts of our state library

Store (aka state object)

Store is just an object for storing values that has initial value. It should be unextensible to prevent random mutations. For more safety you can even freeze the state.

{
  "todos": [ { "id": 0, "text": "Write an article" } ]
}

Action

Action is an object with type and the action itself. Action contains some data that is used by reducer. Example:

{
  "type": "do_something",
  "action": getData()
}

Reducer

Reducer is a thing that accepts an object (aka "action"), checks the type of an action and returns a new store. Usually an action object has type to identify the type of action and the value that will be used in a reducer. I know this sounds confusing, let's break it down.

We pass it to a function called dispatch which applies the reducer. Dispatching means just calling the reducer with the store and action, like this:

dispatch(action) => reducer(action) => check action type and apply some stuff depending on type

where dispatch is calling reducer as a function.

Reducer regularly looks like this:

switch (action.type) {
  case 'do_something':
    doSomething([...state, action.action])
}

Listeners

Listeners are the things that watch changes in the store. So listeners are called in dispatcher. This is something like useEffect in React. When you subscribe to some state, you add a listener.

Subscribing

It adds a new listener.

Implementing

Store

Here's a thing called "initial state". Here we define all our properties. We won't be able to add new ones later so our state becomes immutable at some point.

let state = Object.freeze({ items: [] }), listeners = []

Here we freeze our state not to be able to redefine items property. It is optional, and if you want to use regular properties, e.g. not arrays, just switch to Object.preventExtensions so you'll be able to modify properties but not add new ones.

We also define an array of listeners, it will be used in dispatch and subscribe.

Dispatch

This little function applies the action to store and triggers listeners. We can also filter some properties not to trigger them on every dispatch but for simplicity we'll just trigger on every dispatch:

// Apply state change and trigger subscribers
const dispatch = action => {
    state = reducer(state, action)      
    listeners.map(lst => lst())
}

Subscribe

Subscring to global state is easy as heck, just push a new listener into listeners array and that's it:

// Subscribe to state changes
const subscribe = listener => listeners.push(listener)

And... that's it! Our tiny Redux clone is ready.

Building a TODO

We'll build a simple TODO app (yeah super original). TODO list will be a bunch of objects like { id: 4321, item: 'item' } for simplicity.

HTML

Nothing magical here:

<!DOCTYPE html>
<html>
    <body>
        <h1>Pure JavaScript State Management</h1>
        <h2>State object</h2>
        <pre>
{
  "items": []
}
        </pre>
        <input placeholder="Need something to be done?" />
        <button onclick="addTodo()">Add</button>
         <ul></ul>
    </body>
</html>

Helpers

Not to write documentGetElementBySomeLongSelector we will define some helper functions:

const create = el => document.createElement(el)
const get = s => document.getElementsByTagName(s)[0]

Reducer

First, we need to define the reducer. This will have two actions - one will return a new store without an item, e.g. delete it, another one will add new one. Every new item will have an id. This simplifies finding the item when we need to delete it. This can be something like:

// Handler for state changes
const reducer = (state, action) => {
    switch (action.type) {
        case 'REMOVE_TODO':
        // Return new items without a selected item
        return { items: state.items.filter(el => el.id !== action.id) }
        // Add new item
        case 'ADD_TODO':
        return { items: [...state.items, {
            item: action.item,
            id: parseInt(Math.random() * 10**8)
        }] }
    }
}

Adding todos

We will define an function that dispatches an action. It is a handler for a function that we defined in an HTML page.

// Event handler for button
const addTodo = () => dispatch({
    type: 'ADD_TODO',
    item: get('input').value
})

It reads input value and puts it into action.items.

Listing and removing todos

Here we run through existing items and display them and also attach a dispatcher to each item.

// Each time new todo item is created this subscription is triggered
subscribe(() => {
    const app = get('ul')
    const newApp = create('ul')

    for (let i of state.items) {

        const li = create('li')
        li.innerText = i.item
        const btn = create('button')
        btn.innerText = 'x'

        // Remove item from list
        btn.onclick = () => dispatch({
            type: 'REMOVE_TODO',
            id: i.id
        })

        li.appendChild(btn)

        newApp.appendChild(li)
    }

    // Update DOM
    document.body.replaceChild(newApp, app)
    // Output full object state
    get('pre').innerText = JSON.stringify(state, null, 2)
    // Clear input
    get('input').value = null
})

Conclusion

Redux and other similar state libraries are more simple than they seem to be, especially when you build your own :). I hope this guide was helpful. Of course Redux has tons of features our clone doesn't have but it is for simplicity sake.

Posted on by:

talentlessguy profile

v 1 r t l

@talentlessguy

16yo nullstack dev, OSSer ⚡, expert in nothing

Discussion

pic
Editor guide