{"id":13503641,"url":"https://github.com/clearwater-rb/grand_central","last_synced_at":"2025-04-23T00:14:35.448Z","repository":{"id":56875102,"uuid":"41946744","full_name":"clearwater-rb/grand_central","owner":"clearwater-rb","description":"State-management and action-dispatching for Ruby apps","archived":false,"fork":false,"pushed_at":"2018-04-08T04:26:54.000Z","size":66,"stargazers_count":23,"open_issues_count":6,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-23T00:14:13.247Z","etag":null,"topics":["functional","immutable","isomorphic","ruby","state-management","universal"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/clearwater-rb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-09-05T02:55:59.000Z","updated_at":"2024-10-23T18:49:58.000Z","dependencies_parsed_at":"2022-08-20T22:00:43.549Z","dependency_job_id":null,"html_url":"https://github.com/clearwater-rb/grand_central","commit_stats":null,"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clearwater-rb%2Fgrand_central","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clearwater-rb%2Fgrand_central/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clearwater-rb%2Fgrand_central/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clearwater-rb%2Fgrand_central/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/clearwater-rb","download_url":"https://codeload.github.com/clearwater-rb/grand_central/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250343957,"owners_count":21415041,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["functional","immutable","isomorphic","ruby","state-management","universal"],"created_at":"2024-07-31T23:00:42.095Z","updated_at":"2025-04-23T00:14:35.425Z","avatar_url":"https://github.com/clearwater-rb.png","language":"Ruby","funding_links":[],"categories":["Uncategorized"],"sub_categories":["Uncategorized"],"readme":"# GrandCentral\n\nGrandCentral is a state-management and action-dispatching library for Ruby apps. It was created with [Clearwater](https://github.com/clearwater-rb/clearwater) apps in mind, but there's no reason you couldn't use it with other types of Ruby apps.\n\nGrandCentral is based on ideas similar to [Redux](http://rackt.github.io/redux/). You have a central store that holds all your state. This state is updated via a handler block when you dispatch actions to the store.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'grand_central'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install grand_central\n\n## Usage\n\nFirst, you'll need a store. You'll need to seed it with initial state and give it a handler block:\n\n```ruby\nrequire 'grand_central'\n\nstore = GrandCentral::Store.new(a: 1, b: 2) do |state, action|\n  case action\n  when :a\n    # Notice we aren't updating the state in-place. We are returning a new\n    # value for it by passing a new value for the :a key\n    state.merge a: state[:a] + 1\n  when :b\n    state.merge b: state[:b] + 1\n  else # Always return the given state if you aren't updating it.\n    state\n  end\nend\n\nstore.dispatch :a\nstore.dispatch :b\nstore.dispatch \"You can dispatch anything you want, really\"\n```\n\n### Actions\n\nThe actions you dispatch to the store can be anything. We used symbols in the above example, but GrandCentral also provides a class called `GrandCentral::Action` to help you set up your actions:\n\n```ruby\nmodule Actions\n  include GrandCentral\n\n  # The :todo becomes a method on the action, similar to a Struct\n  AddTodo    = Action.with_attributes(:todo)\n  DeleteTodo = Action.with_attributes(:todo)\n  ToggleTodo = Action.with_attributes(:todo) do\n    # We don't want to toggle the Todo in place. We want a new instance of it.\n    def toggled_todo\n      Todo.new(\n        id: todo.id,\n        name: todo.name,\n        complete: !todo.complete?\n      )\n    end\n  end\nend\n```\n\nThen your handler can use these actions to update the state more easily:\n\n```ruby\nstore = GrandCentral::Store.new(todos: []) do |state, action|\n  case action\n  when Actions::AddTodo\n    state.merge todos: state[:todos] + [action.todo]\n  when Actions::DeleteTodo\n    state.merge todos: state[:todos] - [action.todo]\n  when Actions::ToggleTodo\n    state.merge todos: state[:todos].map { |todo|\n      # We want to replace the todo in the array\n      if todo.id == action.todo.id\n        action.todo\n      else\n        todo\n      end\n    }\n  else\n    state\n  end\nend\n```\n\n### Dispatching actions without the store\n\nThere may be parts of your app that need to dispatch actions but you don't want them to know about the store itself. In a [Clearwater](https://github.com/clearwater-rb/clearwater) app, for example, your components may need to fire off actions, but you don't want them to know how to get things from the store.\n\nSubclasses of `GrandCentral::Action` have a `store` attribute where you can set the default store to dispatch to:\n\n```ruby\nstore = GrandCentral::Store.new(todos: []) do |state, action|\n  # ...\nend\n\nMyAction = GrandCentral::Action.with_attributes(:a, :b, :c)\nMyAction.store = store\n```\n\nTo use this default store, you can send the `call` message to the action class itself:\n\n```ruby\nMyAction.call(1, 2, 3) # these arguments correspond to the a, b, and c attributes above\n```\n\n#### Action Currying\n\nYou can [curry](https://en.wikipedia.org/wiki/Currying) an action with the `[]` operator instead of `call`. There is a minor difference between action currying and traditional function currying, though: function currying automatically invokes the function once all of the arguments are received, whereas currying a `GrandCentral::Action` will curry until you explicitly invoke it with `call`:\n\n```ruby\nSetAttribute = Action.with_attributes(:attr, :value)\n\n# Returns an action type based on `SetAttribute` with `attr` hard-\n# coded to `:name`. When you invoke this new action type, you only\n# need to provide the value.\nSetName = SetAttribute[:name]\n\n# This action is a further specialization where we know both the\n# attribute *and* the value. You don't need to provide any values.\nClearName = SetName[nil]\n```\n\nThese are all equivalent action invocations:\n\n```ruby\nSetAttribute.call :name, nil\nSetAttribute[:name].call nil\nSetAttribute[:name, nil].call\nSetName.call nil\nClearName.call\n```\n\n#### Using with Clearwater\n\nBecause of action currying, we can set action types as event handlers in Clearwater components. GrandCentral even knows how to handle `Bowser::Event` objects whose targets are instances of `Bowser::Element` (Clearwater uses Bowser as its DOM abstraction). So if you set an action to handle toggling a checkbox, the last argument will contain the input's `checked` property; if you use it for a text box, the last argument will be the `value` property. It'll even prevent `form` submission for you:\n\n```ruby\nAddTodo = Action.with_attributes(:description)\nSetNewTodoDescription = Action.with_attributes(:description)\nToggleTodo = Action.with_attributes(:todo, :complete)\nDeleteTodo = Action.with_attributes(:todo)\n\nclass TodoList\n  include Clearwater::Component\n  \n  def initialize(todos, new_description)\n    @todos = todos\n    @new_description = new_description\n  end\n  \n  def render\n    div([\n      # All arguments are provided to the AddTodo action, but we still delay its invocation\n      form({ onsubmit: AddTodo[@new_description] }, [\n        # We omit the description from SetNewTodoDescription - it's inferred from the input event\n        input(oninput: SetNewTodoDescription, value: @new_description),\n        button('Add'),\n      ]),\n      ul(todos.map { |todo|\n        li([\n          input(type: :checkbox, onchange: ToggleTodo[todo]),\n          todo.description,\n          button({ onclick: DeleteTodo[todo] }, '✖️'),\n        ])\n      }),\n    ])\n  end\n```\n\n### Performing actions on dispatch\n\nYou may want your application to do something in response to a dispatch. For example, in a Clearwater app, you might want to re-render the application when the store's state has changed:\n\n```ruby\nstore = GrandCentral::Store.new(todos: []) do |state, action|\n  # ...\nend\n\napp = Clearwater::Application.new(component: Layout.new)\n\n# on_dispatch yields the state before and after the dispatch as well as the action\nstore.on_dispatch do |before, after, action|\n  app.render unless before.equal?(after)\nend\n```\n\nNotice the `unless before.equal?(after)` clause. This is one of the reasons we recommend you update state by returning a new value instead of mutating it in-place. It allows you to do cache invalidation in O(1) time.\n\nUsing `on_dispatch` also useful if you want to consolidate all of your side effects:\n\n```ruby\nFetchUsers = Action.create do\n  def request\n    Bowser::HTTP.fetch('/api/users')\n  end\nend\nLoadUsers = Action.with_attributes(:json) do\n  def users\n    json[:users].map { |attrs| User.new(attrs) }\n  end\nend\n\nstore.on_dispatch do |before, after, action|\n  case action\n  when FetchUsers\n    action.request\n      .then(\u0026:json)\n      .then(\u0026LoadUsers)\n  end\nend\n```\n\n## Models\n\nWe can use the `GrandCentral::Model` base class to store our objects:\n\n```ruby\nclass Person \u003c GrandCentral::Model\n  attributes(\n    :id,\n    :name,\n    :location,\n  )\nend\n```\n\nThis will set up a `Person` class we can instantiate with a hash of attributes:\n\n```ruby\njamie = Person.new(name: 'Jamie')\n```\n\n### Immutable Models\n\nThe attributes of a model cannot be modified once set. That is, there's no way to say `person.name = 'Foo'`. If you need to change the attributes of a model, there's a method called `update` that returns a new instance of the model with the specified attributes:\n\n```ruby\njamie = Person.new(name: 'Jamie')\nupdated_jamie = jamie.update(location: 'Baltimore')\n\njamie.location         # =\u003e nil\nupdated_jamie.location # =\u003e \"Baltimore\"\n```\n\nThis allows you to use the `update` method in your store's handler without mutating the original reference:\n\n```ruby\nstore = GrandCentral::Store.new(person) do |person, action|\n  case action\n  when ChangeLocation\n    person.update(location: action.location)\n  else person\n  end\nend\n```\n\nThis keeps each version of your app state intact if you need to roll back to a previous version. In fact, the app state itself can be a `GrandCentral::Model`:\n\n```ruby\nclass AppState \u003c GrandCentral::Model\n  attributes(\n    :todos,\n    :people,\n  )\nend\n\ninitial_state = AppState.new(\n  todos: [],\n  people: [],\n)\n\nstore = GrandCentral::Store.new(initial_state) do |state, action|\n  case action\n  when AddPerson\n    state.update(people: state.people + [action.person])\n  when DeleteTodo\n    state.update(todos: state.todos - [action.todo])\n\n  else\n    state\n  end\nend\n```\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/clearwater-rb/grand_central. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclearwater-rb%2Fgrand_central","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fclearwater-rb%2Fgrand_central","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclearwater-rb%2Fgrand_central/lists"}