UNPKG

redux-and-the-rest

Version:

Declarative, flexible Redux integration with your RESTful API

955 lines (731 loc) 104 kB
<p align="center"> <img src="https://cdn.rawgit.com/greena13/redux-and-the-rest/f364d1e6/images/logo.png"><br/> <h2 align="center">Redux and the REST</h2> </p> Declarative, flexible Redux integration with RESTful APIs. ## Feature overview * **DRY:** All of the boilerplate code usually required to use Redux is abstracted away into a succinct DSL inspired by the Ruby on Rails framework. * **Convention over configuration:** A sensible set of configurations are used by default, but you can override them with custom behaviour whenever you need. * **Flexible:** All RESTful conventions can be overridden and extended when you need to deviate or add to the standard CRUD functionality. * **Minimal:** You can choose which features to enable, when you want to use them, so there is no unnecessary overhead or bloat. * **Quick to get started:** It's quick to get up-and-running and easy to define new resources and actions in a few lines. * **Plays well with others:** `redux-and-the-rest` does not care what version of Redux you use or how you have architected your app, and it allows you to gradually introduce it to your project alongside other Redux solutions. * **Documented:** The API is minimal and expressive, and all options and common use cases are documented in full. * **Tested:** `redux-and-the-rest` comes with an extensive test suite. ## Design philosophy `redux-and-the-rest` loosely takes its lead from Reduced Instruction Set Computing (RISC) and the standard Create, Read, Update, Delete (CRUD) paradigm and offers as few low-level reducers and actions as possible. In doing so, it allows more re-use and sharing of code, and reduces the overhead of scaling out a store for large applications. You are encouraged to write your own helper functions on top of the action creators `redux-and-the-rest` provides for more nuanced updates, where needed (details and examples follow). ## Basic usage ```javascript import { resources } from 'redux-and-the-rest'; import { createStore, applyMiddleware, combineReducers } from 'redux'; import Thunk from 'redux-thunk'; /** * Define a users resource */ const { reducers: usersReducers, actionCreators: { fetchList: fetchUsers }, getList } = resources( { name: 'users', url: 'http://test.com/users/:id?'. keyBy: 'id' }, { fetchList: true } ); /** * Pass the reducers to your store (the reducers for only one resource is used - * normally you would have many) */ const store = createStore(combineReducers({ users: usersReducers }), {}, applyMiddleware(Thunk)); /** * Action to fetch the users from http://test.com/users and make them available in your store */ fetchUsers(); /** * Retrieve the users from the store */ users = getList(store.getState().users); ``` ## Quick Reference You can find more examples and a walk-through style introduction of how to make the most of `redux-and-the-rest` in the [Quick Reference](QuickReference.md). ## Contents * [Install &amp; Setup](#install--setup) * [Peer Dependencies](#peer-dependencies) * [Defining resources](#defining-resources) * [Configuring individual actions](#configuring-individual-actions) * [Using the default RESTful action configuration](#using-the-default-restful-action-configuration) * [Providing custom action configuration](#providing-custom-action-configuration) * [Actions and their action creators](#actions-and-their-action-creators) * [CRUD actions](#crud-actions) * [Local CRUD actions](#local-crud-actions) * [Remote API CRUD actions](#remote-api-crud-actions) * [Clearing actions](#clearing-actions) * [Selection actions](#selection-actions) * [Dispatchers](#dispatchers) * [Defining associations](#defining-associations) * [One-to-One and One-to-Many relationships](#one-to-one-and-one-to-many-relationships) * [Many-to-Many relationships](#many-to-many-relationships) * [Connecting to React](#connecting-to-react) * [Usage with react-redux](#usage-with-react-redux) * [API Reference](#api-reference) * [Levels of configuration](#levels-of-configuration) * [Global Options API](#global-options-api) * [Usage](#usage) * [Options](#options) * [Resource Options API](#resource-options-api) * [Usage](#usage-1) * [Options](#options-1) * [Naming and indexing](#naming-and-indexing) * [Synchronising with a remote API](#synchronising-with-a-remote-api) * [Reducers](#reducers) * [Action Options API](#action-options-api) * [Usage](#usage-2) * [Options](#options-2) * [Naming and indexing](#naming-and-indexing-1) * [Synchronising with a remote API](#synchronising-with-a-remote-api-1) * [Reducers](#reducers-1) * [Store data](#store-data) * [Getting items from the store](#getting-items-from-the-store) * [Automatically fetching items not in the store](#automatically-fetching-items-not-in-the-store) * [Automatically instantiating new items not in the store](#automatically-instantiating-new-items-not-in-the-store) * [Getting lists from the store](#getting-lists-from-the-store) * [Automatically fetching lists that are not in the store](#automatically-fetching-lists-that-are-not-in-the-store) * [Store data schemas](#store-data-schemas) * [Nomenclature](#nomenclature) * [Use helper methods where possible](#use-helper-methods-where-possible) * [Resource schema](#resource-schema) * [Top level schema](#top-level-schema) * [Item schema](#item-schema) * [List schema](#list-schema) * [Data lifecycle](#data-lifecycle) * [Client statuses](#client-statuses) * [Pending statuses](#pending-statuses) * [Response statuses](#response-statuses) * [Setting initial state](#setting-initial-state) * [RESTful (asynchronous) actions](#restful-asynchronous-actions) * [RESTful behaviour overview](#restful-behaviour-overview) * [Preventing duplicate requests](#preventing-duplicate-requests) * [Dealing with failed requests](#dealing-with-failed-requests) * [Dealing with slow requests](#dealing-with-slow-requests) * [Detecting old data](#detecting-old-data) * [Fetch a list from the server](#fetch-a-list-from-the-server) * [fetchList action creator options](#fetchlist-action-creator-options) * [Fetch an individual item from the server](#fetch-an-individual-item-from-the-server) * [Fetch action creator options](#fetch-action-creator-options) * [Create a new item on the server](#create-a-new-item-on-the-server) * [Adding a created item to a list](#adding-a-created-item-to-a-list) * [Update a item on the server](#update-a-item-on-the-server) * [Update action creator options](#update-action-creator-options) * [Destroy a item on the server](#destroy-a-item-on-the-server) * [DestroyItem action creator options](#destroyitem-action-creator-options) * [Local (synchronous) actions](#local-synchronous-actions) * [Add a new item to the store](#add-a-new-item-to-the-store) * [NewItem action creator options](#newitem-action-creator-options) * [Clear the new item from the store](#clear-the-new-item-from-the-store) * [Edit the new item in the store](#edit-the-new-item-in-the-store) * [Edit an existing item in the store](#edit-an-existing-item-in-the-store) * [Edit an item without worrying whether it's new or not](#edit-an-item-without-worrying-whether-its-new-or-not) * [Detecting if a item has been edited](#detecting-if-a-item-has-been-edited) * [Accessing values before they were edited](#accessing-values-before-they-were-edited) * [Clear local edits](#clear-local-edits) * [Select a item in the store](#select-a-item-in-the-store) * [Select another item in the store](#select-another-item-in-the-store) * [Deselect a item in the store](#deselect-a-item-in-the-store) * [Clear all the selected items in the store](#clear-all-the-selected-items-in-the-store) * [Clearing a resource when a user signs out or other event](#clearing-a-resource-when-a-user-signs-out-or-other-event) * [Configuring requests](#configuring-requests) * [Configuring the URLs used for a request](#configuring-the-urls-used-for-a-request) * [URL Parameters](#url-parameters) * [Using string values](#using-string-values) * [Using object values](#using-object-values) * [Specifying query parameters](#specifying-query-parameters) * [Adapting request bodies](#adapting-request-bodies) * [Pagination](#pagination) * [Working with Authenticated APIs](#working-with-authenticated-apis) * [Auth tokens as headers](#auth-tokens-as-headers) * [Auth tokens as query parameters](#auth-tokens-as-query-parameters) * [Session cookies](#session-cookies) * [Configuring other request properties](#configuring-other-request-properties) * [Adapting responses](#adapting-responses) * [Adapting success responses](#adapting-success-responses) * [Handling error responses](#handling-error-responses) ## Install & Setup `redux-and-the-rest` can be installed as a CommonJS module: ``` npm install redux-and-the-rest --save # OR yarn add redux-and-the-rest ``` ### Peer Dependencies If you have already installed `redux`; `redux-thunk`; some form of fetch polyfill (suggested: `isomorphic-fetch`); and (optionally) `react-redux`, then you can skip to the next section. If you have not already done so, you must also install `redux` ([full installation](https://github.com/reduxjs/redux)): ``` npm install redux --save # OR yarn add redux ``` `redux-and-the-rest` also requires the `redux-thunk` middleware to function: ``` npm install redux-thunk --save # OR yarn add redux-thunk ``` You must then pass the `redux-thunk` middleware in as a parameter when you create your Redux store ([full instructions](https://github.com/reduxjs/redux-thunk#installation)): ```javascript import { createStore, applyMiddleware, combineReducers } from 'redux'; import Thunk from 'redux-thunk'; function buildStore(initialState, reducers) { return createStore(combineReducers(reducers), initialState, applyMiddleware(Thunk)); } export default buildStore; ``` If you are using React, it's also recommended to use the `react-redux` bindings ([full instructions](https://github.com/reduxjs/react-redux)): ``` npm install react-redux --save # OR yarn add react-redux ``` Finally, you will also need to ensure global calls to the `fetch` method work in all your environments (node.js and browser). The simplest way to do this is to install `isomorphic-fetch` ([full instructions](https://github.com/matthew-andrews/isomorphic-fetch)): ``` npm install --save isomorphic-fetch es6-promise # OR yarn add isomorphic-fetch es6-promise ``` ## Defining resources Resources are defined with one of two functions: * `resources` - For when there are many resources, each referenced with one or more ids or keys, or * `resource` - For singular resources; cases where there is only one like the current user's profile They both accept two options hashes as arguments: * `resourceOptions` - options that apply to all of a resource's actions * `actionOptions` - options that configure individual actions (RESTful or not) The functions return an object containing Redux components necessary to use the resource you have just defined: * `reducers` - an object of reducers that you can pass to Redux's `combineReducers` function. * `actions` - an object of action constants where the keys are the generic action names and the values are the specific action constants (e.g. `{ fetchList: 'FETCH_USERS' }`) * `actionCreators` - an object of functions (action creators) you call to interact with the resource which match the actions you specify in `actionOptions` and are passed to Redux's `dispatch` function. Also returned are 3 helper functions that are always available: * `getList` - for retrieving a list based on its key parameters * `getItem` - for retrieving an item based on its key parameters * `getNewItem` - for retrieving the item currently being created * `getNewOrExistingItem` - for first attempting to retrieve and existing item and then falling back to returning the new item currently being created In addition to these, if you enable the underlying actions, the following helper functions are also exported: * `getOrFetchItem` - Retrieves an item from the Redux store, or makes a fetch request for it, if it's not available * `getOrFetchList` - Retrieves a list from the Redux store, or makes a fetch request for it, if it's not available * `getOrInitializeItem` - Retrieves the new item from the Redux store, or instantiates it with the provided values, if it's not available * `saveItem` - Creates an item (by sending a `POST` request) if it's not already in the store, or has a status of `NEW`, otherwise sends and `UPDATE` request with the values provided. Each of these can be thought of as helpers that contain common logic to determine which underlying action creator to invoke. They require the `store` option to be used with `configure()`, so they can manually call `dispatch` as appropriate (consequently, these are not action creators and should not have their return values passed to `dispatch`). Their first argument must be the current resource's state in the Redux store, and all others are passed to the action creators they wrap. These methods are asynchronous (they return a value immediately but do not dispatch any actions synchronously to avoid React warnings about updating other component's state during a render cycle when used with `react-redux`) and throttled (so if multiple components on the same React tree call them in the same render cycle, only one action is dispatched), so they are safe to use in component's `render` functions. ```javascript import { resources } from 'redux-and-the-rest'; const { reducers, actionCreators: { fetchList: fetchUsers } } = resources( { name: 'users', url: 'http://test.com/users/:id?', keyBy: 'id' }, { fetchList: true } ); ``` ### Configuring individual actions `actionOptions` specifies the actions defined for a particular resource and allow you to expand upon, or override, the configuration made in `resourceOptions`. `actionOptions` should be an either one of two formats: An array of action names as strings: ```javascript const { actionCreators: { fetchList: fetchUsers } } = resources( { // ... }, [ 'fetchList' ] ); ``` This format is shorter, and recommended unless you need the second format. The other format is an object with action names as keys and configuration objects as values. #### Using the default RESTful action configuration If you want to use the default configuration for a particular action, you just need to pass a value of `true`, for example: ```javascript const { actionCreators: { fetchList: fetchUsers } } = resources( { // ... }, { fetchList: true } ); ``` #### Providing custom action configuration You can override or extend the default configuration for an action using an options hash instead of `true` when defining your actions: ```javascript const { actionCreators: { fetchList: fetchUsers } } = resources( { // ... }, { fetchList: { // action options } } ); ``` See [Action Options API](#action-options-api) for a full list of supported options. ### Actions and their action creators #### CRUD actions It's common to think about interacting with resources in terms of 4 primary operations: create, read, update, delete (CRUD). `redux-and-the-rest` provides local (client-side) actions to refine the input state of each of these operations, and the remote API actions to persist or submit them. ##### Local CRUD actions Generally your application will need to perform actions on resources locally, until a point is reached where those changes should be synchronised with a remote API. None of them make any requests to a remote API and are client-side operations that happen only in your Redux store. | Action | Action Creator | Description | | ------ | -------------- | ----------- | | newItem | newItem() | Creates a new item in the Redux store | | editNewItem | editNewItem() | Continue to add or modify a new item's attributes until it's ready to be saved. | | clearNewItem | clearNewItem() | Discards (removes) the new item fom the Redux store | | editItem | editItem() | Replaces an existing (saved) item's attributes in the store with new ones | | editNewOrExistingItem | editNewOrExistingItem() | Delegates to editNewItem or editItem depending on the state of the item | | clearItemEdit | clearItemEdit() | Reverts an edit, to restore the item's attributes before the edit | These actions are generally accumulative and reversible, so you can call them successively over multiple screens or stages of a workflow and provide a cancel feature if the user wishes to abort. ##### Remote API CRUD actions When the your application is done with local manipulation of a resource, you can use the following to persist those changes to a remote API. | Action | Action Creator | Description | | ------ | -------------- | ----------- | | fetchList | fetchList() | Fetches a list of items from a remote API | | fetchItem | fetchItem() | Fetches an item from a remote API | | createItem | createItem() | Sends a create request with an item's attributes to a remote API | | updateItem | updateItem() | Sends new attributes (an "update") for an item to a remote API | | destroyItem | destroyItem() | Sends a delete request for an item to a remote API | `resources()` accepts a `localOnly` option, that allows you to maintain resources without a remote API and will turn the asynchronous remote API actions into synchronous updates that label your resources as being in a "saved" state. #### Clearing actions It's generally _not_ recommended to use any of the following directly, as there is usually a better way of achieving what you need, but they are available: | Action | Action Creator | Description | | ------ | -------------- | ----------- | | clearItem | clearItem() | Removes an item from the store. | | clearList | clearList() | Removes a list from the store (but still leaves behind its items). | | clearResource | clearResource() | Completely resets a resource to its empty state, clearing all selections, items and lists. | Some common situations where you may be tempted to use the above, are: * Refreshing an item or list from a remote API: `fetchItem()` or `fetchList()` should handle transitioning between the stale and new records more cleanly. * Cancelling an edit to an item: Use `clearItemEdit()` to roll back the changes without the need to re-fetch from the remote API. * Clearing a resource when an event occurs, such as when user logs out: use the `clearOn` option to achieve this more efficiently (discussed below). #### Selection actions In addition to the CRUD functionality, `redux-and-the-rest` provides a number of actions for selecting one or more items to perform actions on. This is useful if your application needs to selectItem resources on one screen or area and persist that selection to another area, or allow it to be retrieved at a later time. | Action | Action Creator | Description | | ------ | -------------- | ----------- | | selectItem | selectItem() | Selects an item in the store, replacing any previous items that may have been selected. | | selectAnotherItem | selectAnotherItem() | Selects an item in the store, adding it to any previous items that are selected. | | deselectItem | deselectItem() | Unselects an item that is currently selected | | clearSelectedItems | clearSelectedItems() | Unselects all selected items | ### Dispatchers `redux-and-the-rest` exports action creators in the `actionCreators` object of every resource definition, which return objects (actions) that are ready to be passed to redux's `dispatch` function (as users of redux are accustom to doing). However, for convenience, `redux-and-the-rest` also exports _dispatchers_, which are functions that call `dispatch` for you, and are useful in circumstances where the `dispatch` function is not readily available. They have the same name as their action creator counterparts, accept the same arguments, and are enabled with the same configuration when defining your `resource` or `resources`. They are available directly off the exported object: ```javascript const { actionCreators: { fetchList: fetchUsersActionCreator }, fetchList: fetchUsersDispatcher } = resources( { // ... }, { fetchList: { // action options } } ); ``` ## Defining associations You can define associations between resources so that their foreign keys are properly maintained as you add, update and remove related resources. Once associated, if the associated resource is deleted, it will be removed from the foreign keys of any related items of the current resource. If a new item of the associated resource is created with a foreign key pointing at an item of the current resource, it's key will be added to the list of foreign keys. If an existing associated item is updated and the current resource items are swapped, this will also be handled. You define associations on the resources you want to be updated when the associated resource changes. If you need the updates to work both ways, you'll need to define both sides of the association. Association configuration come in two forms: `belongsTo` and `hasAndBelongsTo`. Each expects an array of resource names (values that you provide to the `name` attribute when defining those resources), or an object, where the keys are the names of the associated resources, and the values are a configuration object containing the following options: * `foreignKey` - (string) Name of the attribute that stores the id or ids of the current resource on the associated one. If unspecified, the `as` attribute (or the resource's `name` value) are appended with the suffix of `id`. * `as` - (string) If a foreign key is not specified, this association name is used with a suffix of `id` to derive the foreign key. * `key` - (string) * `dependent` - (boolean) Whether to remove the associated resource if the current one is removed from the store. ### One-to-One and One-to-Many relationships The `belongsTo` resource option is used to define a one-to-one and one-to-many relationships. ```javascript import { resources } from 'redux-and-the-rest'; const addresses = resources({ name: 'addresses', url: 'http://test.com/addresses/:id?', keyBy: 'id' }, ['createItem', 'updateItem', 'destroyItem']); const users = resources({ name: 'users', url: 'http://test.com/users/:id?', keyBy: 'id', belongsTo: ['addresses'] }, { fetchList: true, newItem: true, }); /** * The following will add 'temp' to 'addressIds' of the user with id '1' (if it exists in the store). */ addresses.actionCreators.createItem({ id: 'temp' }, { userId: 1, city: 'Boston' }); ``` ### Many-to-Many relationships The `hasAndBelongsToMany` resource option is used to define many-to-many relationships. It behaves and accepts the same arguments as `belongsTo`, but correctly maintains an *array* of foreign keys on items of the resource being defined, rather than a single id. For example, if each user item has an `addressId` with a singular value, use `belongsTo`, however if a user item has an `addressesId` with an array if address ids, use `hasAndBelongsToMany`. ## Connecting to React ### Usage with react-redux Although not required, when using `redux-and-the-rest` with React, it's recommended you use `react-redux`. It provides the `connect` function, which accepts two arguments: | function | Has access to | Passes to your component as props | Can be thought of as | | -------- | ------------- | --------------------------------- | -------------------- | | `mapStateToProps` | Current Redux state and the props passed to your container | Some subset of the total redux state | READ | | `mapDispatchToProps` | `dispatch` (the function for dispatching actions or updates on the Redux store) | Handler functions that accept values from your component and call `dispatch` | WRITE (CREATE, UPDATE, DELETE) | The full API can be seen in the [docs](https://react-redux.js.org/api/connect). Because `redux-and-the-rest` is built around the principle of providing a reduced set of reducers for the standard CRUD operations (with a few extras for selection and clearing), you're expected to define utility functions for performing "sub-operations". Take the example of a widget that sets the user's age: you only want to modify one of the the user items's values, but you need to provide the entire new set of values back to `redux-and-the-rest` (this is to allow for removal of attributes and complex or deep merging that `redux-and-the-rest` cannot be expected to guess). Because the `connect` function separates access to `dispatch` (`mapDispatchToProps`) and access to the current Redux state (`mapStateToProps`), you have a few options. When your component needs access to all the resource's attributes anyway, you can pass the whole item into your component and then back out again in the handler: ```javascript import { connect } from 'react-redux' import { getUser, updateUser } from './resources/users'; import AgeWidget from './components/AgeWidget'; const mapStateToProps = ({ user } ) => { return { user: getUser(user) } }; const mapDispatchToProps = ((dispatch) => { return { updateAge: (user, newAge) => dispatch(updateUser({ ...user, age: newAge })) }; }); export default connect( mapStateToProps, mapDispatchToProps )(AgeWidget) ``` And you would then call it in your component: ```javascript <Button onPress={updateAge(user.values, user.values.age + 1)} > Increment </Button> ``` You may also choose to use `connect`'s third argument to curry your handler props: ```javascript import { connect } from 'react-redux' import { getUser, updateUser } from './resources/users'; import AgeWidget from './components/AgeWidget'; const mapStateToProps = ({ user } ) => { return { user: getUser(user) } }; const mapDispatchToProps = ((dispatch) => { return { updateAge: (values, newAge) => dispatch(updateUser({ ...values, age: newAge })) }; }); const mergeProps = ((stateProps, dispatchProps, ownProps) => { return { ...stateProps, ...dispatchProps, updateAge: (newAge) => dispatchProps.updateAge(stateProps.user.values, newAge), ...ownProps } }); export default connect( mapStateToProps, mapDispatchToProps, mergeProps )(AgeWidget) ``` And call it in your component with the reduced argument list: ```javascript <Button onPress={updateAge(user.values.age + 1)} > Increment </Button> ``` However, if your component doesn't need access to the rest of the user's attributes it's recommended that you keep the component's interface minimal and your handler arguments as few as possible and just retrieve the state directly from the store when it's needed in your handlers: ```javascript import { connect } from 'react-redux' import { getUser, updateUser } from './resources/users'; import store from './store'; import AgeWidget from './components/AgeWidget'; const mapStateToProps = ({ user } ) => { return { user: getUser(user) } }; const mapDispatchToProps = ((dispatch) => { return { updateAge: (newAge) => { const { values } = getUser(store.getState().users); dispatch(updateUser({ ...values, age: newAge })); } }; }); export default connect( mapStateToProps, mapDispatchToProps )(AgeWidget) ``` Or alternatively, you can chose not to pass down the attributes you only needed to make available to your handlers: ```javascript import { connect } from 'react-redux' import { getUser, updateUser } from './resources/users'; import AgeWidget from './components/AgeWidget'; const mapStateToProps = ({ user } ) => { return { user: getUser(user) } }; const mapDispatchToProps = ((dispatch) => { return { updateAge: (user, newAge) => dispatch(updateUser({ ...user, age: newAge })) }; }); const mergeProps = ((stateProps, dispatchProps, ownProps) => { // The final list of props passed to your component return { age: stateProps.user.values.age, updateAge: (newAge) => dispatchProps.updateAge(stateProps.user.values, newAge), ...ownProps } }); export default connect( mapStateToProps, mapDispatchToProps, mergeProps )(AgeWidget) ``` Either option will allow you to call the handler in your component with only the new values: ```javascript <Button onPress={updateAge(age + 1)} > Increment </Button> ``` ## API Reference ### Levels of configuration `redux-and-the-rest` achieves its flexibility using four levels of configuration; each one has a different scope and is specified at different times. You need to selectItem where you place your configuration depending on how wide you want particular options to apply, and when the desired values are available. The options are set out in a hierarchy, so as their scope becomes increasingly specific, their priority increases and they override any corresponding action that may have been provided to a lower priority set of options. For example, `actionCreatorOptions` take precedence over `actionOptions` (which take precedence over `resourceOptions`). | Options | Priority | Defined | Scope | Required | | ------- | -------- | ------- | ----- | -------- | | `globalOptions` | Lowest | At any time, using `configure()`. | All resources and their actions | No | | `resourceOptions` | | When defining resources, using `resources()` | All of a resource's actions | Yes | | `actionOptions` | | When defining resources, using `resources()` | An action creator function | No | | `actionCreatorOptions` | Highest | When calling an action creator, as the last argument | An invocation of an action creator | No | Here is an example of them used all in once place: ```javascript import { configure, resources } from 'redux-and-the-rest'; configure({ // globalOptions // ... }); const { actionCreators: { fetchList: fetchUsers } } = resources( { // resourceOptions name: 'users', url: 'http://www.example.com/users/:id', keyBy: 'id' }, { fetchList: { // actionOptions // ... }, fetch: { // actionOptions // ... } } ); fetchUsers({order: 'newest'}, { // actionCreatorOptions // ... }) ``` ### Global Options API #### Usage ```javascript import { configure } from 'redux-and-the-rest'; configure({ // globalOptions }); ``` #### Options | key | Type | Required or Default Value | Description | | --- | ---- | ------------------------- | ----------- | | keyBy | string or array of strings | No | The resource attribute used to key/index all items of the current resource type. This will be the value you pass to each action creator to identify the target of each action. By default, 'id' is used. | | localOnly | boolean | False | Set to true for resources that should be edited locally, only. The fetchItem and fetchList actions are disabled (use `getOrFetchItem` and `getOrFetchList` instead) and the createItem, updateItem and destroyItem only update the store locally, without making any HTTP requests. | | urlOnlyParams | Array of string | [] | The attributes passed to action creators that should be used to create the request URL, but ignored when storing the request's response. | | method | String | No | The HTTP method to use for the request. Defaults to the standard method used for the particular RESTful action | | actionName | String | No | The type value to give the action(s) dispatched. If this value is not specified, RESTful actions will use a standard default that includes the resource name and the action name, while custom actions will use the key of the action configuration object, attempting to substitute 'Item' for the resource name, or fallback to a name with the action and resource name concatenated together. | | actionCreator | Function | Required only for custom actions | A custom action creator function that returns an action or thunk action that can then be passed to Redux's dispatch function | | responseAdaptor | (responseBody: Object, response: Response) => { values: Object, error?: Object or string, errors?: Array<Object or string> } | No | Function used to adapt the responses for requests before it is handed over to the reducers. The function must return the results as an object with properties values and (optionally) error. | | requestAdaptor | (requestBody: Object) => Object | No | Function used to adapt the JavaScript object before it is handed over to become the body of the request to be sent to an external API. | | credentials | RequestCredentials | No | Whether to include, omit or send cookies that may be stored in the user agent's cookie jar with the request only if it's on the same origin. | | acceptType | String | No | The `Accept` header to use with each request. Defaults to the contentType if not defined. | | contentType | String | No | The `Content-Type` header to use with each request | | errorContentType | String | No | The `Content-Type` of error responses that should be parsed as JSON. Defaults to the `contentType` if not defined. | | queryStringOptions | Object | {} | Set of options passed to query-string when serializing query strings. (See https://www.npmjs.com/package/query-string) | | request | RequestInit | No | The request configuration object to be passed to the fetch method, or the new XMLHttpRequest object, when the progress option is used. | | listWildcard | String | '*' | The list key used to reference all lists for action creator's option's list operations | | generateId | Function | `() => Date.now().toString()` | A function to use to generate ids for new items | | reducer | Function | Required for custom actions | A custom reducer function to adapt the resource as it exists in the Redux store. By default, the standard RESTful reducer is used for RESTful actions, but this attribute is required for Non-RESTful actions. | | beforeReducers | Array of reducers | No | A list of functions to call before passing the resource to the reducer. This is useful if you want to use the default reducer, but provide some additional pre-processing to standardise the resource before it is added to the store. | | afterReducers | Array of reducers | No | A list of functions to call after passing the resource to the reducer. This is useful if you want to use the default reducer, but provide some additional post-processing to standardise the resource before it is added to the store. | | store | Store | Yes, if you use the mentioned helpers | The Redux store, used to directly invoke dispatch and get state for the getOrFetchItem() and getOrFetchList() functions | ### Resource Options API Values passed to `resourceOptions` are used to configure the resource and apply to all of that resource's actions, unless overridden by more specific configuration in `actionOptions`. #### Usage ```javascript import { resources } from 'redux-and-the-rest'; const { actionCreators: { fetchList: fetchUsers } } = resources( { // resourceOptions }, { // ... } ); ``` #### Options ##### Naming and indexing | key | Type | Required or Default Value | Description | | --- | ---- | ------------------------ | ----------- | | `name` | string | Required | The pluralized name of the resource you are defining, used to create the names of the action types | `keyBy` | string | 'id' | The resource attribute used to key/index all items of the current resource type. This will be the value you pass to each action creator to identify the target of each action. | ##### Synchronising with a remote API | key | Type | Required or Default Value | Description | | --- | ---- | ------------------------- | ----------- | | `localOnly` | boolean | false | Set to true for resources that should be edited locally, only. The `fetchItem` and `fetchList` actions are disabled (you must use `getOrFetchItem` or `getOrFetchList` instead) and the `createItem`, `updateItem` and `destroyItem` only update the store locally, without making any HTTP requests. | | `url` | string | Required | A url template that is used for all of the resource's actions. The template string can include required url parameters by prefixing them with a colon (e.g. `:id`) and optional parameters are denoted by adding a question mark at the end (e.g. `:id?`). This will be used as the default url template, but individual actions may override it with their own. | | `urlOnlyParams` | string[] | [ ] | The attributes passed to action creators that should be used to create the request URL, but ignored when storing the request's response. Useful for pagination. | | `responseAdaptor` | Function | Identity function | Function used to adapt the response for a particular request before it is handed over to the reducers. The function must return the results as an object with properties `values` and (optionally) `error` or `errors`. | | `credentials` | string | undefined | Whether to include, omit or send cookies that may be stored in the user agent's cookie jar with the request only if it's on the same origin. | | `requestAdaptor` | Function | Identity function | Function used to adapt the JavaScript object before it is handed over to become the body of the request to be sent to an external API. | ##### Reducers | key | Type | Required or Default Value | Description | | --- | ---- | ------------------------- | ----------- | | `beforeReducers` | Function[] | [ ] | A list of functions to call before passing the resource to the `reducer`. This is useful if you want to use the default reducer, but provide some additional pre-processing to standardise the resource before it is added to the store. | | `afterReducers` | Function[] | [ ] |A list of functions to call after passing the resource to the `reducer`. This is useful if you want to use the default reducer, but provide some additional post-processing to standardise the resource before it is added to the store. | | `reducesOn` | Object | {} | An object that specifies custom reducers in response to actions external to the current resource. The keys of the objects are action types from other resources, your own custom actions outside of redux-and-the-rest, or the name of the action you're enabling on this resource (e.g. fetchItem). The values are the reducer functions. | | `clearOn` | Action or Action[] | [ ] | A single or list of actions for which the current resource should be cleared. | | `hasAndBelongsToMany` | {\[associationName\]: Resource } | { } | An object of associated resources, with a many-to-many relationship with the current one. | | `belongsTo` | {\[associationName\]: Resource } | { } | An object of associated resources, with a one-to-many relationship with the current one. | The reducer functions used in the `beforeReducers`, `afterReducers` and `reducesOn` options accept 3 arguments: * The current resource(s) Redux state (not the entire Redux state) * The current action being dispatch (not restricted to only those defined on the current resource being defined) * An object of getter and reducer helper functions (to avoid having to manipulate the internal structure directly) The helper object contains the following methods: * `getItemStatus(state, params)`: Returns the status of an item by providing its params * `mergeItemStatus(state, params, newStatus)`: Returns a copy of current resource's redux state with an item's status merged with new values * `getItemValues(state, params)`: Returns the values of an item by providing its params * `mergeItemValues(state, params, newValues)`: Returns a copy of current resource's redux state with an item's values merged with new values * `replaceItemValues(state, params, values)`: Returns a copy of current resource's redux state with an item's values replaced by new values * `clearItemValues(state, params)`: Returns a copy of current resource's redux state with an item's values cleared * `clearItem(state, params)`: Returns a copy of current resource's redux state with an item omitted * `getItemMetadata(state, params)`: Returns the metadata of an item by providing its params * `mergeItemMetadata(state, params, metadata)`: Returns a copy of current resource's redux state with an item's metadata merged with new metadata * `replaceItemMetadata(state, params, metadata)`: Returns a copy of current resource's redux state with an item's metadata replaced by new metadata * `clearItemMetadata(state, params)`: Returns a copy of current resource's redux state with an item's metadata cleared * `getListStatus(state, params)`: Returns the status of an list by providing its params * `mergeListStatus(state, params, newStatus)`: Returns a copy of current resource's redux state with an list's status merged with new values * `getListPositions(state, params)`: Returns the positions of an list by providing its params * `removeItemFromListPositions(state, listParams, itemParams)`: Returns a copy of current resource's redux state with item's key removed from the list specified * `replaceListPositions(state, params, positions)`: Returns a copy of current resource's redux state with an list's positions replaced by new positions * `getListMetadata(state, params)`: Returns the metadata of an list by providing its params * `mergeListMetadata(state, params, metadata)`: Returns a copy of current resource's redux state with a list's metadata merged with new metadata * `replaceListMetadata(state, params, metadata)`: Returns a copy of current resource's redux state with a list's metadata replaced by new metadata * `clearListMetadata(state, params)`: Returns a copy of current resource's redux state with a list's metadata cleared * `clearList(state, params)`: Returns a copy of current resource's redux state with a list omitted * `deselectItem(state, params)`: Returns a copy of current resource's redux state with the item no longer selected * `deselectItems(state, params[])`: Returns a copy of current resource's redux state with the items specified no longer selected * `selectAnotherItem(state, params)`: Returns a copy of current resource's redux state with an item selected (without clearing those already selected) * `selectMoreItems(state, params[])`: Returns a copy of current resource's redux state with the items selected (without clearing those already selected) * `selectItem(state, params)`: Returns a copy of current resource's redux state with only a single item selected * `selectItems(state, params[])`: Returns a copy of current resource's redux state with only the listed items selected * `clearSelectedItems(state, params)`: Returns a copy of current resource's redux state with no items selected * `clearResource()`: Returns an empty singular resource state, for clearing the entire resources * `clearResources()`: Returns an empty resource state, for clearing the entire resource ### Action Options API `actionOptions` are used to configure individual resource actions and override any options specified in `globalOptions` or `resourceOptions`. They are the most specific level of options available at the time that resources are defined and can only be superseded by options provided to action creators when they are called. #### Usage ```javascript import { resources } from 'redux-and-the-rest'; const { actionCreators: { fetchList: fetchUsers } } = resources( { // ... }, { fetchList: { // actionOptions }, fetch: { // actionOptions } } ); ``` #### Options ##### Naming and indexing | key | Type | Required or Default Value | Description | | --- | ---- | ------------------------- | ----------- | | `keyBy` | string | `resourceOptions.keyBy` | The key to index all items on for this particular action. | ##### Synchronising with a remote API | key | Type | Required or Default Value | Description | | --- | ---- | ------------------------- | ----------- | | `url` | string |`resourceOptions.url` | The URL template to use for this particular action. | | `urlOnlyParams` | string[] | `resourceOptions.urlOnlyParams` | The attributes passed to the action creator that should be used to create the request URL, and ignored when storing the result in the store. | | `responseAdaptor` | Function | Identity function | Function used to adapt the response for a particular request before it is handed over to the reducers. The function must return the results as an object with properties `values` and (optionally) `error` or `errors`. | | `requestAdaptor` | Function | Identity function | Function used to adapt the JavaScript object before it is handed over to become the body of the request to be sent to an external API. | | `credentials` | string | undefined | Whether to include, omit or send cookies that may be stored in the user agent's cookie jar with the request only if it's on the same origin. | | `progress` | boolean | false | Whether the store should emit progress events as the resource is uploaded or downloaded. This is applicable to the RESTful actions `fetchList`, `fetchItem`, `createItem`, `updateItem` and any custom actions. | | `metadata` | object | `{ type: 'COMPLETE' }` | An object of attributes and values that describe the list's metadata. It can be used for containing information like page numbers, limits, offsets and includes for lists and types for items (previews, or the complete set of attributes of an item). | | `itemsMetadata` | object | `{ type: 'COMPLETE' }` | Accepted only by `fetchList` and `getOrFetchList`, used to define the metadata of each item in the list (the `metadata` is applied to the list). | ##### Reducers | key | Type | Required or Default Value | Description | | --- | ---- | ------------------------- | ----------- | | `reducer` | Function or String name of action | RESTFUL actions: a sensible default; non-RESTFUL: Required | A custom reducer function to use for the action. Either a Reducer function (accepting the current resource state and the next action as arguments), or the name of one of an action (e.g. 'fetchItem', 'createItem') if you want to re-use one of the standard reducers. By default, the standard RESTful reducer is used for RESTful actions, but this attribute is required for Non-RESTful actions. | | `beforeReducers` | Function[] | [ ] | A list of functions to call before passing the resource to the `reducer`. This is useful if you want to use the default reducer, but provide some additional pre-processing to standardise the resource before it is added to the store. | | `afterReducers` | Function[] | [ ] | A list of functions to call after passing the resource to the `reducer`. This is useful if you want to use the default reducer, but provide some additional post-processing to standardise the resource before it is added to the store. | ## Store data ### Getting items from the store To get an item from a resource, you use the `getItem()` function returned by `resources()`. It will return an [empty item](#item-schema) (instead of `undefined`) if one with the corresponding key does not exist in the store. ```javascript import { serializeKey, ITEM } from `redux-and-the-rest`; import { connect } from 'react-redux'; const { reducers: usersReducers, actionCreators: { fetchList: fetchUsers }, getItem } = resources( { name: 'users', url: 'http://test.com/users/:id?'. keyBy: 'id' }, { fetch: true } ); function mapStateToProps({ users }, { params: { id } }) { return getItem(users, { id }); } ``` ### Automatically fetching items not in the store To get a item or list from the store and fallback to making a request to the remote API if it's not there, use the `getOrFetchItem()` function returned by `resources()`. If the item is in the store, it will return it. However, if it is not there, it will return an [empty item](#item-schema) (instead of `undefined`) and trigger the action(s) to fetch the resource in the background. You can use this function multiple times, across renders and components mounted at the same time, because duplicate actions and requests are ignored, so no unnecessary updates to the store or remote requests will be made. In order for you to use this, a few pre-requisites must be met: You must use the `configure()` function to pass `redux-and-the-rest` the instance of the store after you define it: ```javascript import { configure } from 'redux-and-the-rest'; import { createStore } from 'redux'; const store = createStore(reducers, {}); configure({ store }); ``` And you must define a `fetchItem` action when defining your resource: ```javascript import { serializeKey, ITEM } from `redux-and-the-rest`; import { connect } from 'react-redux'; const { reducers: usersReducers, actionCreators: { fetchList: fetchUsers }, getOrFetchItem } = resources( { name: 'users', url: 'http://test.com/users/:id?'. keyBy: 'id', }, { fetch: true, } ); ``` `getOrFetchItem()` expects the current resources state (the part of the Redux store that contains your resources data) as its first argument. The second argument is the params object that will be serialized to generate the item or list'