Skip to content

Instantly share code, notes, and snippets.

@Nevon
Last active December 4, 2016 19:29
Show Gist options
  • Save Nevon/eada09788b10b6a1a02949ec486dc3ce to your computer and use it in GitHub Desktop.
Save Nevon/eada09788b10b6a1a02949ec486dc3ce to your computer and use it in GitHub Desktop.

When coming from large frameworks like Backbone, Ember or KnockoutJS, as many of us do, it's easy to get used to the fact that the framework provides a lot of "magic". The abstractions they provide, while extremely useful, are often so opaque that even if you wanted to, understanding how they are implemented is a hard task.

Because of this, it's no surprise that we expect the same thing from Redux. We see methods like combineReducers and concepts like action creators and assume that they are complicated, magical beasts that cannot be understood by mortal folk.

The intention of this post is to refute that by taking a look at the central concept of Redux - the store.

The problem

Every solution has a problem that it attempts to solve. Redux's problem is that application state is often spread out and complicated to reason about. The solution it provides is what it calls a state container - the store.

The solution

So the job of the store is to contain the state of the application. Given a simple application that just keeps a counter of how many times you've clicked a button, the state could be described as:

{
    counter: 0
}

If we were building a state container for this application, we could start like this:

const createStore = () => ({
    counter: 0
})

This store will do the job perfectly. We invoke it once to create our store, then we feed this state into a function to turn it into a DOM representation (Hello React!). Every time we click the button, we just increment the count property on the store and re-render with the new state.

However, this store is obviously very specific to our counter application, so let's make it a little more generic:

const createStore = (initialState = {}) => {
    let state = initialState

    return () => ({
        initialState
    })
}

Now we can create a store for any application!

const store = createStore({
    counter: 0
})

Changing the state

We now have something to hold our state, but we don't really have a great way to change it. In essence, we're just getting back a plain object that we can mutate, but if we do, we lose the history of what has happened, and there's not a good way to let other parts of the application know that something has changed.

So let's come up with a way to change the state in such a way that we can solve those problems:

const createStore = (reducer, initialState = {}) => {
    let state = initialState

    return () => {
        state = reducer(state)

        // @TODO: Notify listeners that something has changed

        return state
    }
}

Now when we create our store, we can pass in a function that will modify the state for us. In our clicker application, we would pass in a function that will increment the counter:

const increment = (state) => ({
    ...state,
    counter: state.counter + 1
})

const clickerStore = createStore(increment, {
    counter: 0
})

clickerStore() // { counter: 1 }
clickerStore() // { counter: 2 }
clickerStore() // { counter: 3 }

In this example, we only have the action of clicking to worry about, but let's say that we wanted to have two buttons - one for incrementing and one for decrementing. Then we could generalize this further by passing in an action that would describe if we want to increment or decrement.

const createStore = (reducer, initialState = {}) => {
    let state = initialState

    return (action) => {
        state = reducer(action, state)

        // @TODO: Notify listeners that something has changed

        return state
    }
}

const reducer = (action, state) => {
    if (action === 'increment') {
        return {
            ...state,
            counter: state.counter + 1
        }
    } else if (action === 'decrement') {
        return {
            ...state,
            counter: state.counter - 1
        }
    }
}

const store = createStore(reducer, {
    counter: 0
})

store('increment') // { counter: 1 }
store('decrement') // { counter: 0 }

This is great! Now we can pass in a function that will do arbitrary changes to the state. Using this we can represent pretty much anything that can happen in an application. If we wanted to do some more complex operations, we could just replace our action string with an object containing more data, for example what key you pressed when changing an input field.

If you haven't realized yet, this is exactly why Redux actions have the form: { type: 'input', payload: { key: 69 } }. That way you can use the type field to determine how to treat the action, and within the payload you can pass arbitrary values. Be sure to note, however, that there is absolutely nothing special about this format. It's just a convention ([https://github.com/acdlite/flux-standard-action](Flux Standard Actions)), but there's nothing stopping you from using whatever format you want - just like we did with our strings here.

What we've created here is a reducer. It is just a function that receives the previous state and an action, and returns a new state.

Why is it called a reducer?

If we think about what it does, it takes a number of actions and reduces them to a final state. I mean in a very literal manner. Consider the following:

const deposit = (amount) => ({ type: 'deposit', payload: { amount } })
const withdraw = (amount) => ({ type: 'withdraw', payload: { amount } })
const transactions = [deposit(100), deposit(500), withdraw(300)]

const account = {
    balance: 0
}

const reducer = (state, { type, payload }) => {
    switch (type) {
        case 'deposit':
            return {
                ...state,
                balance: state.balance + payload.amount
            }
        case 'withdraw':
            return {
                ...state,
                balance: state.balance - payload.amount
            }
    }
}

transactions.reduce(reducer, account) // { balance: 300 }

This comes from a concept known as Event Sourcing. Essentially, we get the current state by processing everything that happened from the beginning of time. This is how banking has been done for thousands of years (ledgers, for example, are just records of transactions. If you want to know your balance at any given time, you just process the transactions).

This turns out to be a really good fit for UI as well. We can say that the UI is a representation of the state as a reduction over all actions. What was previously missing in order to enable this was a declarative UI where we could pass in our state and get back a UI representation. Once React entered the scene, that enabled this pattern, which is why it's become so successful.

Subscribing to changes

Now we have a "state container" where we can pass in a function that figures out how to change the state based on some action. If the purpose was just to keep the state, then we would be done at this point.

In fact, I've done exactly this in an application where I needed to keep track of the last value passed into a function in order to merge it with the next value. I used the Redux store pattern for that, even though it wasn't a Redux application.

However, if we want to be able to re-render a UI based on the changes in the state, then we need a way to know when something changed. Thankfully, every single change in our application goes through the store, so there's a very handy place for it (see the @TODO in the store examples above).

So all we really need is a way to subscribe for changes, as well as a way to get the current state.

const createStore = (reducer, initialState = {}) => {
    let state = initialState
    let listeners = []

    const dispatch = (action) => {
        state = reducer(action, state)

        // Notify our listeners that the state has changed
        listeners.forEach((listener) => {
            listener()
        })

        return state
    }

    const subscribe = (listener) => {
        listeners.push(listener)

        // Allow listeners to unsubscribe as well
        return function unsubscribe () {
            var index = listeners.indexOf(listener)
            listeners.splice(index, 1)
        }
    }

    const getState = () => state

    return {
        dispatch,
        subscribe,
        getState
    }
}

Now we had to change the interface to our store a little bit. Before, it was just a function that you would call with an action and it would return the new state. Now that we need to expose a few more functions, we can group them together into an object.

subscribe let's you register functions to be called whenever the state has changed. It doesn't tell you what has changed, just that it has. If you want to get the new state, you can just call getState.

dispatch is essentially the same function that we used to return when creating a store. The only difference is that anytime it's called, we also notify all the listeners that the state has changed.

This is our final implementation of the store! 🎉

Re-rendering the UI

Now we have all that we need to be able to re-render our UI whenever it changes. I'm going to use React here, but remember that there is nothing specific about React or Redux here. It doesn't matter if we want the output to be DOM or JSON or a PNG or whatever, all we are doing is passing the application state into a function that transforms it into some kind of representation.

The UI is just a representation of a reduction over the events. ui = transformation(events.reduce(reducer, initialState))

Back to our application! Given the store implementation above, this is what our app would look like:

Note that this code is likely wrong when it comes to the React specifics, but the general concepts should be correct.

import { Component } from 'react'
import ReactDOM, { h1, button } from 'react-dom'

const reducer = (action, state) => {
    if (action === 'increment') {
        return {
            ...state,
            counter: state.counter + 1
        }
    } else if (action === 'decrement') {
        return {
            ...state,
            counter: state.counter - 1
        }
    }
}

const store = createStore(reducer, {
    counter: 0
})

// Connect the store with a component.
const connect = (component, store) => {
    class Connect extends Component {
        constructor () {
            this.store = store
            this.state = { store.getState() }
        }

        componentDidMount() {
            this.store.subscribe(() => this.handleChange)
        }

        handleChange() {
            this.state = { this.store.getState() }
        }

        render () {
            createElement(
                component,
                {
                    dispatch={this.store.dispatch}
                    getState={this.store.getState}
                }
            )
        }
    }

    return Connect
}

// Our application component
const App = (dispatch, getState) => {
    const { count } = getState()

    const onIncrementClicked = () => {
        dispatch('increment')
    }

    const onDecrementClicked = () => {
        dispatch('decrement')
    }

    return (
        <h1>Count: {count}</h1>
        <button onClick={onIncrementClicked} value="Increment" />
        <button onClick={onDecrementClicked} value="Decrement" />
    )
}

// Render it to the DOM
ReactDOM.render(
    connect(App, store),
    '#clicker-app'
)

The meatiest part of the above code is the connect function. It's fairly React specific, but essentially what it does is that it passes dispatch and getState into the component it wraps (App in this case) as props. It also subscribes to store changes and assigns the store state to it's own internal state. This means that anytime the store state changes, the component will be re-rendered. As it is re-rendered, it gets the new state and thus the DOM is updated with the new values.

This is in fact what react-redux does (albeit in a more robust and flexible manner).

This is not specific to ReactDOM

Like I said before, we can do the exact same thing using any other transformation.

View on JSFiddle

const reducer = (action, state) => {
    if (action === 'increment') {
        return {
            ...state,
            counter: state.counter + 1
        }
    } else if (action === 'decrement') {
        return {
            ...state,
            counter: state.counter - 1
        }
    }
}

const store = createStore(reducer, {
    counter: 0
})

const incrementButton = document.createElement('button')
const decrementButton = document.createElement('button')
const counterOutput = document.createElement('span')
incrementButton.innerText = 'Increment'
decrementButton.innerText = 'Decrement'
counterOutput.innerText = store.getState().counter
incrementButton.addEventListener('click', () => store.dispatch('increment'))
decrementButton.addEventListener('click', () => store.dispatch('decrement'))
store.subscribe(() => {
    const { counter } = store.getState()
    counterOutput.innerText = counter
})
document.body.appendChild(incrementButton)
document.body.appendChild(decrementButton)
document.body.appendChild(counterOutput)

Wrapping up

I hope that this has reduced the feeling that there is some magic going on in Redux. It really is one of the absolute simplest framework (if you could even call it that) that I have ever seen. The code is very small and I pinky-promise that it's readable and understandable without having to be a super genious (as evidenced by yours truly).

Read the Source

Our store implementation here is 32 lines long and actually covers almost all the functionality provided by the redux store. The redux store clocks in at 250 lines, but most of that is actually comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment