Something Useless — Redux Implemented in Elixir

Because why not?

Steven Nunez
Jun 26 · 8 min read
const initializerAction = {type: "@@INIT"}
const createStore = (reducer, state=undefined) => {
state = reducer(state, initializerAction)
let subscribers = []
return {
getState() {
return state
},
subscribe(subscriber){
subscribers.push(subscriber)
return () => {
subscribers = subscribers.filter(s => s !== subscriber)
}
},
dispatch(action){
state = reducer(state, action)
subscribers.forEach(s => s())
}
}
}

const countReducer = (state=0, action) => {
switch(action.type){
case "INCREMENT":
return state + 1
default:
return state
}
}
const store = createStore(countReducer)
const removeSubscriber = store.subscribe(() => console.log("I got called", store.getState()))
store.dispatch({type: "INCREMENT"})
// Should log "I got called: 1"

removeSubscriber()
store.dispatch({type: "INCREMENT"})
// Should log nothing

Let's dream

{:ok, store} = Store.start_link(CountReducer)

subscriber_ref = Store.subscribe(store, fn (state) ->
IO.puts "I Got called: #{state}"
end)

Store.dispatch(store, %{type: "INCREMENT"})
# Should log "I got called: 1"

Store.remove_subscriber(store, subscriber_ref)
Store.dispatch(store, %{type: "INCREMENT"})
# Should log nothing

Enter the GenServer

defmodule Store do
@initializer_action %{type: "@@INIT"}

def start_link(reducer, initial_state \\ nil) do
GenServer.start_link(__MODULE__, [reducer, initial_state])
end

def get_state(store) do
GenServer.call(store, {:get_state})
end

def init([reducer, initial_state]) do
store_state = apply(reducer, :reduce, [initial_state, @initializer_action])
{:ok, %{reducer: reducer, store_state: store_state}}
end

def handle_call({:get_state}, _from, state) do
{:reply, Map.get(state, :store_state), state}
end
end
defmodule Reducer do
@callback reduce(any, %{type: any}) :: any
end

defmodule CountReducer do
@behaviour Reducer
def reduce(nil, action), do: reduce(0, action)
def reduce(state, action), do: do_reduce(state, action)

defp do_reduce(state, %{type: "INCREMENT"}), do: state + 1
defp do_reduce(state, _), do: state
end

Time to run it

c "e_dux.exs"

{:ok, store} = Store.start_link(CountReducer)
Store.get_state(store) # => 0
defmodule Store do
# ...Existing code...
def dispatch(store, action) do
GenServer.cast(store, {:dispatch, action})
end

# ...Existing code...
def handle_cast({:dispatch, action}, %{reducer: reducer, store_state: store_state} = state) do
store_state = apply(reducer, :reduce, [store_state, action])
{:noreply, Map.put(state, :store_state, store_state)}
end
end
c "e_dux.exs"

{:ok, store} = Store.start_link(CountReducer)
Store.get_state(store) # => 0
Store.dispatch(store, %{type: "INCREMENT"})
Store.get_state(store) # => 1

I'd like to subscribe to your newsletter

{:ok, store} = Store.start_link(CountReducer)

subscriber_ref = Store.subscribe(store, fn (state) ->
IO.puts "I Got called: #{state}"
end)

Store.dispatch(store, %{type: "INCREMENT"})
# Should log "I got called: 1"

Store.remove_subscriber(store, subscriber_ref)
Store.dispatch(store, %{type: "INCREMENT"})
# Should log nothing

init/1

def init([reducer, initial_state]) do
store_state = apply(reducer, :reduce, [initial_state, @initializer_action])
{:ok, %{reducer: reducer, store_state: store_state, subscribers: %{}}} # add new subscribers map to state
end

subscribe/2, remove_subscriber/2, and callbacks

def subscribe(store, subscriber) do
GenServer.call(store, {:subscribe, subscriber})
end

def remove_subscriber(store, ref) do
GenServer.cast(store, {:remove_subscriber, ref})
end

def handle_call({:subscribe, subscriber}, _from, %{subscribers: subscribers} = state) do
ref = make_ref()
{:reply, ref, put_in(state, [:subscribers, ref], subscriber)}
end

def handle_cast({:remove_subscriber, ref}, %{subscribers: subscribers} = state) do
subscribers = Map.delete(subscribers, ref)
{:noreply, Map.put(state, :subscribers, subscribers)}
end

dispatch/2

def handle_cast({:dispatch, action}, %{reducer: reducer, store_state: store_state, subscribers: subscribers} = state) do
store_state = apply(reducer, :reduce, [store_state, action])
for {_ref, sub} <- subscribers, do: sub.(store_state) # notify those subscribers
{:noreply, Map.put(state, :store_state, store_state)}
end
{:ok, store} = Store.start_link(CountReducer)

subscriber_ref = Store.subscribe(store, fn (state) ->
IO.puts "I Got called: #{state}"
end)

Store.dispatch(store, %{type: "INCREMENT"})
# Should log "I got called: 1"

Store.remove_subscriber(store, subscriber_ref)
Store.dispatch(store, %{type: "INCREMENT"})
# Should log nothing

Combined Reducers

const rootReducer = combineReducers({
count: countReducer,
square: squareReducer
})
const store = createStore(rootReducer)
store.getState() // {count: 0, square: 2} // default to 2 so we can get some good squaring action
store.dispatch({type: “INCREMENT”})
store.getState() // {count: 1, square: 4}
const combineReducers = (reducerMap) => {
return (state, action) => {
return Object.keys(reducerMap).reduce((map, stateName) => {
map[stateName] = reducerMap[stateName](state[stateName], action)
return map
}, {})
}
}

const countReducer = (state=0, action) => {
switch (action.type) {
case "INCREMENT":
return state + 1
default:
return state
}
}

const squareReducer = (state=2, action) => {
switch (action.type) {
case "INCREMENT":
return state * state
default:
return state
}
}

const rootReducer = combineReducers({count: countReducer, square: squareReducer})
rootReducer({count: 0, square: 2}, {type: "INCREMENT"}) // {count: 1, square: 4}

CombineReducer Module

# New stuff!
def init([reducer_map, nil]) when is_map(reducer_map), do: init([reducer_map, %{}])
def init([reducer_map, initial_state]) when is_map(reducer_map) do
store_state = CombineReducers.reduce(reducer_map, initial_state, @initializer_action)
{:ok, %{reducer: reducer_map, store_state: store_state, subscribers: %{}}} # add new subscribers map to state
end

# Crusty old code
def init([reducer, initial_state]) do
store_state = apply(reducer, :reduce, [initial_state, @initializer_action])
{:ok, %{reducer: reducer, store_state: store_state, subscribers: %{}}} # add new subscribers map to state
end
# New hotness
def handle_cast({:dispatch, action}, %{reducer: reducer_map, store_state: store_state, subscribers: subscribers} = state) when is_map(reducer_map) do
store_state = CombineReducers.reduce(reducer_map, store_state, action)
for {_ref, sub} <- subscribers, do: sub.(store_state) # notify those subscribers
{:noreply, Map.put(state, :store_state, store_state)}
end

# Old and crust
def handle_cast({:dispatch, action}, %{reducer: reducer, store_state: store_state, subscribers: subscribers} = state) do
store_state = apply(reducer, :reduce, [store_state, action])
for {_ref, sub} <- subscribers, do: sub.(store_state) # notify those subscribers
{:noreply, Map.put(state, :store_state, store_state)}
end
defmodule CombineReducers do
def reduce(reducers, state, action) do
for {state_name, reducer} <- reducers do
Task.async(fn () ->
{state_name, apply(reducer, :reduce, [state[state_name], action])}
end)
end
|> Enum.map(&Task.await/1)
|> Enum.into(%{})
end
end
defmodule SquareReducer do
@behaviour Reducer
def reduce(nil, action), do: reduce(2, action)
def reduce(state, action), do: do_reduce(state, action)

defp do_reduce(state, %{type: "INCREMENT"}), do: state * 1
defp do_reduce(state, _), do: state
end
{:ok, store} = Store.start_link(%{count: CountReducer, square: SquareReducer})Store.dispatch(store, %{type: “INCREMENT”})
Store.get_state(store)

Closing up



Footer top
Footer bottom

Flatiron Labs

We're the technology team at The Flatiron School (a WeWork company). Together, we're building a global campus for lifelong learners focused on positive impact.

Thanks to Crystal Chang.

Steven Nunez

Written by

Flatiron Labs

We're the technology team at The Flatiron School (a WeWork company). Together, we're building a global campus for lifelong learners focused on positive impact.