Когда мы работаем с REST-ресурсами, в 90% случаев, все что нам нужно это получить данные и отрендерить или забиндать в форму и сохранить после изменения. Эта, довольно нетривиальная задача приводит к большому количеству однотипного кода. Что-то вроде:
// action types
const FETCH_USERS_LIST = Symbol('FETCH_USERS_LIST')
const FETCH_USER = Symbol('FETCH_USER')
const SAVE_USER = Symbol('SAVE_USER')
const CREATE_USER = Symbol('CREATE_USER')
// action creators
function fetchUsersList() {
return {
type: FETCH_USERS_LIST
}
}
function fetchUser(id) {
return {
type: FETCH_USER,
payload: id,
}
}
function saveUser(id, data) {
return {
type: SAVE_USER,
payload: data,
meta: {id}
}
}
function createUser(data) {
return {
type: CREATE_USER,
payload: data,
}
}
// reducers
// TODO
function users(state = {}, action) {
switch(action.type) {
case FETCH_USERS_LIST:
return {...state, isLoading: true}
}
}
// epics
// TODO
...
И этот пример не содержит обработку ошибок, фильтрацию, педжинцию, кеширование, JWT, options и т.п. При этом подобный код повторяется снова и снова в каждом store файле и по сути отличается лишь наименованием функций и констант:
fetchUsers
fetchBooks
fetchGroups
fetchComputers
fetchOrders
etc
Задача данного пакета ускорить написание этой рутины, оставив время "на подумать"
В данном пакете подразумевается что каждый API endpoint представляет собой REST ресурс. Для начала разберемся с самими ресурсами. Здесь они разделяются на два типа:
- Полноценный CRUD:
endpoint: /api/v1/users/:id
GET /api/v1/users/ - получить список юзеров
POST /api/v1/users/ - создать юзера
GET /api/v1/users/1 - получить юзера
PATCH /api/v1/users/1 - частично обновить юзера
POST /api/v1/users/1 - полностью обновить юзера (лучше использовать PATCH)
PUT /api/v1/users/1 - полностью перезаписать юзера (обычно не используем)
DELETE /api/v1/users/1 - удалить юзера
OPTIONS /api/v1/users/ - получить метаданные (обычно choices)
- endpoint CRUD
endpoint: /api/v1/some/custom/endpoint
отличается от предыдущего тем что это single ресурс, и все запросы формируются на один и тот же endpoint. при этом этом ресурс может быть предварительно создан на сервере, (тогда GET получает его первоначальное состояние) или же создается отдельно при помощи POST запроса.
resource.js предоставляет вам возможность приконектить такие ресурсы без особой сложности. практически одной строчкой кода:
connectResource(resource, options)
// где:
resource = {
namespace: 'internalResourceName',
endpoint: '/some/endpoint/with/:placeholders'
}
options = { /* доп опции, о них ниже */ }
в props получим такую структуру:
props.internalResourceName = {
data: { /* ... resource data from API ... */ },
isLoading: false, // or true,
options: { /* parsed OPTIONS from API */ },
errors: null, // or object with errors { },
loading: 0, // number of currently loadings, used internaly
// actions
fetch: func, // GET request, useful when no prefetch
create: func, // POST request
save: func, // PATCH request
update: func, // PATCH request, alias for save
remove: func, // DELETE request
replace: func, // PUT request if you need it
fetchOptions: func, // OPTIONS request
setData: func, // you can update data in store manually, but please be carefull with this action
setErrors: func, // you can updates errors in store manually, but please be carefull with this action
setFilters: func, // you can updates current filters in store manually, but please be carefull with this action
filters: {}, // current applied filters
filter: func, // action to re-fetch resource with new filters
// filtering (using browser query) TODO
withRouter: false, // or true
navigate: func, // the same as filter({offset: newOffset})
}
почти все опции можно задавать как на уровне конфигурации ресурса, так и на уровне коннекта
property name for resource binding. e.g. for namespace: 'test' you will get resource in props.test
you can set it as array with two strings. useful for list resource. e.g for namespace: ['books', 'book'] you will get this.props.books for list request, or this.props.book for single item request (see list option for details)
will be set to namespace if ommited. resource endpoint name - where to get data from server.
can contains placeholders (see doc for path-to-regexp package for additional information). all you current props will be forwarded to path-to-regexp, plus one additional property id wich will be get by idKey (see below)
mark you resource as list resource. you endpoint should conins :id? placeholder, e.g. accounts/:id?
when you have id property (this props name can be changed via idKey) in props then the item resource will be binded. otherwice list.
name for the property that contains id for item. see list option for details
should the resource be prefetched. if you set this to true you will get props[namespace].data === null. useful for custom resources that are not previously created on the server.
should the resource be refreshed when you render this component and the data for this resource already exists in store
this property doesn't make sense when ommited. in connectFormResource this field is required
connect resource for redux-form. you should specify form name here for properly validation working.
this option means that you will also get initialValues and onSubmit props.
initialValues will be set to prefetched data from server, or empty object (when no prefetch === false)
onSubmit will do create (POST) for new items (prefetch === false means that item is new, for list resources missed id also means that the resource item is new). for existed items onSubmit will do update (PATCH)
used only for list resources, only inside options. will be set to true when form name is specified. this property means that we working with item inside list. sometimes you need to make "create item form".
prefetch OPTIONS from endpoint (useful for geting choices for selects from API)
should the component be rendered without data
used with list resources. representing initial query for fetch request.
compose(
// ypu can omit this for create boook form. when id exists that ths is edit book form
connect(_ => ({uuid: 'e4831316-9e2a-41b7-bb77-514318ac51ba'})),
connectFormResource({
namespace: ['books', 'book'],
endpoint: 'books/:id?',
list: true,
options: true,
}, {form: 'book'}),
reduxForm({
form: 'test',
})
// you can mix resources inside one component
connectSingleResource({
namespace: 'config', // ommited endpoint will be set to 'config'
}),
)