Redux Crud Factory is a declarative toolkit that allows for creating CRUD
(create, read, update, and delete) actions that allow a React
app to interact with a backend api
. State management is handled by Redux
using Redux Thunk
middleware. The api calls are performed using Axios
. The backend can be as simple as an api
based on a ViewSet
using the Django Rest Framework
.
- Request a list of objects from the backend, create a new object, modify or delete an existing object in the list. Either one of these operations automatically modify the state (redux store) and the components will be updated.
- Allow for nested state: Imagine
books
are ordered byauthor
andbooks
received from the backend have anauthor
key. By supplying aparent = 'author'
option in theconfig
tobooks
, allbook
objects received from the backend will be assumed to have thisparent
key and are ordered by the value of the objectsparent
key in the state. This creates multiple separated states for each value ofparent
(i.e. for each author). By supplying theparent
as a prop to a component, either as an id or an object, this component will receive the parents child state:<BooksList author="Stephen King" />
. - Select single object (think radio box) or multiple objects (think check box): The list of objects, either with or without parents, can be selected by adding a
select = 'single'
orselect = 'multiple'
option toconfig.actions
.
import reduxCrudFactory from 'redux-crud-factory';
import axios from 'axios';
// Our object name 'farmAnimals' must be camelCase
export const factory = reduxCrudFactory({
axios,
config: {
farmAnimals: {
route: 'https://example.com/api/farm-animals',
actions: {
create: true, // createFarmAnimal(obj) will perform post request to https://example.com/api/farm-animals
get: true, // getFarmAnimal(42) will perform get request to https://example.com/api/farm-animals/42
getList: true, // getFarmAnimalsList() will perform get request to https://example.com/api/farm-animals
update: true, // updateFarmAnimal(obj) will perform patch request to https://example.com/api/farm-animals/42
delete: true, // deleteFarmAnimal(obj) will perform delete request to https://example.com/api/farm-animals/42
},
},
},
});
Or generate more elaborate cruds with many bells and whistles
import reduxCrudFactory from 'redux-crud-factory';
import axios from 'axios';
import otherAxios from './OtherAxios';
// Our object name 'farmAnimals' must be camelCase
export const factory = reduxCrudFactory({
axios: axios.create({ // Default axios instance that is used for each factory in the config object
baseURL: 'https://example.com'
}),
onError: console.error, // Log errors to console or use react-toastify
actions: { // Default actions for all the factories
get: true,
getList: true,
create: true,
update: true,
delete: true,
},
config: {
farmAnimals: {
route: '/api/farm-animals/', // Trailing slash here
actions: { get: true }, // Duplicate get action as it is already available, can be removed
},
plants: {
route: '/api/plants', // No trailing slash on this route
actions: { delete: false }, // Add or disable actions if you like to don't repeat yourself
axios: otherAxios, // Maybe this route needs authentication
includeActions: { // Create custom actions!
sellPlant: { // The following functions are now generated: getPlant, getPlantsList, createPlant, updatePlant & sellPlant
method: 'post', // Any http method required
route: ({ id }) => // route can be string or function called when you call sellPlant(plant, { args: { your stuff.. }, params ))
`/api/plants/${id}/sell`, // Request params are handled automatically, args can be used in route, prepare or onResponse
prepare: (plant, { args, params } => // Do something with additional args or params before data is sent to api
({ ...plant }),
// Handle response.data from api.
onResponse: (plant, { updatePlant, getFarmAnimalsList, params, args }) =>
{
updatePlant(plant); // All redux actions are available here. Update the plant in the state
getFarmAnimalsList({ // Also request farmAnimals based on this plant id. State update will be automatic as
params: plant: plant.id, // getFarmAnimalsList() is a standard action
});
},
},
},
},
},
},
});
Show what we got in the console
> console.log(factory)
{
actionTypes: {farmAnimals: {…}, plantsAndVegetables: {…}},
actions: {farmAnimals: {…}, plantsAndVegetables: {…}},
actionsStripped: {farmAnimals: {…}, plantsAndVegetables: {…}},
mapToProps: {farmAnimals: ƒ, plantsAndVegetables: ƒ},
mapToPropsStripped: {farmAnimals: ƒ, plantsAndVegetables: ƒ},
reducers: {farmAnimals: ƒ, plantsAndVegetables: ƒ},
config: {farmAnimals: {…}, plantsAndVegetables: {…}},
}
All the Redux action types, for instance
{ getList: 'GET_FARM_ANIMALS_LIST', create: 'CREATE_FARM_ANIMAL', ... }
. Note that namefarmAnimals
is used to create human readable Redux action types. Single/plural is automatically handled including words like category/categories.
All available functions that can trigger Redux actions with formatted names:
{ getFarmAnimalsList: ƒ, createFarmAnimal: ƒ, updateFarmAnimal: ƒ, ... }
.
Same as
actions
above but with stripped down names:{ getList: ƒ, create: ƒ, update: ƒ, ... }
.
The functions that gets data from the store into our React component:
{ farmAnimalsList: { ... }, farmAnimalsIsLoading: false, farmAnimalsHasErrored: false, ... }
. The formatted lis
Same as
mapStateToProps
however with stripped down names:{ list: { ... }, getListIsLoading: false, getListHasErrored: false, ... }
.
The Redux reducer function that will handle state managementfarmAnimals: ƒ }
. This object can be easily used with
combineReducersfrom Redux (see example below) and leads to a *single source of truth* for the object name:
combineReducers({ ...factory.reducers, other: otherReducer })`
The same
config
object as supplied however expanded with all the available options.
import { Provider } from 'react-redux';
import { applyMiddleware, createStore, combineReducers } from 'redux';
// Redux thunk is required middleware
import thunk from 'redux-thunk';
// Log each redux action without changing the state. Not required but this allows us to see what's going on under the hood.
const consoleLogReducer = (state = null, { type, ...action }) => {
console.log(type, action, state);
return state;
}
const rootReducer = (state, action) => consoleLogReducer(
combineReducers({
...factory.reducers
// Add more reducers here...
})(state, action),
action
);
// The `Root` component used in our React App.
const Root = ({ children, initialState = {} }) => {
const middleware = [thunk];
const store = createStore(
rootReducer(,
initialState,
applyMiddleware(...middleware)
);
return (
<Provider store={store}>
{children}
</Provider>
);
};
Here the data will be saved in the redux store like this { farmAnimals: { ... } }
. An axios
instance is required and needs to be supplied.
In the simple example above the specification for a complete CRUD
are created. The api response is assumed to be:
[
{
id: 1,
type: 'donkey',
name: 'Benjamin'
},
{
id: 2,
type: 'goat',
name: 'Muriel'
}
]
{
farmAnimals: {
list: {
1: {
id: 1,
type: 'donkey',
name: 'Benjamin',
},
2: {
id: 2,
type: 'goat',
name: 'Muriel',
},
},
createError: null,
createIsLoading: false,
deleteError: null,
deleteIsLoading: false,
getListError: null,
getListIsLoading: false,
updateError: null,
updateIsLoading: false
},
}
Note that the list object is not an array but a key/value pair based on the id
even though the api
returns a list. Of course this id
field can be modified.
Now we can get the data and Redux functions in our component (FarmAnimalsList.js
).
// import farmAnimalsFactory from ...
import { Component } from 'react';
import { connect } from 'react-redux';
// Feel free to use functional components instead.
class FarmAnimalsList extends Component {
componentDidMount() {
this.props.getFarmAnimalsList();
}
render() {
const { getFarmAnimalsIsLoading, farmAnimalsList, createFarmAnimal } = this.props;
if (getFarmAnimalsIsLoading || !farmAnimalsList) return 'Loading farm animals...';
return <ul>
{Object.values(farmAnimalsList).map((farmAnimal, key) =>
<li key={key}>
The {farmAnimal.type} is called {farmAnimal.name}
</li>
)}
</ul>;
}
};
export default connect(
factory.mapToProps.farmAnimals,
factory.actions.farmAnimals
)(FarmAnimalsList);
and
import React, { Component } from 'react';
import FarmAnimalsList from './FarmAnimals';
class App extends Component {
render() {
return (
<Root>
<FarmAnimalsList />
</Root>
);
}
}
export default App;