Something Useless — Redux Implemented in Elixir

Because why not?

Steven Nunez
Jun 26, 2019 · 8 min read

This post was originally published on the author’s personal blog.

I got a chance to teach Redux recently and what a time! For something so simple, it sure can get complicated. If you take a look at my previous articles, you might notice a technique I use to get my head around concepts. Bring that bad boy on to my own turf.

A naïve implementation of Redux using JavaScript might look something like this:

Cool. So we can build out a store that calls a single reducer. The store can notify subscribers of changes. We'll work on implementing this in Elixir, and we'll even handle the case with combined reducers. Along the way, we'll see where the JavaScript implementation falters, blocking execution of independent reducers and how to get around that in Elixir.

Let's dream

The code of my dreams looks something like:

We swapped out a few things. This is Elixir, so we’re passing in a reference to the Store pid on every invocation. We also changed how we unsubscribe from a store, returning a reference value instead of a function that magically calls in the right scope. Also, we pass the current state to the subscribers on every call. This looks great, but how do we build it?

Enter the GenServer

I love the Erlang lords for creating the GenServer. It works for EVERYTHING.
Let's start by implementing the get_state function on a Store process.

The code above will use the apply function to dispatch a message to a module. If our reducer is a module named CountReducer, it would call CountReducer.reduce(state, action). Pretty nice.

The reducer will be a module with a reduce/2 function. Reducers have a few responsibilities.

  • They provide default values for state on initialization
  • They return a new version of state based on an action being passed
  • They ignore messages it doesn't care for

The Reducers don't care what the state is, all they do is get state, and an action, then respond with new state. Here's a sample reducer:

We first create a behaviour to ensure anyone that plays the Reducer role knows how to do the job. We need it to have a reduce/2 function to support the Store module's expectation. The rest is just some good old fashioned PATTERN MATCHING. The first two reduce/2 heads are just clean up. If we get a nil value, then just call it again with a 0, our do_reduce/2 functions are where the actual reducing happens.

Time to run it

Throw all of that in a file named e_dux.exs and open up iex.

Not too exciting. Let's add the ability to dispatch an action. Back in our Store:

Here we cast asynchronously using GenServer.cast/2. This strategy makes it so our dispatch code won't block our calling code. Imagine if a reducer took a long time to finish. Or better yet, imagine if we have to run through several reducers? This not blocking takes advantage of Elixir's real power: Concurrency. Try this out by loading the file like before and run this:

WOOT! Got some INCREMENT action! Let's move on to adding subscribers and notifying them.

I'd like to subscribe to your newsletter

Back to our dream code:

We want a dispatch to notify our existing subscribers after we've updated our state. We'll also have our store track its subscribers. We'll be updating init/1, as well as adding subscribe/2 and remove_subscriber/2.

init/1

Here we add a new key to our state map: subscribers. We'll be taking advantage of this being a map when we delete subscribers in remove_subscriber/2.

subscribe/2, remove_subscriber/2, and callbacks

Our subscribe/2 function will add our subscriber to our map, storing a ref as the key. Since a ref is a unique value, it's a great way to well... reference stuff. We'll return that ref to our caller. remove_subscriber/2 takes a store pid and a ref. This updates our subscriber map. This is done in a cast since we don't care about the return value.

dispatch/2

FINALLY we update handle_cast/2 for our dispatch/2 function to call every subscriber. With this code, our dreams are ALIVE!

Combined Reducers

So Redux lets you break down complex logic into multiple reducers. Something like:

Internally all a combined reducer is is a reducer that delegates to other reducers. More naive code:

Here we return a function that when called with a state and action will delegate portions of the state to specific reducers. Pretty nice! But something wicked lies in this code. When our reducer dispatches, it can only delegate to one reducer at a time, even though their state is completely isolated. This would be a perfect time for some concurrency! Let's look at how we can handle this in Elixir.

CombineReducer Module

We'll start out with a change to how we treat individual reducers vs a Map with reducers. Above our existing init/1 function, add this:

Converting nil values to an empty map will let our reducers receive nil as their state when we call something like state[:count]. They'll then convert that to their expected default state.

One small change to dispatch/2 to support our reducer potentially being a map instead of a module:

And finally, our brand new module a brand new module to handle our CombineReducers logic.

Ok, dopeness alert. Notice how we're using a task to run this calculation. This means that the calculation for ALL of the reducers will take as long as the slowest one, not the culmination of all of their time. THIS IS REALLY COOL. How are you not excited?!

Let's test it out. First create a SquareReducer:

Then run this!

SO COOL!!!

Closing up

This bit of hacking really deepened my understanding of Redux, how combined reducers worked, and what their limitations were. I also got a chance to work on some interesting Elixir. I don't really have a use for this pattern. It seems like it might be stepping on some of the same pain points you'd solve with GenEvent.

Want to see more of something mentioned here? Leave any ideas in the comments below.

Here's a gist with the code. Happy Clacking.

Thanks for reading! Want to work on a mission-driven team that loves Elixir and experimenting with not-so-useless things? We’re hiring!

Footer top

To learn more about Flatiron School, visit the website, follow us on Facebook and Twitter, and visit us at upcoming events near you.

Flatiron School is a proud member of the WeWork family. Check out our sister technology blogs WeWork Technology and Making Meetup.

Footer bottom

Flatiron Labs

We're the technology team at The Flatiron School (a WeWork…