redux-crud-factory v0.4.4
Redux Crud Factory
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.
Features
- 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
booksare ordered byauthorandbooksreceived from the backend have anauthorkey. By supplying aparent = 'author'option in theconfigtobooks, allbookobjects received from the backend will be assumed to have thisparentkey and are ordered by the value of the objectsparentkey in the state. This creates multiple separated states for each value ofparent(i.e. for each author). By supplying theparentas 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.
The simplest full CRUD can be created like this
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: {…}},
}The object factory contains the following components
actionTypes:
All the Redux action types, for instance
{ getList: 'GET_FARM_ANIMALS_LIST', create: 'CREATE_FARM_ANIMAL', ... }. Note that namefarmAnimalsis used to create human readable Redux action types. Single/plural is automatically handled including words like category/categories.
actions:All available functions that can trigger Redux actions with formatted names:
{ getFarmAnimalsList: ƒ, createFarmAnimal: ƒ, updateFarmAnimal: ƒ, ... }.
actionsStripped:Same as
actionsabove but with stripped down names:{ getList: ƒ, create: ƒ, update: ƒ, ... }.
mapToProps:The functions that gets data from the store into our React component:
{ farmAnimalsList: { ... }, farmAnimalsIsLoading: false, farmAnimalsHasErrored: false, ... }. The formatted lis
mapToPropsStripped:Same as
mapStateToPropshowever with stripped down names:{ list: { ... }, getListIsLoading: false, getListHasErrored: false, ... }.
reducers:The Redux reducer function that will handle state managementfarmAnimals: ƒ }
. This object can be easily used withcombineReducersfrom Redux (see example below) and leads to a *single source of truth* for the object name:combineReducers({ ...factory.reducers, other: otherReducer })`
config:The same
configobject as supplied however expanded with all the available options.
Connect to the redux store
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>
);
};Receive data from an api
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'
}
]Objects in the redux store
{
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.
Get data to components
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;1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago