{"id":28474251,"url":"https://github.com/tomat/react-reflorp","last_synced_at":"2025-07-01T10:31:49.507Z","repository":{"id":57343408,"uuid":"55504637","full_name":"tomat/react-reflorp","owner":"tomat","description":"Basically a simple ORM using Refetch, but the data is stored in Redux. Feel free to open an issue if you have a question or feature request.","archived":false,"fork":false,"pushed_at":"2017-09-06T08:30:47.000Z","size":125,"stargazers_count":45,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-06-04T14:22:16.844Z","etag":null,"topics":["decorator","react","redux","refetch"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tomat.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-04-05T12:05:57.000Z","updated_at":"2019-04-26T05:56:00.000Z","dependencies_parsed_at":"2022-09-12T07:00:27.021Z","dependency_job_id":null,"html_url":"https://github.com/tomat/react-reflorp","commit_stats":null,"previous_names":[],"tags_count":43,"template":false,"template_full_name":null,"purl":"pkg:github/tomat/react-reflorp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tomat%2Freact-reflorp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tomat%2Freact-reflorp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tomat%2Freact-reflorp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tomat%2Freact-reflorp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tomat","download_url":"https://codeload.github.com/tomat/react-reflorp/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tomat%2Freact-reflorp/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262944151,"owners_count":23388706,"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":["decorator","react","redux","refetch"],"created_at":"2025-06-07T13:06:29.880Z","updated_at":"2025-07-01T10:31:49.494Z","avatar_url":"https://github.com/tomat.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"React Reflorp\n=========================\n\nBasically a simple ORM using [Refetch](https://github.com/heroku/react-refetch), but the data is stored in [Redux](https://github.com/reactjs/redux).\n\n# Example project and demo\n\nAn example project can be found [here](https://github.com/tomat/reflorp), and the latest build of it should be live at http://reflorp.com\n\n# Installation\n\nRequires **React 0.14 or later** and [react-redux](https://github.com/reactjs/react-redux).\n\n```\nnpm install --save react-reflorp\n```\n\nThis assumes that you’re using [npm](http://npmjs.com/) package manager with a module bundler like [Webpack](http://webpack.github.io) or [Browserify](http://browserify.org/) to consume [CommonJS modules](http://webpack.github.io/docs/commonjs.html).\n\n# Basic example\n\nIn the example code throughout this README we have a simple app with boards and notes. Each board has a number of notes,\nand each note belongs to a single board. Both notes and boards have a unique `id` property, boards have a `title`\n(string) property, and notes have a `summary` (string) property.\n\n```javascript\n// app.js\nconst configuration = {\n  entities: {\n    board: {\n      plural: 'boards',\n    },\n    note: {\n      parent: 'board',\n      plural: 'notes',\n    },\n  },\n  baseUrl: '/api', // is prepended to all URL:s\n};\n```\n\n```javascript\n// Board.js\nimport { reflorp, EntityState, EntityListState } from 'react-reflorp';\n\n@reflorp(({ id }) =\u003e ({\n  // fetch the board data: GET /api/boards/${id}\n  board: { id, load: true },\n}))\nexport default class Board extends Component {\n  static propTypes = {\n    id: PropTypes.string,\n    board: PropTypes.instanceOf(EntityState),\n  };\n\n  render() {\n    const { board } = this.props;\n\n    if (board.pending) {\n      return \u003cdiv\u003eLoading...\u003c/div\u003e;\n    }\n\n    return (\n      \u003cdiv\u003e\n        \u003ch1\u003e{board.value.title}\u003c/h1\u003e\n      \u003c/div\u003e\n    );\n  }\n}\n```\n\n# Setup\n\nFirst, add the `Container` with a configuration object as a child of the react-redux `Provider`:\n\n```javascript\n// app.js\n\nimport { Container as ReflorpContainer } from 'react-reflorp';\n\nconst configuration = {\n  entities: {\n    board: {\n      plural: 'boards',\n    },\n    note: {\n      parent: 'board',\n      plural: 'notes',\n    },\n  },\n  baseUrl: '/api', // is prepended to all URL:s\n};\n\nReactDOM.render(\n  \u003cProvider store={store}\u003e\n    \u003cReflorpContainer configuration={configuration}\u003e\n      \u003cApp /\u003e\n    \u003c/ReflorpContainer\u003e\n  \u003c/Provider\u003e,\n  document.getElementById('app')\n);\n```\n\nThen, add the reducer:\n\n```javascript\n// reducers.js\n\nimport { reducer as reflorp } from 'react-reflorp';\n\nexport default {\n  reflorp,\n};\n```\n\n# Frontend\n\nThe `@reflorp` decorator corresponds to the `@connect` decorator from Refetch.\n\nIt takes a function mapping `props` and `context` to entities, and as a second argument an `options` object.\n\nBy default it just injects the available data from the redux store. With `load: true` it will actually go fetch the data\nfrom the backend, and keep the component informed about that process.\n \nThe actual data and metadata is enclosed inside the `EntityState` and `EntityListState` abstractions, which are\ndescribed further down.\n\n## Example: Loading and displaying\n\n```javascript\n// Board.js\n\nimport React, { PropTypes, Component } from 'react';\nimport { reflorp, EntityState, EntityListState } from 'react-reflorp';\n\n@reflorp(({ id }) =\u003e ({\n  // fetch the board data: GET /api/boards/${id}\n  board: { id, load: true },\n  // fetch the notes belonging to this board: GET /api/boards/${id}/notes\n  notes: {\n    parentId: id,\n    load: true,\n    // Sorts the notes.value property by id before they are sent to our component\n    then: (notes) =\u003e (notes || null) \u0026\u0026 notes.sort((note1, note2) =\u003e note1.id \u003e note2.id),\n  },\n}), { hideUntilLoaded: true })\nexport default class Board extends Component {\n  static propTypes = {\n    id: PropTypes.string,\n    board: PropTypes.instanceOf(EntityState),\n    notes: PropTypes.instanceOf(EntityListState),\n  };\n\n  render() {\n    const { notes, board } = this.props;\n\n    if (notes.pending || board.pending) {\n      return \u003cdiv\u003eLoading...\u003c/div\u003e;\n    }\n\n    return (\n      \u003cdiv\u003e\n        \u003ch1\u003e{board.value.title}\u003c/h1\u003e\n        \u003cul\u003e\n          {notes.value.map((note) =\u003e (\n            \u003cli\u003e{note.summary}\u003c/li\u003e\n          ))}\n        \u003c/ul\u003e\n        {notes.hasMore \u0026\u0026 (\n          \u003cbutton onClick={notes.more}\u003eLoad more notes...\u003c/button\u003e\n        )}\n      \u003c/div\u003e\n    );\n  }\n}\n```\n\n## Example: Creating\n\n```javascript\n// CreateBoard.js\n\nimport React, { PropTypes, Component } from 'react';\nimport { reflorp, EntityState } from 'react-reflorp';\n\n@reflorp(() =\u003e ({\n  board: {\n    create: true,\n    onCreate: (board) =\u003e {\n      const boardId = board.value.id;\n      // Here you may for example redirect to the view board page\n    },\n  },\n}))\nexport default class CreateBoard extends Component {\n  static propTypes = {\n    board: PropTypes.instanceOf(EntityState),\n  };\n\n  render() {\n    const { board } = this.props;\n\n    return (\n      \u003cform\u003e\n        \u003cinput onChange={board.handleChange} type=\"text\" name=\"title\" placeholder=\"Title\" /\u003e\n        \u003cbutton disabled={board.loading} onClick={board.save}\u003eCreate new board\u003c/button\u003e\n        {board.rejected \u0026\u0026 board.error}\n      \u003c/form\u003e\n    );\n  }\n}\n```\n\n## Example: Editing and deleting\n\n```javascript\n// EditNote.js\n\nimport React, { PropTypes, Component } from 'react';\nimport { reflorp, EntityState } from 'react-reflorp';\n\n@reflorp(({ noteId, boardId }) =\u003e ({\n  note: {\n    id: noteId,\n    parentId: boardId,\n    load: true,\n    onDel: () =\u003e {\n      // Callback for when the note has been deleted\n      // Here you may for example redirect back to another view\n    },\n  },\n}))\nexport default class EditNote extends Component {\n  static propTypes = {\n    noteId: PropTypes.string,\n    boardId: PropTypes.string,\n    note: PropTypes.instanceOf(EntityState),\n  };\n\n  render() {\n    const { note } = this.props;\n    \n    if (note.pending) {\n      return \u003cdiv\u003eLoading...\u003c/div\u003e;\n    }\n\n    return (\n      \u003cform\u003e\n        \u003cinput\n          defaultValue={note.value.summary}\n          onChange={note.handleChange}\n          type=\"text\"\n          name=\"summary\"\n          placeholder=\"Summary\"\n        /\u003e\n        \u003cbutton disabled={note.loading} onClick={note.save}\u003eSave note\u003c/button\u003e\n        \u003cbutton disabled={note.loading} onClick={note.del}\u003eDelete note\u003c/button\u003e\n        {note.draft.rejected \u0026\u0026 note.draft.reason}\n      \u003c/form\u003e\n    );\n  }\n}\n```\n\n# API\n\n## Decorator\n\nThe options available for each entity are:\n\n- `id` - `string`: the id of the entity we want to get\n- `parentId` - `string`: the id of the parent of the entity or entities we want to get\n- `load` - `bool`: loads the data from the backend (default is false)\n- `then` - `function`: using this option we can filter all the incoming values before they reach our component,\nfor example if we want to sort a list, or only show certain parts of it\n- `create` - `bool`: enables create mode, at this point there is no original data, only a draft, and saving will create\na new entity with a POST (default is false)\n- `onCreate` - `function`: callback for when a new entity was created in create mode\n- `onDel` - `function`: callback for when the entity was deleted\n- `onUpdate` - `function`: callback for when the entity was updated successfully\n\nThe options available as the second argument (applies to all entities):\n\n- `refetch` - `function`: a custom instance of Refetch `connect`, see\n[Advanced: Override Refetch defaults.](#advanced-override-refetch-defaults)\n- `hideUntilLoaded` - `bool`: passing `hideUntilLoaded: true` will wait for all initial loads to complete before the\nwrapped component is mounted (default is false)\n\n## State objects\n\nThe state objects `EntityState` and `EntityListState` are the link between your component and Reflorp. They\ncontain data and metadata about either a single entity, or a list of entities.\n\nBoth state objects below have these properties:\n- `value` - `array|object`: the actual data as received from the server\n- `pending` - `bool`: true if the data is being fetched for the first time (`value` is null)\n- `refreshing` - `bool`: true if the data is being updated in some way (`value` is present but may soon be outdated)\n- `fulfilled` - `bool`: true if the latest request was successful\n- `rejected` - `bool`: true if the latest request was unsuccessful\n  - Usually a rejected `PromiseState` does not have a value, but here we actually keep the latest value accessible, even\n  if it may be out of date depending of the kind of error occurred\n- `settled` - `bool`: true if the latest request has been completed, regardless of whether it was a success or not\n\n### EntityState\n\nA synchronous representation of a single entity and its metadata, with helper functions for dispatching common actions\nto redux.\n\nIt has the following properties:\n- `data` - `PromiseState`: describes the state of the data for this entity\n- `draft` - `PromiseState`: describes the state of the draft for this entity\n  - the draft contains changes made with `edit()` that have not yet been sent to the server with `save()`\n  - it also contains information on the latest `PATCH` or `DELETE` request for this entity\n- `entity` - `string`: the name of the entity type (i e `board`)\n- `id` - `string`: the id of the entity (i e `1`)\n- `parentId` - `string`: the id of the parent of the entity (i e `2`)\n- `loading` - `bool`: true if either `data.pending` or `data.refreshing` is true\n\nAnd the following functions:\n- `save(thenCallback, catchCallback)`: sends what is currently in the draft as a `PATCH` or `POST` (depending on if\nwe're in edit or create mode) to the backend\n- `edit(draft)`: updates the current draft for this entity (is merged shallowly with the previous draft)\n- `del(thenCallback, catchCallback)`: sends a `DELETE` to the backend\n  - If this request is successful the entity will be removed entirely from the store\n- `reset()`: resets the draft to the latest data fetched from the backend\n- `handleChange()`: helper function that updates fields in the draft according to the `name` attribute of the `input`\nfield it is called from\n  - `\u003cinput onChange={note.handleChange} name=\"summary\"\u003e` will keep the `summary` field of the `note` draft updated\n  when the input value is changed, see the [Editing and deleting](#example-editing-and-deleting) example above.\n\n### EntityListState\n\nA synchronous representation of a list of entities and its metadata, with helper functions for dispatching common\nactions to redux.\n\nIt has the following properties:\n- `data` - `PromiseState`: describes the state of the data\n- `entity` - `string`: the name of the entity type (i e `board`)\n- `parentId` - `string`: the id of the parent of these entities (i e `2`)\n- `loading` - `bool`: true if either `data.pending` or `data.refreshing` is true\n- `hasMore` - `bool`: true if the next page of this list is probably not empty\n\nAnd the following functions:\n- `more()`: fetches additional data from the backend, see [Pagination.](#pagination)\n\n# Backend\n\nBy default, URL:s for fetching and changing things are automatically generated. URL:s and methods are based on the names\nof the entities and the type of request we're doing.\n\n| Method | URL               | Response | Description                                                                                | Type   |\n|--------|-------------------|----------|--------------------------------------------------------------------------------------------|--------|\n| POST   | /boards           | object   | Creates a new entity                                                                       | create |\n| GET    | /boards           | object[] | Returns a list of entities                                                                 | list   |\n| GET    | /boards?page=2    | object[] | Returns the second page of a list of entities                                              | list   |\n| GET    | /boards/1         | object   | Returns a single entity                                                                    | single |\n| PATCH  | /boards/1         | object   | Updates a single entity, returns the same entity with changes                              | update |\n| DELETE | /boards/1         | -        | Deletes a single entity                                                                    | del    |\n| POST   | /boards/1/notes   | object   | Creates a new entity belonging to a parent entity                                          | create |\n| GET    | /boards/1/notes   | object[] | Returns a list of entities belonging to a parent entity                                    | list   |\n| GET    | /boards/1/notes/2 | object   | Returns a single entity with a parent entity                                               | single |\n| PATCH  | /boards/1/notes/2 | object   | Updates a single entity belonging to a parent entity, returns the same entity with changes | update |\n| DELETE | /boards/1/notes/2 | -        | Deletes a single entity belonging to a parent entity                                       | del    |\n\n### Pagination\n\nThere is built-in rudimentary support for pagination, if you call `EntityListState#more()` a new `GET` will be sent to\nthe same endpoint but with `?page=2`, and the result is appended to the end of the list. The page number is incremented\nuntil it receives an empty response.\n\nIf an empty page has been received the `EntityListState#hasMore` property is set to `false`.\n\n## Advanced: Override Refetch defaults\n\nRefetch has a convenient way to override certain defaults and hook in to internal stuff before and after requests are\nsent. You can do this with Reflorp as well by passing in your own `refetch` function with the `options` object as the\nsecond parameter to the decorator.\n\n```\nimport { connect } from 'react-refetch';\nimport { reflorp } from 'react-reflorp';\n\nconst refetch = connect.defaults({\n  credentials: 'include',\n});\n\n@reflorp(\n  ({ id }) =\u003e ({\n    board: { id, load: true },\n    notes: { parentId: id, load: true },\n  }),\n  {\n    refetch,\n    hideUntilLoaded: true,\n  }\n)\n```\n\n## Advanced: Customize URL:s\n\nThere are two configuration options that can help you here:\n\n- `baseUrl` - `string`: This will basically be prepended to all URL:s, regardless of which `getUrl` function is used\n- `getUrl` - `function`: By default Reflorp uses an internal `getUrl` function that will generate REST:y URL:s like in\nthe examples, but if you want you can override that function and build your own URL:s.\n\n### Example\n\nA call to `getUrl` might look something like this:\n\n```\ngetUrl({\n    entityConfiguration: (se below),\n    id: \"1\",\n    parentId: false,\n    query: { page: 2 },\n    flags: [\"list\"],\n});\n```\n\nThe `flags` parameter indicates what type of request this is (see the Type column under [Backend](#backend)).\n\nThe `query` parameter contains the additional filtering that should be sent (by default they are sent in the query part\nof the URL).\n\nThe `entityConfiguration` parameter contains an internal representation of the configuration for the current entity. It\nhas the following properties:\n\n- `parentEntity` - `EntityConfiguration`: The configuration for the parent entity, if any\n- `entity` - `string`: The name of this entity type (i e `board`)\n- `plural` - `string`: The plural name of this entity type as configured by you (i e `boards`)\n\n# Known limitations\n\n## Two levels of entities, no multiple parents\n\nWe currently only support two levels of entities, and an entity can not have multiple parents. You can still use a\ncustom `getUrl` function to achieve this to some extent, via the `query` property, but it won't be as smooth.\n\n# PromiseState\n\nInternally we use the excellent `PromiseState` class from Refetch, which is fully documented\n[over here](https://github.com/heroku/react-refetch/blob/master/docs/api.md#promisestate). The Reflorp state objects are\nbasically just wrappers around one or more `PromiseState` objects.\n\n# Support\n\nThis software is provided \"as is\", without warranty or support of any kind, express or implied. See\n[license](https://github.com/tomat/react-reflorp/blob/master/LICENSE.md) for details.\n\n# License\n\n[MIT](https://github.com/tomat/react-reflorp/blob/master/LICENSE.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftomat%2Freact-reflorp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftomat%2Freact-reflorp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftomat%2Freact-reflorp/lists"}