Basically a simple ORM using React Refetch as a backend, but the data is stored in the Redux store.
npm install react-reflorpReact Reflorp
=========================
Basically a simple ORM using Refetch, but the data is stored in Redux.
An example project can be found here, and the latest build of it should be live at http://reflorp.com
Requires React 0.14 or later and react-redux.
```
npm install --save react-reflorp
This assumes that you’re using npm package manager with a module bundler like Webpack or Browserify to consume CommonJS modules.
In the example code throughout this README we have a simple app with boards and notes. Each board has a number of notes,
and each note belongs to a single board. Both notes and boards have a unique id property, boards have a titlesummary
(string) property, and notes have a (string) property.
`javascript`
// app.js
const configuration = {
entities: {
board: {
plural: 'boards',
},
note: {
parent: 'board',
plural: 'notes',
},
},
baseUrl: '/api', // is prepended to all URL:s
};
`javascript
// Board.js
import { reflorp, EntityState, EntityListState } from 'react-reflorp';
@reflorp(({ id }) => ({
// fetch the board data: GET /api/boards/${id}
board: { id, load: true },
}))
export default class Board extends Component {
static propTypes = {
id: PropTypes.string,
board: PropTypes.instanceOf(EntityState),
};
render() {
const { board } = this.props;
if (board.pending) {
return
return (
Setup
First, add the
Container with a configuration object as a child of the react-redux Provider:`javascript
// app.jsimport { Container as ReflorpContainer } from 'react-reflorp';
const configuration = {
entities: {
board: {
plural: 'boards',
},
note: {
parent: 'board',
plural: 'notes',
},
},
baseUrl: '/api', // is prepended to all URL:s
};
ReactDOM.render(
,
document.getElementById('app')
);
`Then, add the reducer:
`javascript
// reducers.jsimport { reducer as reflorp } from 'react-reflorp';
export default {
reflorp,
};
`Frontend
The
@reflorp decorator corresponds to the @connect decorator from Refetch.It takes a function mapping
props and context to entities, and as a second argument an options object.By default it just injects the available data from the redux store. With
load: true it will actually go fetch the data
from the backend, and keep the component informed about that process.
The actual data and metadata is enclosed inside the EntityState and EntityListState abstractions, which are
described further down.Example: Loading and displaying
`javascript
// Board.jsimport React, { PropTypes, Component } from 'react';
import { reflorp, EntityState, EntityListState } from 'react-reflorp';
@reflorp(({ id }) => ({
// fetch the board data: GET /api/boards/${id}
board: { id, load: true },
// fetch the notes belonging to this board: GET /api/boards/${id}/notes
notes: {
parentId: id,
load: true,
// Sorts the notes.value property by id before they are sent to our component
then: (notes) => (notes || null) && notes.sort((note1, note2) => note1.id > note2.id),
},
}), { hideUntilLoaded: true })
export default class Board extends Component {
static propTypes = {
id: PropTypes.string,
board: PropTypes.instanceOf(EntityState),
notes: PropTypes.instanceOf(EntityListState),
};
render() {
const { notes, board } = this.props;
if (notes.pending || board.pending) {
return
Loading...;
} return (
{board.value.title}
{notes.value.map((note) => (
- {note.summary}
))}
{notes.hasMore && (
)}
);
}
}
`Example: Creating
`javascript
// CreateBoard.jsimport React, { PropTypes, Component } from 'react';
import { reflorp, EntityState } from 'react-reflorp';
@reflorp(() => ({
board: {
create: true,
onCreate: (board) => {
const boardId = board.value.id;
// Here you may for example redirect to the view board page
},
},
}))
export default class CreateBoard extends Component {
static propTypes = {
board: PropTypes.instanceOf(EntityState),
};
render() {
const { board } = this.props;
return (
);
}
}
`Example: Editing and deleting
`javascript
// EditNote.jsimport React, { PropTypes, Component } from 'react';
import { reflorp, EntityState } from 'react-reflorp';
@reflorp(({ noteId, boardId }) => ({
note: {
id: noteId,
parentId: boardId,
load: true,
onDel: () => {
// Callback for when the note has been deleted
// Here you may for example redirect back to another view
},
},
}))
export default class EditNote extends Component {
static propTypes = {
noteId: PropTypes.string,
boardId: PropTypes.string,
note: PropTypes.instanceOf(EntityState),
};
render() {
const { note } = this.props;
if (note.pending) {
return
Loading...;
} return (
);
}
}
`API
Decorator
The options available for each entity are:
-
id - string: the id of the entity we want to get
- parentId - string: the id of the parent of the entity or entities we want to get
- load - bool: loads the data from the backend (default is false)
- then - function: using this option we can filter all the incoming values before they reach our component,
for example if we want to sort a list, or only show certain parts of it
- create - bool: enables create mode, at this point there is no original data, only a draft, and saving will create
a new entity with a POST (default is false)
- onCreate - function: callback for when a new entity was created in create mode
- onDel - function: callback for when the entity was deleted
- onUpdate - function: callback for when the entity was updated successfullyThe options available as the second argument (applies to all entities):
-
refetch - function: a custom instance of Refetch connect, see
Advanced: Override Refetch defaults.
- hideUntilLoaded - bool: passing hideUntilLoaded: true will wait for all initial loads to complete before the
wrapped component is mounted (default is false)State objects
The state objects
EntityState and EntityListState are the link between your component and Reflorp. They
contain data and metadata about either a single entity, or a list of entities.Both state objects below have these properties:
-
value - array|object: the actual data as received from the server
- pending - bool: true if the data is being fetched for the first time (value is null)
- refreshing - bool: true if the data is being updated in some way (value is present but may soon be outdated)
- fulfilled - bool: true if the latest request was successful
- rejected - bool: true if the latest request was unsuccessful
- Usually a rejected PromiseState does not have a value, but here we actually keep the latest value accessible, even
if it may be out of date depending of the kind of error occurred
- settled - bool: true if the latest request has been completed, regardless of whether it was a success or not$3
A synchronous representation of a single entity and its metadata, with helper functions for dispatching common actions
to redux.
It has the following properties:
-
data - PromiseState: describes the state of the data for this entity
- draft - PromiseState: describes the state of the draft for this entity
- the draft contains changes made with edit() that have not yet been sent to the server with save()
- it also contains information on the latest PATCH or DELETE request for this entity
- entity - string: the name of the entity type (i e board)
- id - string: the id of the entity (i e 1)
- parentId - string: the id of the parent of the entity (i e 2)
- loading - bool: true if either data.pending or data.refreshing is trueAnd the following functions:
-
save(thenCallback, catchCallback): sends what is currently in the draft as a PATCH or POST (depending on if
we're in edit or create mode) to the backend
- edit(draft): updates the current draft for this entity (is merged shallowly with the previous draft)
- del(thenCallback, catchCallback): sends a DELETE to the backend
- If this request is successful the entity will be removed entirely from the store
- reset(): resets the draft to the latest data fetched from the backend
- handleChange(): helper function that updates fields in the draft according to the name attribute of the input
field it is called from
- will keep the summary field of the note draft updated
when the input value is changed, see the Editing and deleting example above.$3
A synchronous representation of a list of entities and its metadata, with helper functions for dispatching common
actions to redux.
It has the following properties:
-
data - PromiseState: describes the state of the data
- entity - string: the name of the entity type (i e board)
- parentId - string: the id of the parent of these entities (i e 2)
- loading - bool: true if either data.pending or data.refreshing is true
- hasMore - bool: true if the next page of this list is probably not emptyAnd the following functions:
-
more(): fetches additional data from the backend, see Pagination.Backend
By default, URL:s for fetching and changing things are automatically generated. URL:s and methods are based on the names
of the entities and the type of request we're doing.
| Method | URL | Response | Description | Type |
|--------|-------------------|----------|--------------------------------------------------------------------------------------------|--------|
| POST | /boards | object | Creates a new entity | create |
| GET | /boards | object[] | Returns a list of entities | list |
| GET | /boards?page=2 | object[] | Returns the second page of a list of entities | list |
| GET | /boards/1 | object | Returns a single entity | single |
| PATCH | /boards/1 | object | Updates a single entity, returns the same entity with changes | update |
| DELETE | /boards/1 | - | Deletes a single entity | del |
| POST | /boards/1/notes | object | Creates a new entity belonging to a parent entity | create |
| GET | /boards/1/notes | object[] | Returns a list of entities belonging to a parent entity | list |
| GET | /boards/1/notes/2 | object | Returns a single entity with a parent entity | single |
| PATCH | /boards/1/notes/2 | object | Updates a single entity belonging to a parent entity, returns the same entity with changes | update |
| DELETE | /boards/1/notes/2 | - | Deletes a single entity belonging to a parent entity | del |
$3
There is built-in rudimentary support for pagination, if you call
EntityListState#more() a new GET will be sent to
the same endpoint but with ?page=2, and the result is appended to the end of the list. The page number is incremented
until it receives an empty response.If an empty page has been received the
EntityListState#hasMore property is set to false.Advanced: Override Refetch defaults
Refetch has a convenient way to override certain defaults and hook in to internal stuff before and after requests are
sent. You can do this with Reflorp as well by passing in your own
refetch function with the options object as the
second parameter to the decorator.`
import { connect } from 'react-refetch';
import { reflorp } from 'react-reflorp';const refetch = connect.defaults({
credentials: 'include',
});
@reflorp(
({ id }) => ({
board: { id, load: true },
notes: { parentId: id, load: true },
}),
{
refetch,
hideUntilLoaded: true,
}
)
`Advanced: Customize URL:s
There are two configuration options that can help you here:
-
baseUrl - string: This will basically be prepended to all URL:s, regardless of which getUrl function is used
- getUrl - function: By default Reflorp uses an internal getUrl function that will generate REST:y URL:s like in
the examples, but if you want you can override that function and build your own URL:s.$3
A call to
getUrl might look something like this:`
getUrl({
entityConfiguration: (se below),
id: "1",
parentId: false,
query: { page: 2 },
flags: ["list"],
});
`The
flags parameter indicates what type of request this is (see the Type column under Backend).The
query parameter contains the additional filtering that should be sent (by default they are sent in the query part
of the URL).The
entityConfiguration parameter contains an internal representation of the configuration for the current entity. It
has the following properties:-
parentEntity - EntityConfiguration: The configuration for the parent entity, if any
- entity - string: The name of this entity type (i e board)
- plural - string: The plural name of this entity type as configured by you (i e boards)Known limitations
Two levels of entities, no multiple parents
We currently only support two levels of entities, and an entity can not have multiple parents. You can still use a
custom
getUrl function to achieve this to some extent, via the query property, but it won't be as smooth.PromiseState
Internally we use the excellent
PromiseState class from Refetch, which is fully documented
over here. The Reflorp state objects are
basically just wrappers around one or more PromiseState` objects.This software is provided "as is", without warranty or support of any kind, express or implied. See
license for details.