This document is intended as a quick-reference for the most popular features of redux-and-the-rest
. It's not necessarily complete, and is intended to be read in conjunction with the more comprehensive Readme file, where required.
resources/users.js
:
import { resources } from 'redux-and-the-rest';
// Define a users resource
const {
// Dispatchers
getOrFetchList: getOrFetchUsers,
getOrFetchItem: getOrFetchUser,
saveItem: saveUser,
destroyItem: destroyUser,
// Reducers
reducers: usersReducers
} = resource({
url: 'http:s//example.com/api/v1/users/:id'
}, {
// Enable index, show, new, edit and destroy RESTful actions, respectively
fetchList: true,
fetchItem: true,
newItem: true,
editItem: true,
destroyItem: true
})
export default {
// Export the helpers you'll want to call in your application
getOrFetchUsers,
getOrFetchUser,
saveUser,
destroyUser,
// Export the reducers you'll need to configure Redux with
usersReducers
};
store.js
:
import { combineReducers, createStore, compose } from 'redux';
import { configure } from 'redux-and-the-rest';
import Thunk from 'redux-thunk';
import { usersReducers } from '../resources/users.js'
// Configure what part of the Redux store the resource's reducers should run on
const reducers = combineReducers({
users: usersReducers
});
// Enable the redux-thunk Middleware
const reduxMiddleware = applyMiddleware([Thunk]);
const store = createStore(reducers, {}, compose(reduxMiddleware))
// Configure redux-and-the-rest with a reference to your store
configure({ store });
export default store;
App.js
:
import { Route } from 'react-router-dom';
import UsersIndexScreen from '../screens/users/UsersIndexScreen'
import UserShowScreen from '../screens/users/UserShowScreen'
import NewUserScreen from '../screens/users/NewUserScreen'
import EditUserScreen from '../screens/users/EditUserScreen'
// Configure RESTful routes
const App = () => {
return(
<Switch>
<Route exact path='users'>
<UsersIndexScreen />
</Route>
<Route exact path='users/:id'>
<UserShowScreen />
</Route>
<Route exact path='users/new'>
<NewUserScreen />
</Route>
<Route exact path='users/:id/edit'>
<EditUserScreen />
</Route>
</>
);
}
export default App;
Structure your application so that it is conformant with the RESTful specification as possible:
URL
- Requires no URL parameters (unless a nested resource) e.g.
/users/
Content
- Screen named something like
UsersIndexScreen
- Displays the list of results - typically with the options to view, edit or destroy those items (these options may also only made available when viewing the item in the show page)
- Pagination, filtering/search and sorting links or UI may also be available and are encoded as URL query parameters, so each filter can be linked to directly
Helpers
- Calls
getOrFetchList()
to retrieve a list (possibly filtered, sorted or paginated) of results if it doesn't already exist in the store. You can also chose to usefetchList()
if you want to always fetch the list anew, each time a component mounts, for example.
Preloader
- While fetching initial list (and each page or change of filter):
isFetching(list)
and thenisFinishedFetching(list)
andisSuccess(list)
- May also need to fetch associated resources (e.g. current user)
Errors
- Errors with fetching the list (server error, unauthenticated, etc):
isFinishedFetching()
andisError()
(and possiblygetHttpStatus()
)
Example:
import React from 'react';
import { isFetching, isFinishedFetching, isSuccess, isError } from 'redux-and-the-rest';
import { connect } from 'react-redux';
import { getOrFetchUsers } from '../../resources/users';
const UsersIndexScreen = ({ users }) => {
if (isFetching(users)) {
// Return preloader
}
if (isFinishedFetching(users)) {
if (isError(users)) {
// Return error, using users.status.error or possiblyg using getHttpStatus(users)
}
if (isSuccess(users)) {
return users.items.map(({ values }) => {
// Render user in JSX
});
}
}
}
const mapReduxStateToProps = ({ users }) => {
return {
// Returns list of users from Redux store, or empty collection while they're being fetched
users: getOrFetchUsers(users),
};
}
export default connect(mapReduxStateToProps)(UsersIndexScreen);
URL
- Values used to fetch and index your collections (such as sort, filter and pagination params) should be stored and retrieved from the current route’s url and query parameters: E.g.
/users?type=premium&status=confirmed
- A user should be able to navigate to a particular route, have the correct components mount, which pass some or all of those parameters to
redux-and-the-rest
fetch helpers
Content
- Usually part of, or re-use your components and UI for the index route
- Typically provide the UI widgets for changing the current URL or its query parameters (which, because they’re then passed to
fetchList()
will cause the results of the new filter to be re-fetched)
Helpers
- Get the sort and filter params from the URL query parameter and pass them to
getOrFetchList(params)
- as long as they don’t match the resource action's url params, they’re encoded as query parameters on the request for the resource list, and are used to index where in the redux store the results are located (so you don’t have to worry about getting the wrong results if your filter changes)
Preloader
- While fetching initial list (and each change of filter):
isFetching(list)
and thenisFinishedFetching(list)
andisSuccess(list)
Errors
- Errors with fetching the list (server error, unauthenticated, etc):
isFinishedFetching()
andisError()
(and possiblygetHttpStatus()
)
Example:
import React from 'react';
import { isFetching, isFinishedFetching, isSuccess, isError } from 'redux-and-the-rest';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import queryString from 'query-string';
import { getOrFetchUsers } from '../../resources/users';
const UsersIndexScreen = ({ users, queryParams, pathname }) => {
if (isFetching(users)) {
// Return preloader
}
if (isFinishedFetching(users)) {
if (isError(users)) {
// Return error, using users.status.error or possiblyg using getHttpStatus(users)
}
if (isSuccess(users)) {
return(
<>
<Link
to={{ pathname, search: queryString.stringify({ ...queryParams, archived: 1 }) }}
>
Show archived
</Link>
{
users.items.map(({ values }) => {
// Render user in JSX
})
}
</>
);
}
}
}
const mapReduxStateToProps = ({ users }, { location: { pathname, search } }) => {
// Parse the query string into a hash
const queryParams = queryString.parse(location.search);
return {
// Returns list of filtered users from Redux store, or empty collection while they're being fetched
users: getOrFetchUsers(users, queryParams),
queryParams,
pathname
};
}
export default withRouter(connect(mapReduxStateToProps)(UsersIndexScreen));
When you have page links, and the user views pages one at a time, and can jump to pages other than the next one.
URL
- The current page should be among the URL query parameters: E.g.
/users?page=1
Content
- One page of content should be visible at once, with links to the subsequent pages (if applicable) - each changing the current query parameter value that keeps track of the page
Helpers
- The pagination parameters should be part of how the list is stored and retrieved (you’ll maintain separate lists in the Redux store - one for each page) so you need to get it out of the current url’s query params and pass it to
getOrFetchList(params)
Preloader
- While fetching the initial page (and each subsequent page) display a preloader:
isFetching(list)
and thenisFinishedFetching(list)
andisSuccess(list)
Metadata
- Store the total number of results or pages in the list’s
metadata
(for the purpose of displaying the correct number of page links and knowing when the user is on the final page of results)
Errors
- Errors with fetching a page (server error, unauthenticated, etc):
isFinishedFetching()
andisError()
(and possiblygetHttpStatus()
)
Example
import React from 'react';
import { isFetching, isFinishedFetching, isSuccess, isError, getListMetadata } from 'redux-and-the-rest';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import queryString from 'query-string';
import { getOrFetchUsers } from '../../resources/users';
const UsersIndexScreen = ({ users, queryParams, pathname }) => {
if (isFetching(users)) {
// Return preloader
}
if (isFinishedFetching(users)) {
if (isError(users)) {
// Return error, using users.status.error or possiblyg using getHttpStatus(users)
}
if (isSuccess(users)) {
const { totalPages } = getListMetadata(users);
return(
<>
{
users.items.map(({ values }) => {
// Render user in JSX
})
}
Page:
{ Array(totalPages).fill().map((_, pageNumber) => {
return(
<Link
to={{ pathname, search: queryString.stringify({ ...queryParams, page: pageNumber }) }}
>
{pageNumber}
</Link>
);
})
}
</>
);
}
}
}
const mapReduxStateToProps = ({ users }, { location: { pathname, search } }) => {
// Parse the query string into a hash
const queryParams = queryString.parse(location.search);
return {
// Returns list of filtered users from Redux store, or empty collection while they're being fetched
users: getOrFetchUsers(users, queryParams),
queryParams,
pathname
};
}
export default withRouter(connect(mapReduxStateToProps)(UsersIndexScreen));
When you provide an infinite scroll of content (and it’s not possible jump to another page other than the next one)
URL
- You can chose to dynamically update the current URL as the user scrolls to change a page query parameter, or you can store the current page in some other storage mechanism, like a component’s state if you don’t want a user to be able to navigate directly to a place in the infinite scroll
Helpers
- Add your pagination parameters to the
urlOnlyParams
list to exclude them from the keys used to index a collection - Still pass the current page to
getOrFetchList(params)
- the page number will be included in the request for the next page from the remote API, but the results returned will be appended to those of the previous pages
Preloader
- While fetching the initial page display a preloader and while fetching the next page, display the previous pages' items, with a preloader at the bottom:
isFetching(list)
and thenisFinishedFetching(list)
andisSuccess(list)
Metadata
- Store the total number of results or pages in the list’s
metadata
(for the purpose of knowing when the user is on the final page of results)
Errors
- Errors with fetching a page (server error, unauthenticated, etc):
isFinishedFetching()
andisError()
(and possiblygetHttpStatus()
)
URL
- Requires an id url parameter, e.g.
/users/1
that points to the resource item being viewed
Content
- Screen named something like
UserShowScreen
- Displays all the attributes of the item (perhaps restricted by a user’s role) and buttons for editing or deleting the resource item, where appropriate
Helpers
- Either the user has come via the index page and already has the item in the Redux store, or they are visiting the page directly it needs to be fetched. Use
getOrFetchItem()
to handle either case, with the itemid
from the URL parameter
Preloader
- While fetching the item:
isFetching(item)
and thenisFinishedFetching(item)
andisSuccess(item)
- May also need to fetch associated resources (e.g. current user)
Errors
- Errors with fetching the item (server error, unauthenticated, etc):
isFinishedFetching(item)
andisError(item)
(and possiblygetHttpStatus(item)
)
Example:
import React from 'react';
import { isFetching, isFinishedFetching, isSuccess, isError } from 'redux-and-the-rest';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { getOrFetchUser } from '../../resources/users';
const UsersShowScreen = ({ user }) => {
if (isFetching(user)) {
// Return preloader
}
if (isFinishedFetching(user)) {
if (isError(user)) {
// Return error, using user.status.error or possiblyg using getHttpStatus(user)
}
if (isSuccess(user)) {
// Render user in JSX
}
}
}
const mapReduxStateToProps = ({ users }, { match: { id } }) => {
return {
// Returns list of users from Redux store, or empty collection while they're being fetched
user: getOrFetchUser(users, id),
};
}
export default withRouter(connect(mapReduxStateToProps)(UsersShowScreen));
URL
- Doesn't require a url parameter (the resource item hasn't been given one yes) e.g.
/users/new
Content
- Screen named something like
NewUserScreen
- May be a single form or UI for creating a new resource item all on one screen or a multi-step wizard (e.g.
/users/new/details
leads to/users/new/address
) - Depending on your application’s needs, the user is taken back to the show or index screen once the item has been created
Helpers
- You can initialise the form with default values using
getOrInitializeItem()
- For a single form or UI for creating a new resource item all on one screen, call
createItem()
at the end of it; for a wizard calleditNewItem()
with the new attribute values defined as each form is submitted, and thencreateItem()
at the end, with all the attributes of the new item (not just those specified on the final screen) - To fetch associated resources to pick from (to populate things like select fields):
getOrFetchList()
- When a user cancels creating a new item, you can call
clearNewItem()
to reset back to a clean slate
Automatic routing
- To determine when the item has been created:
isFinishedCreating()
andisSuccess()
and then redirect to the show or index page
Preloader
- While associated resources to pick from (to populate things like select fields):
isFetching(list)
and thenisFinishedFetching(list)
andisSuccess(item)
- May also need to fetch associated resources (e.g. current user)
- While the resource is being created on the server:
isCreating(item)
and thenisFinishedCreating(item)
andisSuccess(item)
Errors
- Errors with creating the item (server error, unauthenticated, invalid attribute values, etc):
isFinishedCreating(item)
andisError(item)
(and possiblygetHttpStatus(item)
) to handle the errors appropriately - usually re-rendering the form with validation errors
The endpoint that createItem()
is configured to make a call to by specifying a create action when defining the resources()
.
- Returns the new item, complete with server-allocated id and all of it’s new attributes
URL
- Require’s the resource item’s id as a URL parameter e.g.
/users/1/edit
- May be a single form or UI for creating a new resource item all on one
Content
- Screen named something like
EditUserScreen
- May be a single form or UI for updating an existing resource item all on one screen or a multi-step wizard (e.g.
/users/1/edit/details
leads to/users/1/edit/address
) - Depending on your application’s needs, the user is taken back to the show or index screen once the item has been updated
Helpers
- You getting the current item’s attributes:
getOrFetchItem()
- For a single form or UI for creating a new resource item all on one screen, call
updateItem()
at the end of it; for a wizard calleditItem()
with the new attribute values defined as each form is submitted and thenupdateItem()
at the end, with all the attributes of the updated item - To fetch associated resources to pick from (to populate things like select fields):
getOrFetchList()
- When a user cancels editing an item, you can call
clearNewItem()
to reset back to the un-edited values
Automatic routing
- To determine when the item has been updated:
isFinishedUpdating()
andisSuccess()
and then redirect to the show or index page
Preloader
- While associated resources to pick from (to populate things like select fields):
isFetching(list)
and thenisFinishedFetching(list)
andisSuccess(item)
- May also need to fetch associated resources (e.g. current user)
- While the resource is being updated on the server:
isUpdating(item)
and thenisFinishedUpdating(item)
andisSuccess(item)
Errors
- Errors with updating the item (server error, unauthenticated, invalid attribute values, etc):
isFinishedUpdating(item)
andisError(item)
(and possiblygetHttpStatus(item)
) to handle the errors appropriately - usually re-rendering the form with validation errors
The edit and new pages can use the same components by using the generic versions of the redux-and-the-rest functions:
editNewOrExistingItem()
instead ofeditNewItem()
andeditItem()
saveItem()
instead ofcreateItem()
andupdateItem()
isFinishedSaving()
instead ofisFinishedCreating()
andisFinishedUpdating()
Your application still has to handle the logic for deciding whether to call getOrInitializeItem()
(usually when there is no id
URL parameter) or getOrFetchItem()
(usually when there is an id
URL parameter)
Example:
const UserForm = ({ user }) => {
if (isSaving(user)) {
// Return preloader
}
if (isFinishedSaving(user)) {
if (isError(user)) {
// Re-render form with validation errors
}
// The container will automatically redirect away instead of rendering the component once the user
// has been successfully created
return null;
}
// Render form for creating or updating a user
}
export default UserForm;
New user form
import React from 'react';
import { isSaving, getItemValues, isFinishedSaving, isSuccess, isError } from 'redux-and-the-rest';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { createUser, getNewOrInitializeUser } from '../../resources/users';
import UserForm from './UserForm';
const mapReduxStateToProps = ({ users }, { history }) => {
const user = getNewOrInitializeUser(users, { username: 'user' + Math.random() });
if (isFinishedSaving(user) && isSuccess(user)) {
// Automaticaly redirect to the user show page when it's created
history.replace('/users/' + getItemValues(user).id);
}
return {
user,
onSubmit: createUser
};
}
export default withRouter(connect(mapReduxStateToProps)(UserForm));
Edit user form
import React from 'react';
import { isSaving, getItemValues, isFinishedSaving, isSuccess, isError } from 'redux-and-the-rest';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { updateUser, getOrFetchUser } from '../../resources/users';
import UserForm from './UserForm';
const mapReduxStateToProps = ({ users }, { history, match: { id } }) => {
const user = getOrFetchUser(users, { id });
if (isFinishedSaving(user) && isSuccess(user)) {
// Automaticaly redirect to the user show page when it's created
history.replace('/users/' + id);
}
return {
user,
onSubmit: (newValues) => updateUser(id, { ...getItemValues(user), ...newValues})
};
}
export default withRouter(connect(mapReduxStateToProps)(UserForm));
The endpoint that updateItem()
is configured to make a call to, by specifying a update action when defining the resources()
.
- Returns the updated item, with all of it’s new attribute values
The endpoint that destroyItem()
is configured to make a call to, by specifying a destroy action when defining the resources()
.
- Returns a http success message if completed
Usually these are "sub-updates" or special updates that involve some or all of an item’s attributes, with some extra domain logic or meaning on the remote API: E.g. an Archive button
URL
- Not required
Content
- A button (e.g. "Archive")
Helpers
- The helpers returned by defining a custom (e.g.
archiveItem()
) to perform a custom action - usually these re-use the same status values and reducers as either create or update
Preloader
- Because custom actions usually re-use the same status values as create or update, you can use the same
isSaving(item)
orisSyncing(item)
and thenisFinishedSaving(item)
orisFinishedSyncing(item)
andisSuccess(item)
Errors
- Errors with performing the action (server error, unauthenticated, etc):
isFinishedSaving(item)
orisFinishedSyncing(item)
andisError()
(and possiblygetHttpStatus()
)
These are usually summary or preview views of a subset of aggregation of data available from one or more endpoints
URL: A sub-route of the resource item they relate to
/users/1/preview
/users/1/progress
Content
- Screen named something like matches URL
UsersPreviewScreen
- Whatever display of data and client-side functionality is appropriate
Helpers
- Either the user has come via the index page and already has the item in the redux store, or they are visiting the page directly and you need to call
getOrFetchItem()
with the id from the URL parameter
Preloader
- While fetching the item:
isFetching(item)
and thenisFinishedFetching(item)
andisSuccess(item)
- May also need to fetch associated resources (e.g. current user)
Errors
- Errors with fetching the item (server error, unauthenticated, etc):
isFinishedFetching(item)
andisError(item)
(and possiblygetHttpStatus(item)
)
These expect the current resource state as their first argument (not the entire Redux store state), and usually some extra arguments to help filter their behaviour
They are convenience methods with the appropriate context and knowledge of the internal redux-and-the-rest schema to get the correct nested data
They’re exported at the top of the resource definition returned by resources()
.
These are your typical Redux action creators that can return either an action object or a thunk (function)
- They accept
params
,values
andoptions
, depending on the function - They return a value that should be passed to Redux’s
dispatch()
function
They’re exported in the actionCreators
object of the resource definition returned by resources
These are like action creators, but they already have access to Redux’s dispatch
function in scope and will handle calling it for you.
They have the same name and method signature as their action creator counterparts, but can be used in scopes where dispatch is not readily available
They’re exported at the top of the resource definition returned by resources()
Many helpers have variable arguments, with the first argument (params
) being optional. So if you chose to provide a single argument, it will be interpreted as the values
and the params
value will be:
- Deduced for creating new resources from an id maintained internally
- Not used at all for singular resources
So for most helpers, variable arguments works like so:
helper(values)
helper(params, values)
helper(params, values, options)
Exceptions are helpers that don’t accept params
at all, like fetchList()
:
helper(values)
helper(values, options)
Finally, getters - or helpers that wrap getters - require the current Redux state for the resource as the first argument, and the second argument onwards behaves as described above.
Most redux-and-the-rest methods accept params
. These are used to index items and collections internally, and to generate the URLs to perform requests
- They can be objects, strings or integers.
- Internally, objects are stringified by normalizing their attributes and values
- Objects keys are matched against the url parameters defined for a resource or action, performing string substitution where there is a match
- String values behave as if they were wrapped in an object, with a key of id
Helpers that update the store or generate POST or PUT requests accept values
- For local changes, these values are merged into those already in the store
- For remote changes, all of the values to send to the remote API must be provided (depending on whether your API needs or all values to update a resource or not, you may still provide only those values that have changed)
- This is because action creators (which perform the async requests) do not have access to the current values in the store - they must be told them as parameters
- Of particular note is that if your user has edited a resource across multiple steps, you must make sure all values that have changed locally are submitted - not just those in the final step.
Some helpers accept a 3rd argument of options to configure their behaviour, however most configuration is usually done when defining a resource and is consistent between calls to the helper.
Level | Description |
---|---|
Global | Applies to all resources and all actions (where applicable), but can be overridden by one of the the more specific configuration points. |
Resource | Applies to a resource and all of its actions - uses the first argument of resources |
Action | Applies to a single action, and is part of the action options when defining a resource |
Action Creator | Applies for a specific call of an action creator and is defined as the final argument of calling an action creator |
Example:
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
// ...
})