Skip to content
Switch branches/tags

Latest commit


Git stats


Failed to load latest commit information.
Latest commit message
Commit time


The Zinke gem implements the Reducer pattern in Ruby, as seen in JavaScript libraries like React and languages like Elm. This provides a Store that serves as a single, stable source of truth for stateful applications.

It defines the following concepts:

  • Stores - Encapsulates a state and dispatches and subscribes to actions.
  • Reducers - Mechanism for updating a state based on dispatched actions.


The Reducer pattern is better known in the front-end world, and underpins the Redux library for JavaScript applications and state management in Elm projects. By encapsulating the state in a store and using reducers to manage changes to the state, you decouple the state from its consumers. This provides several advantages:

  • Control: You define when and how the state can change.
  • Consistency: Well-defined states and transitions provide a consistent interface for consumers to dispatch and subscribe to updates.
  • Testability: Unit test states and transitions without worrying about consumers, and test state consumers with their expected states and transitions without a complicated setup of the initial state.

See also:

  • Redux: A popular JavaScript library implementing the reducer pattern. The Redux documentation was heavily referenced while developing Zinke.
  • Elm: A functional language that compiles to JavaScript.


Cuprum is tested against Ruby (MRI) 2.5.


Method and class documentation is available courtesy of RubyDoc.

Documentation is generated using YARD, and can be generated locally using the yard gem.


Copyright (c) 2018 Rob Smith

Cuprum is released under the MIT License.


The canonical repository for this gem is located at

To report a bug or submit a feature request, please use the Issue Tracker.

To contribute code, please fork the repository, make the desired updates, and then provide a Pull Request. Pull requests must include appropriate tests for consideration, and all code must be properly formatted.


Hi, I'm Rob Smith, a Ruby Engineer and the developer of this library. I use these tools every day, but they're not just written for me. If you find this project helpful in your own work, or if you have any questions, suggestions or critiques, please feel free to get in touch! I can be reached on GitHub or via email. I look forward to hearing from you!



require 'zinke/store'

Stores are the core feature of Zinke. Each store encapsulates a state and provides methods to dispatch and subscribe to updates to that state.

require 'hamster'
require 'zinke/state'

class BankStore < Zinke::Store
  def balance(account_id)
    index = account_index(account_id)



  def account_index(account_id)
    state.get(:accounts).find_index { |account| account[:id] == account_id }

initial_state = {
  accounts: [
      id: 0,
      balance: 500.0
      id: 1,
      balance: 1_500.0
      id: 2,
      balance: -25.0
immutable_state =
bank_store      =

#=> Hamster::Hash[accounts: [{ id: 0, balance: 500.0 }, { ... }, { ... }]]
#=> 1500.0

Notice that in this example we are using the Hamster gem to make our initial state immutable. Zinke does not have a dependency on Hamster or any specific immutability library, nor does it enforce that the state must be immutable. It is, however, very strongly recommended.

Initial State

If is called with nil or with no arguments, it will initialize the store with the default state, which is an empty hash. To override this behavior, redefine the private #initial_state method on your Store subclass.

class BankStore < Zinke::Store

  def initial_state
    Hamster::Hash[accounts: []]

empty_store =
#=> Hamster::Hash[accounts: []]

Even if an initial state is defined, passing a non-nil value will set the state of the store to the passed value.

Dispatching Actions

Updates to the state are handled using the Store#dispatch method, which takes a single argument. This argument is the action, which is traditionally a Hash with a :type key and optionally other keys and values representing additional data.

# This action will deposit $50 into the account with id 0.
deposit_action = { type: :deposit, account_id: 0, balance: 50.0 }

# This action will withdraw $25 from the account with id 1.
withdrawal_action = { type: :withdraw, account_id: 0, balance: 25.0 }

Before we dispatch these actions, though, we need to subscribe to the :deposit and :withdraw events.

Subscribing to Actions

Now that we have our store set up, we need to keep an eye on it. Zinke::Store defines the #subscribe method for this purpose. Continuing our example above:

class BankStore
  def initialize(initial_state)

    # Whenever the store dispatches a :deposit action, we'll call the #deposit
    # method and set the state to the new value. The id of the account and the
    # amount to deposit are dispatched as part of the action.
    subscribe(:deposit) do |action|
      self.state = deposit(action[:account_id], action[:amount])

    subscribe(:withdraw) do |action|
      self.state = withdraw(action[:account_id], action[:amount])


  # Our deposit method does the arithmetic and returns a new state with the
  # updated balance of the appropriate account. Because we are using an
  # immutable state, there is no danger of accidentally changing the previous # state or any references to it.
  def deposit(id, amount)
    index   = account_index(id)
    balance = state.get(:accounts).get(index).get(:balance)

    state.put_at(:accounts, index, :balance) { balance + amount }

  def withdraw(account_id, amount)
    index   = account_index(id)
    balance = state.get(:accounts).get(index).get(:balance)

    state.put_at(:accounts, index, :balance) { balance - amount }

audit_log = []

# We want to keep track of all changes to the accounts, so we use the
# #subscribe method with no action name. All dispatched actions will be added
# to our audit log, regardless of the action name.
bank_store.subscribe do |action|
  audit_log << action

#=> Hamster::Hash[accounts: [{ id: 0, balance: 550.0 }, { ... }, { ... }]]
#=> 550.0

#=> 1475.0

#=> [
#     { type: :deposit, account_id: 0, balance: 50.0 },
#     { type: :withdraw, account_id: 0, balance: 25.0 }
#   ]

This logic can be better handled with Reducers (see below), but the underlying implementation is based on the Store#subscribe method.

Unsubscribing From Actions

You can unsubscribe from actions by storing a reference to the listener object.

def watch_for_fraud(action)
  raise 'withdrawal too large' if action[:amount] > 25.0

listener =
  bank_store.subscribe(:withdraw) do |action|

# Sometime later...

# Does not raise the error.
bank_store.dispatch(type: :withdraw, account_id: 0, amount: 50.0)


It's possible to model state changes using Store#dispatch and #subscribe, but adding a Reducer provides a simpler mechanism for handling changes to the state, in addition to the benefits of better-organized code.

To define a reducer, create a Module and include Zinke::Reducer. Then to use your new reducer, just include it in your Store class.

Let's revisit our sample application and define a reducer for it.

module BankReducer
  include Zinke::Reducer

  update :deposit, :handle_deposit

  update :withdraw, :handle_withdrawal

  update :transfer do |state, action|
    state = handle_deposit(
        account_id: action[:to_account_id],
        amount: action[:amount]

        account_id: action[:from_account_id],
        amount: action[:amount]


  def handle_deposit(state, action)
    account, index = find_account_with_index(action[:account_id])
    new_balance    = account.get(:balance) + action[:amount]

    state.put_at(:accounts, index, :balance) { new_balance }

  def handle_withdrawal(state, action)
    account, index = find_account_with_index(action[:account_id])
    new_balance    = account.get(:balance) - action[:amount]

    raise 'insufficient funds' if new_balance < 0

    state.put_at(:accounts, index, :balance) { new_balance }

class BankStoreWithReducers < Zinke::Store
  include BankReducer

All of our business logic has been refactored from the store to our new reducer. The ::update class method in our reducer takes the place of manually #subscribe-ing to actions, and also handles updating the state. Each update just returns the new state, and the reducer handles the rest automatically.

Because we are using pure reducers with no side effects (see below) and an immutable state object, we're protected from some possible errors in our business logic. For example, in our :transfer update, we are depositing the amount before the "insufficient funds" error is raised - but since :transfer will not return a state, that change is never reflected in the Store. In effect, raising an error results in a free rollback of any changes in a pure reducer.

Each Store can include multiple reducers. If more than one reducer handles a particular action type, then each update will be called in sequence with the state returned by the previous update (and, of course, the action).

Pure Reducers

Let's take a closer look at what a pure reducer is. In a nutshell, in a pure reducer each ::update handler must be a pure function with no side effects. A pure function always has the same behavior given the same inputs, does not mutate the inputs, and does not read from or write to anything outside of the function.

That means the update cannot:

  • Access the file system (read or write files).
  • Access a data store (such as a SQL database or MongoDB document store).
  • Access external services (including logging or system instrumentation).
  • Call any methods that are not also pure functions (for example, is not a pure function, because it does not always return the same value with the same inputs).

For logging and instrumentation, use the Store#subscribe method. For accessing other systems or services, that should be handled by the code that is dispatching actions. In other words, treat your store as a composed object that is used by your application code, rather than embedding external references in your store or reducer.


In our above examples, we used symbols to define our actions. This is fine for a small application, but as a project grows it can lead to issues, especially as more concepts are added to the domain. If two different parts of your application try and use a :query action for two different things, you have some debugging ahead of you.

There is also the problem of typos. What happens when you dispatch a :deposlt action? Nothing, because your reducer is looking for :deposit. A similar issue arises if an action name changes.

Thus, the recommendation is to use scoped strings - for example, 'users.balance.query' and 'admin.transactions.query'. To resolve the issue of changes or typos, use constant values. For example, in our bank application:

# frozen_string_literal: true

module BankActions
  DEPOSIT  = 'bank.deposit'
  WITHDRAW = 'bank.withdraw'
  TRANSFER = 'bank.transfer'

Thus, our reducer would look like this:

module BankReducerWithActions
  update BankActions::DEPOSIT, :handle_deposit

bank_store.dispatch(type: BankActions::DEPOSIT, account_id: 1, amount: 50.0)

Action Creators

Taking things one step further, we can add a function to build our actions.

# frozen_string_literal: true

module BankActions
  DEPOSIT = 'bank.deposit'

  def self.deposit(id:, amount:)
      type:       DEPOSIT,
      account_id: id,
      amount:     amount

bank_store.dispatch(BankActions.deposit(id: 1, amount: 50.0))


An implementation of the Reducer pattern in Ruby.







No releases published


No packages published