{"id":19010399,"url":"https://github.com/instacart/redux-rails","last_synced_at":"2026-03-11T13:18:20.915Z","repository":{"id":33008147,"uuid":"79502822","full_name":"instacart/redux-rails","owner":"instacart","description":"Redux and your server talking without fuss.","archived":false,"fork":false,"pushed_at":"2023-07-12T02:39:04.000Z","size":2412,"stargazers_count":59,"open_issues_count":30,"forks_count":12,"subscribers_count":127,"default_branch":"master","last_synced_at":"2025-10-03T16:42:30.763Z","etag":null,"topics":["api","javascript","rails","redux","redux-rails","rest","state-management"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/instacart.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-01-19T22:44:54.000Z","updated_at":"2025-08-27T12:11:18.000Z","dependencies_parsed_at":"2024-10-29T12:52:31.347Z","dependency_job_id":null,"html_url":"https://github.com/instacart/redux-rails","commit_stats":{"total_commits":108,"total_committers":11,"mean_commits":9.818181818181818,"dds":"0.19444444444444442","last_synced_commit":"7db4d1c7d9235a0e3275b773324484b4ee0b28b7"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/instacart/redux-rails","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/instacart%2Fredux-rails","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/instacart%2Fredux-rails/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/instacart%2Fredux-rails/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/instacart%2Fredux-rails/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/instacart","download_url":"https://codeload.github.com/instacart/redux-rails/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/instacart%2Fredux-rails/sbom","scorecard":{"id":489932,"data":{"date":"2025-08-11","repo":{"name":"github.com/instacart/redux-rails","commit":"7db4d1c7d9235a0e3275b773324484b4ee0b28b7"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3,"checks":[{"name":"Code-Review","score":2,"reason":"Found 7/28 approved changesets -- score normalized to 2","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/test.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:11: update your workflow using https://app.stepsecurity.io/secureworkflow/instacart/redux-rails/test.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/instacart/redux-rails/test.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:19: update your workflow using https://app.stepsecurity.io/secureworkflow/instacart/redux-rails/test.yml/master?enable=pin","Info:   0 out of   3 GitHub-owned GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 14 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":0,"reason":"80 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-6chw-6frg-f759","Warn: Project is vulnerable to: GHSA-v88g-cgmw-v5xw","Warn: Project is vulnerable to: GHSA-93q8-gq69-wqmw","Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92","Warn: Project is vulnerable to: GHSA-qwcr-r2fm-qrc7","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-x9w5-v3q2-3rhw","Warn: Project is vulnerable to: GHSA-pxg6-pf52-xh8x","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c","Warn: Project is vulnerable to: GHSA-w573-4hg7-7wgq","Warn: Project is vulnerable to: GHSA-hr2v-3952-633q","Warn: Project is vulnerable to: GHSA-ff7x-qrg7-qggm","Warn: Project is vulnerable to: GHSA-vh7m-p724-62c2","Warn: Project is vulnerable to: GHSA-r9p9-mrjm-926w","Warn: Project is vulnerable to: GHSA-434g-2637-qmqr","Warn: Project is vulnerable to: GHSA-49q7-c7j4-3p7m","Warn: Project is vulnerable to: GHSA-977x-g7h5-7qgw","Warn: Project is vulnerable to: GHSA-f7q4-pwc6-w24p","Warn: Project is vulnerable to: GHSA-fc9h-whq2-v747","Warn: Project is vulnerable to: GHSA-vjh7-7g9h-fjfh","Warn: Project is vulnerable to: GHSA-rv95-896h-c2vc","Warn: Project is vulnerable to: GHSA-qw6h-vgh9-j6wx","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-4q6p-r6v2-jvc5","Warn: Project is vulnerable to: GHSA-pfrx-2q88-qq97","Warn: Project is vulnerable to: GHSA-qqgx-2p2h-9c37","Warn: Project is vulnerable to: GHSA-896r-f27r-55mw","Warn: Project is vulnerable to: GHSA-9c47-m6qq-7p4h","Warn: Project is vulnerable to: GHSA-6c8f-qphg-qjgp","Warn: Project is vulnerable to: GHSA-76p3-8jx3-jpfq","Warn: Project is vulnerable to: GHSA-3rfm-jhwj-7488","Warn: Project is vulnerable to: GHSA-hhq3-ff78-jv3g","Warn: Project is vulnerable to: GHSA-jf85-cpcp-j695","Warn: Project is vulnerable to: GHSA-p6mc-m468-83gw","Warn: Project is vulnerable to: GHSA-29mw-wpgm-hmr9","Warn: Project is vulnerable to: GHSA-35jh-r3h4-6jhm","Warn: Project is vulnerable to: GHSA-4xc9-xhrj-v574","Warn: Project is vulnerable to: GHSA-x5rq-j2xg-h7qm","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-vh95-rmgr-6w4m","Warn: Project is vulnerable to: GHSA-xvch-5gv4-984h","Warn: Project is vulnerable to: GHSA-mwcw-c2x4-8c55","Warn: Project is vulnerable to: GHSA-76c9-3jph-rj3q","Warn: Project is vulnerable to: GHSA-9wv6-86v2-598j","Warn: Project is vulnerable to: GHSA-rhx6-c78j-4q9w","Warn: Project is vulnerable to: GHSA-g6ww-v8xp-vmwg","Warn: Project is vulnerable to: GHSA-h7cp-r72f-jxh6","Warn: Project is vulnerable to: GHSA-v62p-rq8g-8h59","Warn: Project is vulnerable to: GHSA-hrpp-h998-j3pp","Warn: Project is vulnerable to: GHSA-p8p7-x288-28g6","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-m6fv-jmcg-4jfg","Warn: Project is vulnerable to: GHSA-hxcc-f52p-wc94","Warn: Project is vulnerable to: GHSA-cm22-4g7w-348p","Warn: Project is vulnerable to: GHSA-vx3p-948g-6vhq","Warn: Project is vulnerable to: GHSA-3jfq-g458-7qm9","Warn: Project is vulnerable to: GHSA-r628-mhmh-qjhw","Warn: Project is vulnerable to: GHSA-9r2w-394v-53qc","Warn: Project is vulnerable to: GHSA-5955-9wpr-37jh","Warn: Project is vulnerable to: GHSA-qq89-hq3f-393p","Warn: Project is vulnerable to: GHSA-f5x3-32g6-xq36","Warn: Project is vulnerable to: GHSA-4wf5-vphf-c2xc","Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3","Warn: Project is vulnerable to: GHSA-c4w7-xm78-47vh","Warn: Project is vulnerable to: GHSA-p9pc-299p-vxgp","Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-phwq-j96m-2c2q","Warn: Project is vulnerable to: GHSA-ghr5-ch3p-vcr6","Warn: Project is vulnerable to: GHSA-765h-qjxv-5f44","Warn: Project is vulnerable to: GHSA-f2jv-r9rf-7988","Warn: Project is vulnerable to: GHSA-43f8-2h32-f4cj","Warn: Project is vulnerable to: GHSA-r683-j2x4-v87g","Warn: Project is vulnerable to: GHSA-5fw9-fq32-wv5p","Warn: Project is vulnerable to: GHSA-jgrx-mgxx-jf9v","Warn: Project is vulnerable to: GHSA-j8xg-fqg3-53r7","Warn: Project is vulnerable to: GHSA-6fc8-4gx4-v693","Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-19T18:49:33.919Z","repository_id":33008147,"created_at":"2025-08-19T18:49:33.919Z","updated_at":"2025-08-19T18:49:33.919Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30382669,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-11T12:49:11.341Z","status":"ssl_error","status_checked_at":"2026-03-11T12:46:41.342Z","response_time":84,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["api","javascript","rails","redux","redux-rails","rest","state-management"],"created_at":"2024-11-08T19:11:04.824Z","updated_at":"2026-03-11T13:18:20.877Z","avatar_url":"https://github.com/instacart.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"Redux Rails\n=========================\n\u003cstrong\u003eRedux and your server talking without fuss.\u003c/strong\u003e\n\n[![npm](https://img.shields.io/npm/v/redux-rails.svg?style=flat-square)](https://www.npmjs.com/package/redux-rails)  [![license](https://img.shields.io/npm/l/redux-rails.svg?style=flat-square)](https://github.com/instacart/redux-rails/blob/master/LICENSE)  [![CircleCI](https://circleci.com/gh/instacart/redux-rails.svg?style=shield\u0026circle-token=dfe00f3d2f89c4b810250f6d18b384df3ab1bc6b)](https://circleci.com/gh/instacart/redux-rails) [![Maintainability](https://api.codeclimate.com/v1/badges/577b533d91a0f56fa728/maintainability)](https://codeclimate.com/github/instacart/redux-rails/maintainability)\n\nRedux Rails is a Redux middleware for auto-generating the actions, reducers and settings for talking to your RESTful backend. It removes boilerplate and keeps your app consistent.\n\n## Features\n- Simple, pre-defined Redux actions\n- Automated loading state, including loading errors\n- Promise based actions without additional thunk middleware\n- Optionally disabled, optimistic updates\n- Optional response parsing\n- Sane defaults, highly customizable\n- Auto-generated, extendable reducers for each resource\n- And more!\n\n## How is it done?\n\u003e 1. Create your config\n\u003e 2. Create your Redux store\n\u003e 3. Use provided actions to talk with your backend api\n\u003e 4. Access your api results in your Redux state\n\nYou create a config object that lays out your backend resources. The config roughly matches a Rails routes file, but **a Rails backend is not a requirement**. You then hand this config to the Redux Rails middleware and assign your config to a reducer creator. Redux Rails then gives you specific actions for fetching, updating, creating and deleting these resources. You also get handy metadata related to the resources' loading states. It also keeps an internal queue to ensure methods are executed in order, per resource.\n\n\n## Basic Usage\n\n```js\n// Import necessary tools\n\nimport { createStore, applyMiddleware, compose } from 'redux'\nimport { middleWare, apiReducer, railsActions } from 'redux-rails'\n\n// Set up your config\n\nconst apiConfig = {\n  baseUrl: 'https://your-site-url.com/api/',\n  resources: {\n    Posts: {\n      controller: 'posts'\n    },\n    User: {\n      controller: 'user'\n    }\n  }\n}\n\n// Create your Redux store\n\nconst App = createStore(\n  {\n    resources: apiReducer(apiConfig) // auto-generates reducers\n  },\n  {},\n  compose(\n    applyMiddleware(middleWare(apiConfig))\n  )\n)\n\n// Fetch your resources\n\nApp.dispatch(railsActions.show({\n  resource: 'Posts',\n  id: 3\n}))\n\nApp.dispatch(railsActions.show({resource: 'User'}))\n\n// ...wait for server response\n\n// Use your fetched resources\n\nApp.getState().resources.Posts.models.map(m =\u003e console.log(m))\nconsole.log(App.getState().resources.User)\n\n// OR - use the returned promise to wait for call to server to complete\nApp.dispatch(railsActions.show({resource: 'User'}))\n  .then(repsonse =\u003e {\n    console.log(response)\n  })\n\n```\n\n## Available Rails Actions\n\n### index\n\nFetch list of members from a resource collection.\n```js\nApp.dispatch(railsActions.index({\n  resource: 'Posts',\n  queryParams: {\n    q: 'React best practices',\n    page: 2\n  }\n}))\n```\n\nHTTP GET on resources. Example: `GET http://my-site/myapi/posts`\n\n\n### show\n\nFetch a specific member from a resource collection\n```js\nApp.dispatch(railsActions.show({\n  resource: 'Posts',\n  id: 3\n}))\n```\nHTTP GET on specific member in resources. Example: `GET http://my-site/myapi/posts/3`\n\n### update\n\nUpdate a specific member of a resource collection\n```js\nApp.dispatch(railsActions.update({\n  resource: 'Posts',\n  id: 3,\n  attributes: {\n    title: 'foo',\n    body: 'bar'\n  }\n}))\n```\n\nHTTP PUT on specific member in resources. Example: `PUT http://my-site/myapi/posts/3`\n\n### create\n\nCreate a new member in a resource collection\n```js\nApp.dispatch(railsActions.create({\n  resource: 'Posts',\n  attributes: {\n    title: 'foo',\n    body: 'bar'\n  }\n}))\n```\n\nHTTP POST on resources. Example: `POST http://my-site/myapi/posts`\n\n### destroy\n\nDestroy a specific member inside a resource collection\n```js\nApp.dispatch(railsActions.destroy({\n  resource: 'Posts',\n  id: 3\n}))\n```\n\nHTTP DELETE on specific member in resources. Example: `DELETE http://my-site/myapi/posts/3`\n\n### HTTP Method to Redux Actions Mappings\n\n\n| Redux Rails action | HTTP Method  |\n| -----------------  | -----------  |\n| index              | GET          |\n| show               | GET          |\n| create             | POST         |\n| update             | PUT          |\n| destroy            | DELETE       |\n\n### Singular vs Plural Resources\nA resource can be singluar (user) or plural (users). This is determined by the name of the controller for your resource.\n\n```js\nconst apiConfig = {\n  baseUrl: 'https://your-site-url.com/api/',\n  resources: {\n    User: {\n      controller: 'user' // singluar\n    },\n    Users: {\n      controller: 'users' // plural\n    },\n    Comment: {\n      controller: 'comments' // plural\n    },\n    Cats: {\n      controller: 'cat' // singular\n    }\n  }\n}\n```\n\nSingular resources do not use or require an id in the actions, nor is an id added to the constructed url.\n\n```js\n  App.dispatch(railsActions.show({resource: 'user'}))\n  // GET https://your-site-url.com/api/user\n```\n\nWith the exception of `create`, plural resources require a member id, and the id is added to the constructed url.\n\n```js\n  App.dispatch(railsActions.show({resource: 'users', id: 123}))\n  // GET https://your-site-url.com/api/users/123\n```\n\nWith user(s) as an example resource, and `https://your-site-url.com/api/` as your baseUrl, your url structures would look like this for different rails actions.\n\n| Resource Controller | Redux Rails action | HTTP Method  | controller URL |\n| ------------------- | ------------------ | -----------  | -------------- |\n| user                | index              | GET          | `https://your-site-url.com/api/user`\n| users               | index              | GET          | `https://your-site-url.com/api/users`\n| user                | show               | GET          | `https://your-site-url.com/api/user`\n| users               | show               | GET          | `https://your-site-url.com/api/users/\u003cmemberId\u003e`\n| user                | create             | POST         | `https://your-site-url.com/api/user`\n| users               | create             | POST         | `https://your-site-url.com/api/users`\n| user                | update             | PUT          | `https://your-site-url.com/api/user`\n| users               | update             | PUT          | `https://your-site-url.com/api/users/\u003cmemberId\u003e`\n| user                | destroy            | DELETE       | `https://your-site-url.com/api/user`\n| users               | destroy            | DELETE       | `https://your-site-url.com/api/users/\u003cmemberId\u003e`\n\n\n## Structure of Redux models and collections\nYour backend resources, like users, comments, posts, etc, are put into `models` and `collections`. Which are just objects and arrays, respectively. Collections and models both have metadata (loading state, id, etc). Models have an attributes object, which is set to the response from the server for that model.\n\n```js\n  Posts: {\n    loading: false,\n    loaingError: undefined,\n    models: [\n      {\n        loading: false,\n        loaingError: undefined,\n        id: 123,\n        attributes:{\n          id: 123,\n          name: 'Dominic'\n        }\n      },\n      {...},\n      {...}\n    ]\n  }\n```\n\n**Full exmaple:**\n\n```js\nimport { createStore, applyMiddleware, compose } from 'redux'\nimport { middleWare, apiReducer, railsActions } from 'redux-rails'\n\n  const config = {\n    baseUrl: 'https://your-site-url.com/api/',\n    resources: {\n      User: {\n        controller: 'user' // singular resource\n      },\n      Users: {\n        controller: 'users' // plural resource\n      }\n    }\n  }\n\n  const App = createStore(\n  {\n    resources: apiReducer(config)\n  },\n  {},\n  compose(\n    applyMiddleware(middleWare(apiConfig))\n  )\n)\nApp.dispatch(railsActions.show(resource: 'User')) // fetch user resource\nApp.dispatch(railsActions.index(resource: 'Users')) // fetch users resource\n\nApp.getState()\n/*\n  After calls finish the redux state would be:\n  {\n    resources: {\n      User: {\n        queryParams: {\n          verbose: false\n        },\n        loading: false,\n        loadingError: undefined,\n        attributes: {\n          first_name: 'Leia',\n          last_name: 'Organa',\n          title: 'General'\n        }\n      },\n      Users: {\n      queryParams: {\n          q: 'rebels'\n        },\n        loading: false,\n        loadingError: undefined,\n        models: [\n          {\n            loading: false,\n            loadingError: undefined,\n            id: 123,\n            attributes: {\n              id: 123,\n              first_name: 'Leia',\n              last_name: 'Organa',\n              title: 'General'\n            }\n          },\n          {\n            loading: false,\n            loadingError: undefined,\n            id: 124,\n            attributes: {\n              id: 124,\n              first_name: 'Han',\n              last_name: 'Solo',\n              title: 'Smuggler'\n            }\n          },\n          {\n            loading: false,\n            loadingError: undefined,\n            id: 125,\n            attributes: {\n              id: 125,\n              first_name: 'Luke',\n              last_name: 'Skywalker',\n              title: 'Commander'\n            }\n          }\n        ]\n      }\n    }\n  }\n*/\n\n```\n### Model\nModels are always an object with metadata and an attributes object:\n```js\n{\n  loading: false,\n  loadingError: undefined,\n  id: 123,\n  attributes: {\n    id: 123,\n    first_name: 'Leia',\n    last_name: 'Organa',\n    title: 'General'\n  }\n}\n```\n\n### Collection\nCollections are an object with metadata and an array of models.\n```js\nUsers: {\n  queryParams: {\n    q: 'rebels'\n  },\n  loading: false,\n  loadingError: undefined,\n  models: [\n    {\n      loading: false,\n      loadingError: undefined,\n      id: 123,\n      attributes: {\n        id: 123,\n        first_name: 'Leia',\n        last_name: 'Organa',\n        title: 'General'\n      }\n    },\n    {\n      loading: false,\n      loadingError: undefined,\n      id: 124,\n      attributes: {\n        id: 124,\n        first_name: 'Han',\n        last_name: 'Solo',\n        title: 'Smuggler'\n      }\n    },\n    {\n      loading: false,\n      loadingError: undefined,\n      id: 125,\n      attributes: {\n        id: 125,\n        first_name: 'Luke',\n        last_name: 'Skywalker',\n        title: 'Commander'\n      }\n    }\n  ]\n}\n```\n\nSingular resources, and models within a collection, can be updated, destroyed, or refetched using `railsActions`.\n\n```js\n  App.dispatch(railsActions.update({\n    resource: 'Users',\n    id: 124,\n    attributes: {\n      title: 'Captain'\n    }\n  }))\n\n  /*\n  after loading finished, the model would be updated to:\n  {\n    loading: false,\n    loadingError: undefined,\n    id: 124,\n    attributes: {\n      id: 124,\n      first_name: 'Han',\n      last_name: 'Solo',\n      title: 'Captain'\n    }\n  }\n  */\n\n```\n\n### Metadata\nModels and collections each get a few pieces of metadata. Some are optional and some are always around.\n- **loading** - true while any rails action is currently awaiting a response from the server.\n- **loadingError** - any error that occurred during the last rails action. Cleared on subsequent actions.\n- **id (optional)** - the id of the model/resource member.\n- **cId (optional)** - used for internal purposes only.\n- **queryParams** - the query params for the current corresponding model or collection.\n\nYou can also add your own arbitray metadata to collections. This is useful for things like paginating a collection. See the `setMetadata` config setting for more info.\n\n## Optimistic Updates\nUpdate and Create actions assume immediate success on the client, by default. If the server returns an error, this data is automatically reverted on the client for you. A loading error will be set on the model. This option can be disabled per config or per resource with the `optimisticUpdateEnabled` attribute.\n\nWhile an optimistic update is in-flight, previous data is stored in `__prevData`. Using this data directly is not advised, as the structure and permanence of it cannot be guaranteed. Consistently using `attributes` on your models is a far more reliable way to access your data.\n\n## Usage with React-Redux\n\nFirst, set up your Redux Rails config, set up your apiReducer and apply the Redux Rails middleware\n\n```js\nimport React, { Component } from 'react'\nimport PropTypes from 'prop-types'\nimport { connect } from 'react-redux'\nimport { railsActions } from 'redux-rails'\n\nclass MyReactComponent extends Component {\n  static propTypes = {\n    fetchPosts: PropTypes.func,\n    loading: PropTypes.bool,\n    posts: PropTypes.array\n  }\n\n  componentWillMount() {\n    this.props.fetchPosts()\n  }\n\n  render() {\n    if (this.props.loading) { return \u003cp\u003eLoading Posts...\u003c/p\u003e }\n\n    return (\n      \u003cul\u003e\n        {this.props.posts.map(post) =\u003e {\n          let currentPost = post.attributes\n          return (\n            \u003cli key={`${post.id`}}\u003e\n              \u003ch2\u003e{currentPost.title}\u003c/h2\u003e\n              \u003cp\u003e{currentPost.body}\u003c/p\u003e\n            \u003c/li\u003e\n          )\n        }}\n      \u003c/ul\u003e\n    )\n  }\n}\n\nconst mapStateToProps = (state) =\u003e ({\n  posts: state.resources.Posts.models,\n  loading: state.resources.Posts.loading\n})\n\nconst mapDispatchToProps = {\n  fetchPosts: () =\u003e { railsActions.index({ resource: 'Posts'}) }\n}\n\nexport default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent)\n```\n\n\n## Config in depth\nThe Redux Rails config has many options. You can also use several configs along with several instances of the apiReducer to distribute your resources throughout your redux store's hierarchy.\n\nA complex example of a Redux Rails config:\n```js\nconst apiConfig = {\n  baseUrl: 'https://your-site-url.com/api/',\n  resources: {\n    Posts: {\n      controller: 'posts',\n      parse: {\n        member: resp =\u003e resp.posts,\n        collection: (resp) =\u003e {\n          return {\n            foo: 'bar',\n            response: resp.posts\n          }\n        }\n      },\n      setMetadata: (resp) =\u003e {\n        const { pagination } = resp\n        return { pagination }\n      },\n      idAttribute: '_id',\n      baseUrl: 'https://your-OTHER-site-url.com/api/',\n      optimisticUpdateEnabled: false,\n      queryParams: {\n        deleted: false\n      }\n    },\n    User: {\n      controller: 'user',\n      disableFetchQueueing: true,\n      reducer: (state={}, action) =\u003e {\n        switch(action.type) {\n          case 'User.SOME_CUSTOM_ACTION': {\n            return Object.assign({}, state, {\n              myPersonalStateChange: 'I can add on to the Redux Rails default reducer!'\n            })\n          }\n          default: {\n            return state\n          }\n        }\n      }\n    }\n  },\n  fetchParams: {\n    headers: {\n      'content-type':'application/json'\n    },\n    queryParams: {\n      foo: 'I am the fallback query string for GET requests!'\n    }\n  }\n}\n```\n\n### baseUrl\nThis is the top level url for all resources in the config. If your site's api is entirely under `https://your-site-url.com/api/`, ex. `https://your-site-url.com/api/posts`, then this should be the only baseUrl setting you need.\n\nIf you need a different baseUrl per resource, ex. `https://your-site-url.com/api/posts` and `https://your-OTHER-site-url.com/api/comments`, you can set a specific baseUrl per resource that needs it. Any resource without a baseUrl setting will fallback to the top-level baseUrl setting.\n\nA baseUrl is also not required at all, if you'd rather pass your resource's url on every railsAction call. The baseUrl can also be overriden per action, if needed. Just pass it in with your `railsAction`.\n\n### resources\nThis is a mapping of your Rails resources. Resources can be plural (ex: posts) or singular (ex: post). Each resource has a list of optional attributes and one required attribute (controller)\n\n**example\n```js\nconst apiConfig = {\n  baseUrl: 'https://your-site-url.com/api/',\n  resources: {\n    Posts: {\n      controller: 'posts',\n      parse: resp =\u003e resp.posts.map(p =\u003e p.title)\n    },\n    Comments: {\n      controller: 'comments',\n      idAttribute: '_id',\n      baseUrl: 'https://your-OTHER-site-url.com/api/'\n    },\n    User: {\n      controller: 'user',\n      fetchParams: {\n        headers: {\n          'content-type':'application/json'\n        }\n      }\n    }\n  }\n}\n```\n\n\n### controller (required)\nThe controller attribute tells Redux Rails what specific url to make HTTP actions against. For example, the resource `Posts` could be found at `https://your-site-url.com/api/posts`. This would make the baseUrl for this resource `https://your-site-url.com/api/` and the controller `posts`. The controller does not need to match the name of the resource, though this is generally good practice for a RESTful api. If you're using Rails, the controller set here should probably match the one set in your routes file.\n\n### parse (optional)\nThis can be either a single function or an object with two functions, `member` and `collection`. These functions are used to parse the response from your api, and its where you should do any data transformation before the data is added to your Redux store. `member` is used for responses related to a specific model and `collection` is used for responses to `index` calls.\n\nExample of a single function for all resource types:\n```js\nconst apiConfig = {\n  baseUrl: 'https://your-site-url.com/api/',\n  resources: {\n    Posts: {\n      controller: 'posts',\n      parse: (resp) =\u003e {\n        return {\n          foo: 'bar',\n          response: resp.posts\n        }\n      }\n    }\n  }\n}\n```\n\nExample of a function for each resource type:\n```js\nconst apiConfig = {\n  baseUrl: 'https://your-site-url.com/api/',\n  resources: {\n    Posts: {\n      controller: 'posts',\n      parse: {\n        member: resp =\u003e resp.posts,\n        collection: (resp) =\u003e {\n          return {\n            foo: 'bar',\n            response: resp.posts\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n\u003e Note: Index calls will automatically look for and use an array at the top-level key matching the name of your resource, if the response it receives is not an array.\n\n```js\nApp.dispatch(railsActions.index({resource: 'Posts'}))\n\n// if the response returns an array, this array will be used directly in the redux store, as expected.\n// response: [ {}, {}, {} ]\nApp.getState().resources.Posts // =\u003e { loading: false, models: [ {}, {}, {} ] }\n\n// if the response returns an object with an array under a key matching the resource name, that will be used\n// response: { Posts: [ {}, {}, {} ] }\nApp.getState().resources.Posts // =\u003e { loading: false, models: [ {}, {}, {} ] }\n\n// the resource name is not case sensitive here, so you can name your resource `Posts` but still use the key `posts`\n// response: { posts: [ {}, {}, {} ] }\n\nApp.getState().resources.Posts // =\u003e { loading: false, models: [ {}, {}, {} ] }\n\n// if it does not find an array in any of these cases, it will set an error\n// response: { posts: {...} }\nApp.getState().resources.Posts // =\u003e { loading: false, loadingError: 'Bad data received from server. INDEX calls expect an array.', models: [] }\n```\n### setMetadata (optional)\nThe setMetadata option is a function which receives your raw JSON response as an argument, and should return an object. That object will be set at the top level of your model or collection along with the other metadata, such as loading state. This is great for things like pagination data.\n\nExample of a setMetadata function\n```js\nconst apiConfig = {\n  baseUrl: 'https://your-site-url.com/api/',\n  resources: {\n    Posts: {\n      controller: 'posts',\n      paginated: true,\n      setMetadata: (resp) =\u003e {\n        const { pagination } = resp.meta\n        return { pagination }\n      }\n    }\n  }\n}\n```\n\nThis will put the data on the resp.meta.pagination object into the `posts` collection's metadata.\n\nLater...\n```js\nApp.dispatch(railsActions.index({resource: 'Posts'})).then(() =\u003e {\n  App.getState().resources.Posts // =\u003e { loading: false, models: [ {}, {}, {} ], pagination: { ... } }\n})\n```\n\n### paginated\nBy default, redux rails will empty out a collection when an `INDEX` action is used on a resource. The `paginated` option tells Redux Rails to persist the models in a collection between calls. New models are simply added to the collection, and any duplicate models will be overwritten by the new model (de-duped by `id`).\n\nExample of paginated use\n```js\nconst apiConfig = {\n  baseUrl: 'https://your-site-url.com/api/',\n  resources: {\n    Posts: {\n      controller: 'posts',\n      paginated: true,\n      setMetadata: (resp) =\u003e {\n        const { pagination } = resp.meta\n        return { pagination }\n      }\n    }\n  }\n}\n\nApp.getState().resources.Posts // =\u003e { loading: false, models: [ { id: 1, attriubtes: {} }  } ] }\n\nApp.dispatch(railsActions.index({resource: 'Posts', queryParam: { page: 2 } })).then(() =\u003e {\n  App.getState().resources.Posts // =\u003e { loading: false, models: [ { id: 1, attriubtes: {} }, { id: 2, attriubtes: {}  }, { id: 3, attriubtes: {}  } ], pagination: { ... } }\n})\n```\n\n### idAttribute (optional - default: id)\nThis is defaulted to `id` and tells Redux Rails which attribute on your resource is the unique identifier. If your api assigns ids to the attribute `_@@id`, for example, you would set `idAttribute` to `_@@id` for that specific resource or for all resources in the config. Models still get `id` set as metadata no matter the `idAttribute` setting.\n\nExample with `idAttribute` set to `_@@id`:\n```js\n{\n  loading: false,\n  loadingError: undefined,\n  id: 123,\n  attributes: {\n    `_@@id`: 123,\n    first_name: 'Leia',\n    last_name: 'Organa',\n    title: 'General'\n  }\n}\n```\n\n### baseUrl (optional)\nUrl base for your resource(s). If you'd like a different baseUrl for a specific resource, you can set `baseUrl` on the resource level as well. If you're using multiple configs, each config can have a top-level baseUrl.\n\n### Nested Resources\nFor actions with a rails route like `/v3/carts/:id/cart_total` where we need to access a nested action, we can use the wildcard ID matcher `/:id/` to let the middleware know this request is nested and interpolate the id appropriately. Here is an example configuration.\n\n```js\nconst ApiConfig = {\n  baseUrl: 'http://www.kibbles.com/',\n  resources: {\n    DogFriends: {\n      controller: 'dogs/:id/friends'\n    }\n  }\n}\n```\n\n### optimisticUpdateEnabled (optional - default: true)\nOptimistic updates are on by default, but can be disabled per resource or per config using the `optimisticUpdateEnabled` attribute. Disabling optimistic updates will tell Redux Rails to wait for a successful server response before updating or creating models on the client. This can lead to a less responsive feeling app for users, but client state will exactly match the known state of the model on the server.\n\n### fetchParams\nThese are the options sent to the `Fetch` call when making any call to your api. These map directly to the options available in the [Fetch Request object](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request).\n\nThis is where you would set your headers or credentials, for example. This can be set per resource or top-level. Each config, if you're using multiple configs, can also have their own settings.\n\n### queryParams\nThis attribute will be translated to the appropriate query string for GET requests.\nQuery params can be set globally at the top level of the apiConfig, per resource, or per call.\nValid values are primitives and arrays. Array values will be translated to the form `keyname[]=element0\u0026keyname[]=element1`.\n\n```\nqueryParams: {\n  q: 'foobar',\n  page: 2,\n  categories: [3, 4]\n}\n\n// '?q=foobar\u0026page=2\u0026categories[]=3\u0026categories[]=4'\n```\n\n### disableFetchQueueing\nFetch queueing can be disabled per config or per resource. Resources with fetch queueing disabled will not execute actions in the order they were called, but will instead execute actions in the order they are received from the server. *This makes your app susceptible to race conditions.* For example, if a user edits a posts and then edits it again very quickly, the first edit may return from the server after the second, giving the user a false representation of the post's state on the server.\n\n*It's highly recommended that fetch queueing remain enabled unless you are very aware of the consequences.*\n\n### reducer\nIf you'd like to add additional functionality to your resources, you can pass in a reducer per resource. For example, if you have a resource, `comments`, on the server and you want all of the default functionality given to you through Redux Rails but you also want a way to set some client-side state:\n\n```js\nconst apiConfig = {\n  baseUrl: 'https://your-site-url.com/api/',\n  resources: {\n    Comments: {\n      controller: 'comments',\n      reducer: (state={}, action) =\u003e {\n        switch(action.type) {\n          case 'Comments.MY_OWN_ACTION': {\n            return Object.assign({}, state, {\n              myAttribute: true\n            })\n          }\n          default: {\n            return state\n          }\n        }\n      }\n    }\n  }\n}\n```\n\nOf course, you can also create a large, richly defined reducer and simply import it and pass it along. Your own reducer may be as large or small as you need it to be.\n\n\n```js\nimport myCommentsReducer from 'my/comments/reducer'\n\nconst apiConfig = {\n  baseUrl: 'https://your-site-url.com/api/',\n  resources: {\n    Comments: {\n      controller: 'comments',\n      reducer: myCommentsReducer\n    }\n  }\n}\n```\n\nThe same rules that apply to a regular Reducer apply to the reducer here ie. it must return state as a default case.\n\n### Using multiple configs\nMultiple configs can be used throughout your Redux store's hierarchy. Use `combineConfigs` to do this.\n\n```js\nimport { createStore, applyMiddleware } from 'redux'\nimport { middleWare, apiReducer, combineConfigs } from 'redux-rails'\n\n// these settings will be the fallback for all configs\nconst defaultConfig = {\n  baseUrl: 'https://your-site-url.com/api/',\n  fetchParams: {\n    headers: {\n      'content-type':'application/json'\n    }\n  }\n}\n\nconst postsConfig = {\n  resources: {\n    Posts: {\n      controller: 'posts'\n    }\n  }\n}\n\nconst commentsConfig = {\n  baseUrl: 'https://your-comments-site-url.com/api/',\n  resources: {\n    Comments: {\n      controller: 'comments'\n    }\n  }\n}\n\nconst reduxRailsConfigs = combineConfigs(\n  defaultConfig\n  postsConfig,\n  commentsConfig\n)\n\nconst App = createStore(\n  combineReducers({\n    posts: apiReducer(postsConfig),\n    comments: apiReducer(commentsConfig)\n  }),\n  {},\n  composeEnhancers(\n    applyMiddleware(middleWare(reduxRailsConfigs))\n  )\n)\n```\n\nThe first config given to `combineConfigs` is used as the default for top-level `baseUrl` and `fetchParams`. These can be overriden per config and per resource.\n\n### Passing config as a function\n\nIf you need to determine some data for your config after your redux app has been instantiated, you can pass a function instead of an object as your config parameter. That function must return a valid config object.\n\nThe function will be passed the current redux store state during the middleware proccesses. This can be useful if you need redux state in your Redux Rails config.\n\u003e Note that the redux store will be an empty object on initialization. Your config function must be able to handle this. See example below.\n\n```js\nimport { createStore, applyMiddleware, compose } from 'redux'\nimport { middleWare, apiReducer } from 'redux-rails'\nimport { myUtil } from 'my/utilities/directory'\n\nconst getMyConfig = (store = {}) =\u003e {\n  return {\n    baseUrl: 'https://your-site-url.com/api/',\n    fetchParams: {\n      headers: {\n        'content-type':'application/json',\n        'csrf-token': store.csrfToken, // get some data from the store\n        'another-header': store.deep \u0026\u0026 store.deep.object.thing, // value in deep object must be ready for init empty store state\n        'some-other-header': myUtil.getSomeData() // no longer hard-coded at initialization\n      }\n    }\n    resources: {\n      Posts: {\n        controller: 'posts'\n      },\n      User: {\n        controller: 'user'\n      }\n    }\n  }\n}\n\n// Create your Redux store just like you would with an object\n\nconst App = createStore(\n  {\n    resources: apiReducer(getMyConfig) // auto-generates reducers\n  },\n  {},\n  compose(\n    applyMiddleware(middleWare(getMyConfig))\n  )\n)\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finstacart%2Fredux-rails","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Finstacart%2Fredux-rails","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finstacart%2Fredux-rails/lists"}