@rexlabs-spicerhaart/model-generator v1.3.48
Model Generator
A nice abstraction to create Redux actions and reducers and connect your components, with a special focus on dealing with and normalizing data from REST APIs.
Why?
Setting up projects using Redux, even more when dealing API data, is tedious and brings a lot of redux boilerplate with it to get it right. Besides the redundancy of defining CRUD actions for all your API entities, you also need to deal with proper normalisation of the redux store. A lot of repetitive work.
model-generator
tries to solve this problem, sitting on top of redux as an abstraction layer. It provides you with pre-defined CRUD actions, as well as proper state normalisation (incl. dealing with entity relations, grabage collection, etc.). Also it simplifies writing custom actions, synchronous and asynchronous, using the redux-thunk
middleware.
Getting started
Install
$ yarn add @rexlabs/model-generator
Create a Model
Define the model
import ModelGenerator from '@rexlabs/model-generator';
// Default state
const defaultState = {
count: 0
};
// All actions
const actionCreators = {
increase: {
reduce: state => ({
count: state.count + 1
})
},
decrease: {
reduce: state => ({
count: state.count - 1
})
},
// You can also define async actions, in which case you'd need
// to define a reducer for each stage of it
asyncIncrease: {
request: () => new Promise(resolve => setTimeout(resolve, 1000)),
reduce: {
initial: state => state,
success: state => ({ count: state.count + 1 }),
failure: state => state
}
}
};
// The selectors you want to be available when connecting to
// this model
const selectors = {
counter: state => state.counter
}
// Create the model
export default new ModelGenerator('counter').createModel({
defaultState,
actionCreators,
selectors
});
Create the store
import { createStore, combineReducers } from 'redux';
import counterReducer from './redux/models/counter';
// Create store
const store = createStore(
combineReducers({ counter: counterReducer })
);
// NOTE: when you use async actions, you have to apply redux thunk
// as part of your stores middleware!! See entity example below
Connect a component
import React from 'react';
import { withModel } from '@rexlabs/model-generator';
import counter from './redux/models/counter';
// Connect to Component
const ExampleComponent = ({counter}) =>
<div>
<button onClick={counter.decrease}>-</button>
<span>{counter.count}</span>
<button onClick={counter.increase}>+</button>
</div>;
const ConnectedExampleComponent = withModel(counter)(ExampleComponent);
Entity Models
If you have to deal with data coming from a REST API, you'll know how tedious and redundant it is to set up the boilerplate for each entity and endpoint. You'll likely copy and paste your common actions for fetching entities and lists, sending updates to the API, creating entities, etc. Also, you'll have to deal with the data coming in and ideally normalize it before storing it in redux. Then, when connecting a component, you'll have to de-normalize the data to be able to access it as you need to.
Model generator was built to take away all these problems and boilerplates.
Define the entity model
import ModelGenerator from '@rexlabs/model-generator';
import axios from 'axios'; // Obv you can use any request library you wish
// Define API endpoints
const config = {
entities: {
api: {
// NOTE: the API functions have to return promises!
fetchItem: (type, args, id) => axios.get(`/todos/${id}`, args),
fetchList: (type, args) => axios.get(`/todos`, args),
updateItem: (type, args, id) => axios.put(`/todos/${id}`. args),
createItem: (type, args) => axios.post(`/todos`, args),
trashItem: (type, args, id) => axios.delete(`/todos/${id}`, args)
}
}
}
export default new ModelGenerator('todos', config).createEntityModel();
// ^ This will automatically create all common actions, reducers and selectors
// for you ... you can optionally extend these with custom actions/reducers/selectors
// by passing them in like you would for normal models!
For more details on the created actions etc. see API docs.
Combine the models in redux
Usually you'd want to have an entities
node in your redux store, that contains all your entity data. If so, you can use combineModels
to do so easily. Also note that model generator will create asynchronous actions for you to deal with the API, so you need to include redux-thunk
to your middleware.
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { thunk }
import { combineModels } from '@rexlabs/model-generator';
import todosReducer from './redux/models/todos';
const entities = combineModels('entities', { todos: todosReducer });
const store = createStore(
combineReducers({entities}),
{},
applyMiddleware([thunk])
)
// this will create the following store
// { entities: { todos: { lists: {}, items: {} } } }
NOTE: if you use redux dev tools, just follow their instructions on how to wrap the middleware around their utils
Connect component
You now have multiple options to connect a component to entities in redux. If you just care about the data that is already there or just want the actions available in your component, you can use withModel
just like you would for normal models.
Usually however you'd want to automatically load data if it is not already in the store. Instead of having to do the check and dispatch the action manually every time you want to do that, model generator comes with handy abstractions for these scenarios, namely withEntity
and withEntityList
.
Probably the best way however is to use withQuery
, where you can define the response fields you actually care about. This makes the check weather or not an API request is necessary as well as building smart arguments for your API requests a lot easier. Also it lets model generator know what related data your component cares about, which allows it to just update your component if any of these actually changed, rather than on every entity store change.
import React from 'react';
import { withQuery } from '@rexlabs/model-generator';
import todos from './redux/models/todos';
const TodosList = ({todos}) => {
if (todos.list.status === 'loading') {
return <p>Loading todos...</p>
}
return (
<ul>
{todos.list.items.map(item => {
return <li key={item.id}>{item.label}</li>
})}
</ul>
);
}
// Define model and fields for your query
const listQuery = {
model: todos,
fields: {
id: true,
label: true
}
};
const ConnectedTodosList = withQuery(listQuery)(TodosList)
If you think this feels a lot like GraphQL, you're quite right. That's why you can also use GraphQL syntax to build your queries if you prefer that. This doesn't mean you need a GraphQL API! Its only an alternative syntax to define the query object in the above example.
import { withQuery, query } from '@rexlabs/model-generator';
const listQuery = query`{
${todos} {
id
label
}
}`;
const ConnectedTodosList = withQuery(listQuery)(TodosList)
Read more about the connector options and the usage of GraphQL query syntax in the API docs.
Defaults for the generator
The above is still quite redundant, when you have to define all API endpoints for all your entities. Even more considering that these endpoints usually follow simple conventions. In real projects you would define these defaults in a project specific abstraction of the model generator as follows.
import ModelGenerator from '@rexlabs/model-generator';
import axios from 'axios';
const defaultConfig = {
entities: {
api: {
createItem: (type, args) => axios.post(`/${type}`, args),
fetchList: (type, args) => axios.get(`/${type}`, args),
fetchItem: (type, args, id) => axios.get(`/${type}/${id}`, args),
updateItem: (type, args, id) => axios.patch(`/${type}/${id}`, args),
trashItem: (type, args, id) => axios.delete(`/${type}/${id}`, args)
}
}
};
export default class MyModelGenerator extends ReduxModelGenerator {
constructor (type, config = {}) {
super(type, _.merge({}, defaultConfig, config));
}
}
Now, instead of using the core model generator class for your models, you can just use your specific abstraction of it. That way you'll only ever need to define API endpoints if they differ from these defaults 😊
More reading
API docs
- You can find more detailed documentation on the core model generator here
Used libraries
- https://github.com/reactjs/redux
- https://github.com/facebook/react
- https://github.com/gaearon/redux-thunk
- https://github.com/graphql/graphql-js
Development
Install dependencies
$ yarn
Available Commands
$ yarn test # runs all units tests
$ yarn test:watch # runs unit tests when files change
$ yarn build # bundles the package for production
Legal
Copyright © 2018 Rex Software All Rights Reserved.
4 years ago