@asd14/redux-all-is-list v0.7.0
redux-all-is-list
A Redux data "gateway", similar to an API gateway. A less strict version of GraphQL, where you can have multiple data sources.
Features
Aggregate
Combine data coming from different sources (users from own api, tweet count from Twitter)
Mitigate inconsistent API
Uniform into a common shape, ie. stop backend tech dept from propagating into the frontend
Race free
All CRUD operations are done in sequence. If
updateis issued afterdelete, theupdatepromise will wait fordeleteto finish and then do it's work.
Cache
findoperations are cached based on it's signature.
It's Redux
Treat your state data as simple lists with common metadata helpers (isLoading, isUpdating etc.) and less boilerplate.
Install
npm i @asd14/redux-all-is-listExample
1:1 mapping of a Todo list's CRUD methods to corresponding API endpoints.
Define a list of todos from our local API.
// todos.state.js
import { buildList } from "@asd14/redux-all-is-list"
export const TodosList = buildList({
name: "PAGE__SECTION--TODOS",
cacheTTL: 1000,
methods: {
create: data => POST("/todos", data),
find: () => [{id: 1, title: "lorem ipsum"}],
update: (id, data) => PATCH(`/todos/${id}`, date),
delete: id => DELETE(`/todos/${id}`),
},
})Hook internal list reducers into the state store.
// store.js
import { createStore, combineReducers } from "redux"
import { TodosList } from "./todos.state"
const store = createStore(
combineReducers({
[TodosList.name]: TodosList.reducer,
}),
)Use the list's selector helpers to access it's data.
// todos.container.jsx
import React from "react"
import cx from "classnames"
import { connect } from "react-redux"
import { TodosList } from "./todos.state"
@connect(
store => {
const todosSelector = listSelector.selector(store)
return {
todos: todosSelector.items(),
todosIsLoading: todosSelector.isLoading(),
}
},
dispatch => ({
xHandleTodosFind: TodosList.find(dispatch),
})
)
class TodosContainer extends React.Component {
componentDidMount = () => {
const { xHandleTodosFind } = this.props
xHandleTodosFind()
}
render = () => {
const { todos, todosIsLoading } = this.props
return (
<div
className={cx({
[css.isLoading]: todosIsLoading,
})}>
{todos.map(item => <div>{item.title}</div>)}
</div>
)
}
}
export { TodosContainer }API
Definition
buildList is the only exposed function. It prepares the reducer and CRUD actions that interface and data sources.
import { buildList } from "@asd14/redux-all-is-list"
buildList({
name: "PAGE__SECTION--TODOS",
cacheTTL: 100,
methods: {
create: data => POST("/todos", data),
find: ({ offset, limit }) =>
GET("/todos", {
offset,
limit,
}),
update: (id, data) => PATCH(`/todos/${id}`, date),
delete: id => DELETE(`/todos/${id}`),
},
})Params
Object containing:
*name:string
Unique name used for the redux store key. If multiple lists use the same name, an error will be thrown. This is because the list is ment to be added on the root level of the redux store. Use BEM for naming, ex. {page}__{section}--{entity}
methods:Object
Define list's CRUD actions and map to one or more data sources (local storage, 3rd party APIs or own API). There are only 4 actions that can be defined.
.create(data: Object, { isDraft: bool = false }): Promise<Object>- Add return obj to main
slice.items-idfield is required. - Add data obj to
slice.creatingarray, cleared after promise resolves. - Toggle
slice.isCreatingflag before and after promise resolves. - If
isDraftis true, the method will not run. The data object will be simply added to theslice.itemsarray. - Clear cache if
cacheTTLis set.
- Add return obj to main
.find(...args: any[]): Promise<Object[]>- Replace
slice.itemscontents with return array -idfield is required in each item. - Toggle
slice.isLoadingflag before and after promise resolves. - Set
slice.loadDateto the current time (Date object) after promise resolves. - Results will be cached based on
args.find({offset: 10})will be cached separately thanfind().
- Replace
update(id: string|number, data: Object, { isDraft: bool = false }): Promise<Object>- Update item in
slice.itemsif exists (merge byid), add otherwise. - Add item to
slice.updatingarray, cleared after promise resolves. - If
isDraftis true, the method will not run. The data object will be simply merged or added to theslice.itemsarray. - Clear cache if
cacheTTLis set.
- Update item in
delete: (id: string|number): Promise- Delete item with
id. Return value is ignored. - Clear cache if
cacheTTLis set.
- Delete item with
cacheTTL: number
Number of miliseconds a cached value is valid.
Retuns
Object containing:
name:string and reducer:Function
Map the same name passed in the builder function to the constructed reducer function specific each list. Use when initializing the store.
import { createStore, combineReducers } from "redux"
import { TodosList } from "./todos.state"
const store = createStore(
combineReducers({
[TodosList.name]: TodosList.reducer,
}),
)create|find|update|delete: (dispatch: Function): Function
Curry function that make available the store's dispatch to the functions in methods. Error will be thrown if the method is not defined in builder function's methods obj.
@connect(mapStateToProps, dispatch => ({
xHandleTodosFind: TodosList.find(dispatch),
}))Internal state slice
{
...
[list.name] : {
items: [],
creating: [],
updating: [],
deleting: [],
errors: {},
loadDate: null,
isLoading: false,
}
}Add to Radux
Consume in container component
Selectors
.head: () => Object|undefined.byId: (id: string|number) => Object|undefined.items: () => Object[].creating: () => Object[].updating: () => Object[].deleting: () => Object[].error: (action: string) => Object.isCreating: () => boolean.isLoaded: () => boolean.isLoading: () => boolean.isUpdating: (id: string|number) => boolean.isDeleting: (id: string|number) => boolean
Recommendations
- Don't reuse. A list should be used once per page/section.
- Group all lists per page into a separate file to avoid variable name collision.
- Don't store data locally, data lives in the database - that's the real application state.
Develop
git clone git@github.com:asd14/redux-all-is-list.git && \
cd redux-all-is-list && \
npm run setupRun all *.test.js in src folder
npm testWatch src folder for changes and re-run tests
npm run tddChangelog
History of all changes in CHANGELOG.md
0.7 - 10 July 2019
Change
- Update packages