Security News
Weekly Downloads Now Available in npm Package Search Results
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.
THIS WAS ORIGINALLY A FORK OF REDUX CRUD (https://github.com/Versent/redux-crud) AND WAS FORKED AGGRESSIVELY ENOUGH TO SEPARATE ENTIRELY. BELOW IS MOSTLY THE OLD README + FIND AND REPLACE.
Redux Pony is a convention driven way of building CRUD applications using Redux. After building several Flux applications we found that we always end up creating the same action types, actions and reducers for all our resources.
Redux Pony gives you an standard set of:
USER_UPDATE_SUCCESS
updateSuccess
, updateError
updateSuccess
Redux Pony uses seamless-immutable.
When building an app you might have resources like users
, posts
and comments
.
You'll probably end up with action types for them like:
USERS_FETCH_SUCCESS
POSTS_FETCH_SUCCESS
COMMENTS_FETCH_SUCCESS
And action creators like:
users.fetchSuccess
posts.fetchSuccess
comments.fetchSuccess
There's obvious repetition there. Redux Pony aims to remove this boilerplate by providing strong conventions on naming and processing data.
.actionTypesFor
Creates an object with standard CRUD action types:
var reduxCrud = require('redux-crud');
var actionTypes = reduxCrud.actionTypesFor('users');
// actionTypes =>
{
USERS_FETCH_START: 'USERS_FETCH_START',
USERS_FETCH_SUCCESS: 'USERS_FETCH_SUCCESS',
USERS_FETCH_ERROR: 'USERS_FETCH_ERROR',
USERS_UPDATE_START: 'USERS_UPDATE_START',
USERS_UPDATE_SUCCESS: 'USERS_UPDATE_SUCCESS',
USERS_UPDATE_ERROR: 'USERS_UPDATE_ERROR',
USERS_CREATE_START: 'USERS_CREATE_START',
USERS_CREATE_SUCCESS: 'USERS_CREATE_SUCCESS',
USERS_CREATE_ERROR: 'USERS_CREATE_ERROR',
USERS_DELETE_START: 'USERS_DELETE_START',
USERS_DELETE_SUCCESS: 'USERS_DELETE_SUCCESS',
USERS_DELETE_ERROR: 'USERS_DELETE_ERROR',
// Object also contains shortcuts
fetchStart: 'USERS_FETCH_START',
fetchSuccess: 'USERS_FETCH_SUCCESS',
fetchError: 'USERS_FETCH_ERROR',
updateStart: 'USERS_UPDATE_START',
updateSuccess: 'USERS_UPDATE_SUCCESS',
updateError: 'USERS_UPDATE_ERROR',
createStart: 'USERS_CREATE_START',
createSuccess: 'USERS_CREATE_SUCCESS',
createError: 'USERS_CREATE_ERROR',
deleteStart: 'USERS_DELETE_START',
deleteSuccess: 'USERS_DELETE_SUCCESS',
deleteError: 'USERS_DELETE_ERROR',
}
.actionCreatorsFor
Generates the following action creators:
fetchStart
fetchSuccess
fetchError
createStart
createSuccess
createError
updateStart
updateSuccess
updateError
deleteStart
deleteSuccess
deleteError
var reduxCrud = require('redux-crud');
var actionCreators = reduxCrud.actionCreatorsFor('users');
// actionCreators =>
{
fetchStart: function(data) {
return {
data: data,
type: 'USERS_FETCH_START',
};
},
fetchSuccess: function(users, data) {
return {
data: data,
records: users,
type: 'USERS_FETCH_SUCCESS',
};
},
fetchError: function(error, data) {
return {
data: data,
error: error,
type: 'USERS_FETCH_ERROR',
};
},
/*
The user record must have a client generated key
so it can be inserted in the collection optimistically.
*/
createStart: function(user, data) {
return {
data: data,
record: user,
type: 'USERS_CREATE_START',
};
},
createSuccess: function(user, data) {
return {
data: data,
record: user,
type: 'USERS_CREATE_SUCCESS',
};
},
/*
The user record must have the client generated key
so it can be matched with the record inserted optimistically.
*/
createError: function(error, user, data) {
return {
data: data,
error: error,
record: user,
type: 'USERS_CREATE_ERROR',
};
},
updateStart: function(user, data) {
return {
data: data,
record: user,
type: 'USERS_UPDATE_START',
};
},
updateSuccess: function(user, data) {
return {
data: data,
record: user,
type: 'USERS_UPDATE_SUCCESS',
};
},
updateError: function(error, user, data) {
return {
data: data,
error: error,
record: user,
type: 'USERS_UPDATE_ERROR',
};
},
deleteStart: function(user, data) {
return {
data: data,
record: user,
type: 'USERS_DELETE_START',
};
},
deleteSuccess: function(user, data) {
return {
data: data,
record: user,
type: 'USERS_DELETE_SUCCESS',
};
},
deleteError: function(error, user, data) {
return {
data: data,
error: error,
record: user,
type: 'USERS_DELETE_ERROR',
};
}
}
data
attributeThe data
attribute in the actions payload is optional. The reducer doesn't do anything with this. This is only provided in case you want to pass extra information in the actions.
.reducersFor
Creates a reducer function for the given resource. Redux Pony assumes that all records will have a unique key, e.g. id
. It generates the following reducers:
fetchSuccess
createStart
createSuccess
createError
updateStart
updateSuccess
updateError
deleteStart
deleteSuccess
deleteError
Note: There are no fetchStart
and fetchError
reducers.
var reduxCrud = require('redux-crud');
var reducers = reduxCrud.reducersFor('users');
// reducers =>
function (state, action) {
switch (action.type) {
case 'USERS_FETCH_SUCCESS':
...
case 'USERS_CREATE_START':
...
case 'USERS_CREATE_SUCCESS':
...
}
}
reducersFor
takes an optional config object as second argument:
reduxCrud.reducersFor('users', {key: '_id', store: reduxCrud.STORE_SI});
config.key
Key to be used for merging records. Default: 'id'.
config.store
Type of store to use. Defaults to seamless-immutable. Options:
fetchSuccess
Listens for an action like this (generated by actionCreatorsFor
):
{
records: users,
type: 'USERS_FETCH_SUCCESS',
}
Takes one record or an array of records and adds them to the current state. Uses the given key
or id
by default to merge.
createStart
Listens for an action like:
{
type: 'USERS_CREATE_START',
record: user,
}
Adds the record optimistically to the collection. The record must have a client generated key e.g. id
, otherwise the reducer will throw an error. This key is necessary for matching records on createSuccess
and createError
. This client generated key is just temporary, is not expected that you will use this key when saving your data in the backend, it is just there so records can be matched.
This action is optional, dispatch this only if you want optimistic creation. Read more about this.
For generating keys see cuid.
Also adds busy
and pendingCreate
to the record so you can display proper indicators in your UI.
createSuccess
Listens for an action like this (generated by actionCreatorsFor
):
{
type: 'USERS_CREATE_SUCCESS',
record: user,
cid: clientGeneratedId
}
Takes one record and adds it to the current state. Uses the given key
(id
by default) to merge.
The cid
attribute is optional but it should be used when dispatching createStart
. This cid
will be used for matching the record and replacing it with the saved one.
createError
Listens for an action like:
{
type: 'USERS_CREATE_ERROR',
record: user,
}
This reducer removes the record from the collection. The record key is used for matching the records. So if a record was added optimistically using createStart
then the keys must match.
updateStart
Listens for an action like this (generated by actionCreatorsFor
):
{
type: 'USERS_UPDATE_START',
record: user
}
Takes one record and merges it to the current state. Uses the given key
or id
by default to merge.
It also add these two properties to the record:
busy
pendingUpdate
You can use this to display relevant information in the UI e.g. a spinner.
updateSuccess
Listens for an action like this (generated by actionCreatorsFor
):
{
type: 'USERS_UPDATE_SUCCESS',
record: user
}
Takes one record and merges it to the current state. Uses the given key
or id
by default to merge.
updateError
Listens for an action like this (generated by actionCreatorsFor
):
{
type: 'USERS_UPDATE_ERROR',
record: user,
error: error
}
This reducer will remove busy
from the given record. It will not rollback the record to their previous state as we don't want users to lose their changes. The record will keep the pendingUpdate
attribute set to true.
deleteStart
Listens for an action like this (generated by actionCreatorsFor
):
{
type: 'USERS_DELETE_START',
record: user
}
Marks the given record as deleted
and busy
. This reducer doesn't actually remove it. In your UI you can filter out records with deleted
to hide them.
deleteSuccess
Listens for an action like this (generated by actionCreatorsFor
):
{
type: 'USERS_DELETE_SUCCESS',
record: user
}
This reducer removes the given record from the store.
deleteError
Listens for an action like this (generated by actionCreatorsFor
):
{
type: 'USERS_DELETE_ERROR',
record: user,
error: error
}
Removes deleted
and busy
from the given record.
Create your action creators by extending the standard actions:
import _ from 'lodash';
import reduxCrud from 'redux-crud';
const standardActionCreators = reduxCrud.actionCreatorsFor('users');
let actionCreators = {
update(user) {
...
}
}
actionCreators = _.extend(actionCreators, standardActionCreators);
export default actionCreators;
Redux Pony only generates sync action creators. Async action creators still need to be added:
const standardActionCreators = reduxCrud.actionCreatorsFor('users');
let actionCreators = {
update(user) {
return function(dispatch) {
// dispatch a `updateStart` for optimistic updates
const action = standardActionCreators.updateStart(user);
dispatch(action);
// send the request
const url = `/users/${user.id}`;
const promise = someAjaxLibrary({
url: url,
method: 'PUT',
data: {
user
}
});
promise.then(function(response) {
// dispatch the success action
const user = response.data.data;
const action = standardActionCreators.updateSuccess(user);
dispatch(action);
}, function(response) {
// rejection
// dispatch the error action
// first param is the error
const action = standardActionCreators.updateError(response, user);
dispatch(action);
}).catch(function(err) {
console.error(err.toString());
});
return promise;
}
},
...
}
Redux Pony generates standard reducers for fetch
, create
, update
and delete
.
Create your Redux application:
import thunkMiddleware from 'redux-thunk';
import loggerMiddleware from 'redux-logger';
import { combineReducers } from 'redux';
import { createStore, applyMiddleware } from 'redux';
import reduxCrud from 'redux-crud';
const createStoreWithMiddleware = applyMiddleware(
thunkMiddleware, // lets us dispatch() functions
loggerMiddleware // neat middleware that logs actions
)(createStore);
const allReducers = combineReducers({
users: reduxCrud.reducersFor('users'),
posts: reduxCrud.reducersFor('posts'),
});
const store = createStoreWithMiddleware(allReducers);
There are many cases when the generated reducers are not enough. For example you might want to delete relevant comments
when a post
is deleted. You can extend a reducer function like this:
// comments/reducers.js
import SI from 'seamless-immutable';
import reduxCrud from 'redux-crud';
const standardReducers = reduxCrud.reducersFor('comments');
function reducers(state=SI([]), action) {
switch(action.type) {
case 'POSTS_DELETE_SUCCESS':
// ...delete comments for the given post and return a new state for comments
return state;
default:
// pass to the generated reducers
return standardReducers(state, action);
}
}
export default reducers;
Then you can use this reducer:
import commentsReducers from './comments/reducers';
const allReducers = combineReducers({
comments: commentsReducers,
posts: reduxCrud.reducersFor('posts'),
});
With React use React-Redux.
Don't atttempt to store nested resources. e.g. {id: 1, posts: [{...}]}
. This makes harder to keep the information in sync with the UI. Instead always normalize the resources when they arrive from the server and store them in collections of their own.
Your API might return something like:
{
id: 1,
label: 'Some post',
comments: [
{id: 1, body: '...'},
{id: 2, body: '...'},
]
}
Instead of trying to work with nested records in your views, you should normalize them in your async action creator:
const baseActionCreators = reduxCrud.actionCreatorsFor('posts');
const baseCommentsActionCreators = reduxCrud.actionCreatorsFor('comments');
fetch() {
return function(dispatch) {
const action = baseActionCreators.fetchStart();
dispatch(action);
const url = `/posts/`;
const promise = someAjaxLibrary({
url: url,
method: 'GET'
});
promise.then(function(response) {
const posts = response.data.data;
const action = baseActionCreators.fetchSuccess(posts);
dispatch(action);
/***********************************************/
/* Get the comments and send them to the store */
const comments = _(posts).map(function(post) {
return post.comments;
}).flatten().value();
const commentsAction = baseCommentsActionCreators.fetchSuccess(comments);
dispatch(commentsAction);
/**********************************************/
}, function(response) {
const action = baseActionCreators.fetchError(response);
dispatch(action);
}).catch(function(err) {
console.error(err.toString());
});
return promise;
}
},
Use a collection of resources and name them using the plural form e.g. users
instead of user
.
Dispatching createStart
, updateStart
and deleteStart
will result in optimistic changes to your store. See the description of what each reducer does above. updateStart
and deleteStart
will just work out of the box. createStart
needs additional code from you.
This is an example async action creator with optimistic creation:
create(user) {
return function(dispatch) {
// Generate a cid so we can match the records
var cid = cuid();
// Add the cid as the primary key
user = user.merge({id: cid});
// Optimistic creation
// This action creator will throw if user doesn't have a primary key
const action = baseActionCreators.createStart(user);
dispatch(action);
// send the request
const url = `/users/`;
const promise = someAjaxLibrary({
url: url,
method: 'POST',
data: {
user
}
});
promise.then(function(response) {
const returnedUser = response.data.data;
// We need to pass the cid as the second argument
const action = baseActionCreators.createSuccess(createdUser, cid);
dispatch(action);
}, function(response) {
const action = baseActionCreators.createError(response, user);
dispatch(action);
}).catch(function(err) {
console.error(err.toString());
});
return promise;
}
},
Note how we need to pass the cid
as the second argument to createSuccess
. If we don't the reducer will not be able to match the records and you will end up with duplicates.
Adding a client generated id
to a record doesn't mean that you need to use that id
for saving it in the backend. You can still generate ids as usual in your DB.
When the record comes back saved from the server the reducer will try to match id
on the optimistically inserted record with cid
on the createSuccess
action. If it finds a match it will replace the optimistically inserted record with the given one. That record will now have the normal id
given by the backend (The client generated id is thrown away at this point).
createStart
and updateStart
will add the following attributes:
busy
and pendingCreate
busy
and pendingUpdate
busy
but leaves pendingUpdate
You can use these special attributes for showing indicators and preventing navigation:
busy
is true.pendingCreate
is true.busy
is false but pendingUpdate
is true.Most likely you will get a seamless-immutable
collection in you components. Don't map over it to create a list because then seamless-immutable
will attempt to make the react components immutable, which doesn't work.
Don't do this:
var lis = records.map(function(record) {
return <li key={record.id}>{record.name}</li>;
});
Instead, use lodash to map or convert to mutable first:
var lis = _.map(records, function(record) {
return <li key={record.id}>{record.name}</li>;
});
or:
var lis = records.asMutable().map(function(record) {
return <li key={record.id}>{record.name}</li>;
});
seamless-immutable
Immutable.js is nice but we prefer seamless-immutable's stronger immutable guarantees.
npm test
You can see a basic example here
1.0 added Immutable.js store
0.10.1 upgrade action-names
dep, remove left over ES6
0.10.0 .reducersFor
does not mutate the config object
0.9.0 Added mutable store (config.store: reduxCrud.STORE_MUTABLE)
0.8.0 Add data
attribute to actions payload.
0.7.0 Replaced unsaved
in createStart and updateStart with pendingCreate
and pendingUpdate
.
FAQs
Standard actions and reducers for the Flip CRUD API.
The npm package redux-pony receives a total of 0 weekly downloads. As such, redux-pony popularity was classified as not popular.
We found that redux-pony demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.
Security News
A Stanford study reveals 9.5% of engineers contribute almost nothing, costing tech $90B annually, with remote work fueling the rise of "ghost engineers."
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.