Understanding Redux store concepts by little Ruby reimplementation

Redux is a popular state management for JavaScript applications these days and at its heart it’s actually just a simple data store. So how is Redux store different? As any other store it saves data, but Redux is different in that it always changes data by dispatching actions to so-called reducers and notifies any subscribed listeners. When we talk about Redux store we need to understand the concepts of reducers, actions and listeners.

Reducer is a fancy name in the Redux world for a pure function taking state and action as arguments, and returning a new state. By saying that a function is pure we mean that the function has no side effects. It is a stateless function that always returns a new value and never modifies the given one (the original state we passed to the function). Let’s look at a common example of a counter reducer that knows how to respond to the increment and decrement actions (with no additional data):

The only thing different from a JavaScript version is using lambdas (anonymous functions) for function definitions and a Ruby Hash to define an action (as opposed to the JavaScript object). State is always passed as an argument, but we are also providing a default (starting counter at zero). Our new counter reducer now responds to increment and decrement actions and returns appropriate new value of the counter. What is also important to notice, is that in case it does not know how to respond to the provided action it simply returns the original state unchanged. This will become important later.

This is how to use our new counter:

> counter.call(2, { type: ‘increment’ })
=> 3
> counter.call(nil, { type: ‘decrement’ })
=> -1

If you haven’t seen any reducers before, you might wonder why the action is defined as a Hash since in our counter example this seems like a redundant thing to do. However, actions can be more complex than that and might need to carry additional information with them. This additional information defining an action would be then saved within the hash under other keys. So an action in a Redux store is actually an object (and technically a Hash in our Ruby example).

Now that we know about reducers and actions let’s define listeners. Listener is exactly what you would expect it to be except that on its own it’s just a function again. A listener function does not have to be pure, it’s just a definition for what happens if a state changes in the store.

Here we define our common listener as lambda again:

counter_listener = -> () { puts “I am counting numbers” }

In our case, we are just printing a string to the standard output, but in the original Redux use-case we would be probably updating the DOM.

Now that we got the basics together and have our counter reducer and listener ready we can implement a basic Ruby-based ReduxStore:

Our new ReduxStore is a general store that can be used to work with any reducer that is passed on its creation. Our initialization process is basically a JavaScript version of createStore:

let createStore(reducer)

createStore is a function creating and returning a store based on the passed reducer. One store always works with only one reducer so the counter reducer would be saving its state in the counter store. After an action is dispatched, all subscribed listeners are notified (called). Here is an example of how to take advantage of our new store:

> my_counter_store = ReduxStore.new(counter_reducer)
> my_counter_store.dispatch({type: ‘increment’})

First, we create our counter store and then we dispatch increment action which changes our current_state in the store. Let’s see how we can take advantage of our listener to carry out some operations when state changes:

> my_counter_store.subscribe(counter_listener)
> my_counter_store.dispatch({type: ‘increment’})
I am counting numbers
> my_counter_store.dispatch({type: ‘decrement’})
I am counting numbers
> puts “Counter is #{my_counter_store.current_state}”
Counter is 3

In just a few lines of code we have a Redux-like store ready! However, there is one more thing about Redux stores that I haven’t mentioned yet. They are essentially used for storing all of our application state and not just for a value returned by one reducer.

Wait! First you said a store always works on top of one reducer. Now you are saying it stores values for more than one?

We don’t need to change how our store works at all and we can save the state from many reducers in one store at the same time. To do this, we have to combine our reducers into one root (app) reducer. This way our store stores and updates a tree-like structure representing values from all our reducers. Do you remember how I wrote that it’s important that a reducer returns unchanged passed state when given an action that it cannot handle? That is exactly how dispatching one action to the root store updates only the value in the tree that should be updated and keeps everything else untouched.

Let’s extend our ReduxStore with a class method that can combine our reducers:

This corresponds to the Redux combineReducers function. It returns another function which is in fact a reducer taking state and action as params and returning a new state again. Only this time across the whole tree (a nested Ruby Hash or JavaScript object).

To see how this works in practice let’s define one more reducer that we will combine with our counter one.

The new todos_reducer keeps a list of TODO items. Notice it takes advantage of the action as an object concept by accepting additional parameter called todo. Let’s create a root_reducer that will combine both our reducers into one:

> root_reducer = ReduxStore.combine_reducers({ counter: counter_reducer, todos: todos_reducer })

Once we have only one reducer again we can create our application store:

> app_store = ReduxStore.new(root_reducer)
> app_store.dispatch({type: ‘increment’})
> app_store.dispatch({type: ‘add’, todo: ‘Buy milk’})
> app_store.dispatch({type: ‘increment’})
> app_store.current_state
=> {:counter=>2, :todos=>["Buy milk"]}

By creating a root reducer we are now saving states for both reducers in a root hash and this is how we build the one and only store with a tree like structure by implementing and combining reducers with actions and listeners.

Hopefully our article sheds some light on the concepts behind Redux for anybody who was interested to know what is Redux store really about.

Like what you read? Give Josef Strzibny a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.