0.7.0 • Published 5 years ago

@asd14/redux-all-is-list v0.7.0

Weekly downloads
-
License
BSD-3-Clause
Repository
github
Last release
5 years ago

npm package version dev-badge Coverage Status

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 update is issued after delete, the update promise will wait for delete to finish and then do it's work.

Cache

find operations 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-list

Example

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 - id field is required.
    • Add data obj to slice.creating array, cleared after promise resolves.
    • Toggle slice.isCreating flag before and after promise resolves.
    • If isDraft is true, the method will not run. The data object will be simply added to the slice.items array.
    • Clear cache if cacheTTL is set.
  • .find(...args: any[]): Promise<Object[]>

    • Replace slice.items contents with return array - id field is required in each item.
    • Toggle slice.isLoading flag before and after promise resolves.
    • Set slice.loadDate to the current time (Date object) after promise resolves.
    • Results will be cached based on args. find({offset: 10}) will be cached separately than find().
  • update(id: string|number, data: Object, { isDraft: bool = false }): Promise<Object>

    • Update item in slice.items if exists (merge by id), add otherwise.
    • Add item to slice.updating array, cleared after promise resolves.
    • If isDraft is true, the method will not run. The data object will be simply merged or added to the slice.items array.
    • Clear cache if cacheTTL is set.
  • delete: (id: string|number): Promise

    • Delete item with id. Return value is ignored.
    • Clear cache if cacheTTL is set.

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 setup

Run all *.test.js in src folder

npm test

Watch src folder for changes and re-run tests

npm run tdd

Changelog

History of all changes in CHANGELOG.md

0.7 - 10 July 2019

Change

  • Update packages
0.7.0

5 years ago

0.6.0

5 years ago

0.5.1

5 years ago

0.5.0

5 years ago

0.4.1

5 years ago

0.4.0

5 years ago

0.3.0

5 years ago

0.2.2

5 years ago

0.2.1

5 years ago

0.2.0

5 years ago