studiokit-net-js v4.1.4
StudioKit Net Library
A library for declarative, configurable data (API) access with built-in retry, periodic refresh, and concurrency handling.
For in-vivo examples of how to use this library, see the example react app and the example react native app
Installation
Install this library and redux-saga as a dependency
yarn add studiokit-net-jsyarn add redux-saga(which depends onreduxitself)Create a
reducers.jsmodule that includes the reducer from this library, i.e.import { combineReducers } from 'redux' import { reducers as netReducers } from 'studiokit-net-js' export default combineReducers({ models: netReducers.fetchReducer })Create an
endpointMappings.jsmodule specifying a mapping of any APIs you will call in your application. All configuration properties are set under_config. Fetch request specific default properties are set on_config.fetch, i.e.const endpointMappings = { publicData: { _config: { fetch: { path: 'https://httpbin.org/get', queryParams: { foo: 'bar' } } } } } export default endpointMappingsCreate a
rootSaga.jsmodule that includes the fetchSaga from this library, i.e.import { all } from 'redux-saga/effects' import { sagas as netSagas } from 'studiokit-net-js' import endpointMappings from '../../endpointMappings' export default function* rootSaga() { yield all({ fetchSaga: netSagas.fetchSaga( endpointMappings, 'https://yourapp.com' ) }) }Wire up your store in your app (perhaps in
index.js) with the above, i.e.import createSagaMiddleware from 'redux-saga' import { createStore, applyMiddleware } from 'redux' import reducer from './redux/reducers' import rootSaga from './redux/sagas/rootSaga' const sagaMiddleware = createSagaMiddleware() const store = createStore( reducer, applyMiddleware(sagaMiddleware) ) sagaMiddleware.run(rootSaga)
Usage
Once you have the above steps completed, you can dispatch actions to the store and the data will be fetched and populated in the redux store, i.e.
import { dispatchAction } from '../services/actionService'
import { actions as netActions } from 'studiokit-net-js'
.
.
.
store.dispatch({ type: netActions.DATA_REQUESTED, modelName: 'publicData' })Once the data is fetched, it will live in the redux store at the models.publicData key, i.e.
models: {
publicData: {
data: { foo: 'bar', baz: ['quux', 'fluux']},
isFetching: false,
hasError: false,
fetchedAt: "2017-05-23T20:38:11.103Z"
}
}API
Actions are dispatched using the following keys in the action object for configuring the request
type FetchAction = {
modelName: string,
guid?: string
method?: string,
headers?: Object,
queryParams?: Object,
pathParams?: Object,
noStore?: boolean,
period?: number,
taskId?: string,
noRetry?: boolean
contentType?: string
}modelNamerefers to the path to the fetch configuration key found inendpointMappings.jsguidis an optional pre-generated (by your application) GUID that will be attached to a fetch result's data, to be stored in redux and used to match request results in componentsmethodis an optional string used as the HTTP Method for the fetch. Otherwise will use the method set inendpointMappings.js, or'GET'headersis an optional object used as key/value pairs to populate the request headersqueryParamsis an optional object used as key/value pairs to populate the query parameterspathParamsis an optional array of values to be replaced in the fetch path using pattern matching, in order, e.g.[1, 2]and/collection/{:id}/subcollection/{:id}=>/collection/1/subcollection/2noStoreis an optional boolean that, if true, indicates the request should be made without storing the response in the redux storeperiodis an optional number of milliseconds after which a request should repeat when dispatching a recurring fetchtaskIdis a string that must be passed to a recurring fetch for future cancellationnoRetrywill prevent the use of the default logarithmic backoff retry strategy
The following actions can be dispatched
DATA_REQUESTED: This will fetch the data specified at themodelNamekey of the actionPERIODIC_DATA_REQUESTED: This will fetch the data specified at themodelNamekey at an interval specified by theperiodkey in the action. This also requires you to generate and pass ataskIdkey for subsequent cancellationPERIODIC_TERMINATION_REQUESTED: This will cause the periodic fetch identified bytaskIdto be cancelledDATA_REQUESTED_USE_LATEST: This will fetch data specified at themodelNamekey, using only the latest result in time if multiple requests are dispatched at the same time (i.e. others are started with the samemodelNamebefore some are completed)
Examples
Given the following endpointMappings.js
{
basicData: {
_config: {
fetch: {
path: 'https://httpbin.org/get'
}
}
},
futurama: {
_config: {
fetch: {
path: 'https://www.planetexpress.com/api/goodNewsEveryone',
queryParams: {
doctor: 'zoidberg'
}
}
}
},
theWalkers: {
_config: {
fetch: {
path: 'https://thewalkingdead/api/walker/{:walkerId}',
pathParams: {
walkerId: 1
}
}
}
}.
theOffice: {
_config: {
fetch: {
path: 'https://dundermifflin.com/api/paper'
headers: {
'Content-Type': 'x-beet-farmer'
}
}
}
},
aGrouping: {
apiOne: {
_config: {
fetch: {
path: '/api/one'
}
}
},
apiTwo: {
_config: {
fetch: {
path: '/api/two/{{models.futurama.zoidberg}}'
}
}
}
},
basicPost: {
_config: {
fetch: {
path: '/api/createSomeThing'
method: 'POST'
}
}
},
basicPostTwo: {
_config: {
fetch: {
path: '/api/createSomeKnownThing'
method: 'POST',
body: { person: 'Fry' }
}
}
},
entities: {
_config: {
fetch: {
path: '/api/entities'
},
isCollection: true
}
},
topLevelEntities: {
_config: {
isCollection: true
},
secondLevelEntities: {
_config: {
isCollection: true
}
}
}
}You can make the following types of requests:
Basic Fetch
Nested Model
Add Headers
Add Query Params
Periodic Fetch
Cancel Periodic Fetch
No Store
Post
Collections
Nested Collections
Basic fetch:
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'basicData'
})request generated
GET https://httpbin.org/getresulting redux
{
models: {
basicData: {
foo: 'bar',
...,
_metadata: {
isFetching: false,
hasError: false,
fetchedAt: '2017-05-23T20:38:11.103Z'
}
}
}
}Nested model:
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'aGrouping.apiOne'
})request generated
GET https://myapp.com/api/oneresulting redux
{
models: {
aGrouping: {
apiOne: {
foo: 'bar',
...,
_metadata: {
isFetching: false,
hasError: false,
fetchedAt: '2017-05-23T20:38:11.103Z'
}
}
}
}
}Add headers:
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'basicData',
headers: {'Accept-Charset': 'utf-8'}
})request generated
Accept-Charset: utf-8
GET https://httpbin.org/getresulting redux
Same as basic fetch above, with possibly different data, depending on response relative to additional header
Note: Headers specified in the action will be merged with headers specified in endpointMappings.js with the headers in the action taking precedence
Add query params:
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'basicData',
queryParams: {robot: 'bender'}
})request generated
GET https://httpbin.org/get?robot=benderresulting redux
Same as basic fetch above, with possibly different data, depending on response relative to new query params
Note: Query parameters specified in the action will be merged with query parameters specified in endpointMappings.js with the query params in the action taking precedence
Add route params:
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'theWalkers',
queryParams: {walkerId: 1}
})request generated
GET https://thewalkingdead/api/walker/1resulting redux
Same as basic fetch above, with possibly different data, depending on response relative to new route params
Note: Route parameters specified in the action will be merged with route parameters specified in endpointMappings.js with the route params in the action taking precedence
Periodic fetch:
dispatch
store.dispatch({
type: netActions.PERIODIC_DATA_REQUESTED,
modelName: 'basicData',
period: 1000,
taskId: 'something-random'
})request generated
GET https://httpbin.org/getresulting redux
Same as basic fetch above, but refreshing every 1000ms, replacing the data key in redux with new data and updating the fetchedAt key
Cancel periodic fetch:
dispatch
store.dispatch({
type: netActions.PERIODIC_TERMINATION_REQUESTED,
modelName: 'basicData',
taskId: 'something-random'
})request generated
None
resulting redux
Same as basic fetch above with data and fetchedAt reflecting the most recent fetch before the cancellation request
No store:
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'basicData',
noStore: true
})request generated
GET https://httpbin.org/getresulting redux
No change to the redux store. Your application can create its own sagas and use take and friends in redux-saga, however, to watch for responses and cause side-effects
Post:
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'basicPost',
body: {
ruleOne: "Don't talk about Fight Club",
ruleTwo: "Don't talk about Fight Club"
}
})request generated
Content-Type: application/json
POST https://myapp.com/api/createSomeThing
{"ruleOne": "Don't talk about Fight Club","ruleTwo": "Don't talk about Fight Club"}resulting redux
Same as basic fetch above, with the data key containing the response data from the POST request
Post with form data:
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'basicPost',
body: new FormData(),
contentType: 'multipart/form-data'
})request generated
Content-Type: multipart/form-data; XXX boundary--------
POST https://myapp.com/api/createSomeThing
(formData values)resulting redux
Same as basic fetch above, but with Content-Type equals to multipart/form-data
Collections
GET all
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'entities'
})request generated
GET https://myapp.com/api/entitiesresulting redux
{
models: {
entities: {
1: {
id: 1,
...,
_metadata: {
isFetching: false,
hasError: false,
fetchedAt: '2017-05-23T20:38:11.103Z'
}
},
...,
_metadata: {
isFetching: false,
hasError: false,
fetchedAt: '2017-05-23T20:38:11.103Z'
}
}
}
}GET item
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'entities',
pathParams: [1]
})request generated
GET https://myapp.com/api/entities/1resulting redux
Updates item in store at entities.1
POST item
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'entities',
method: 'POST',
body: {
name: 'entity name'
}
})request generated
Content-Type: application/json
POST https://myapp.com/api/entities/1
{"name": "entity name"}resulting redux
Adds item in store at entities under the return object's id
Note
During the request, status is stored in entities under a guid key, which can be provided in the action for tracking
PATCH item
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'entities',
method: 'PATCH'
pathParams: [1],
body: {
op: 'replace',
path: 'Name',
value: 'updated group name'
}
})request generated
Content-Type: application/json
PATCH https://myapp.com/api/entities/1
{"op": "replace", "path": "Name", "value": "updated group name"}resulting redux
Updates item in store at entities.1
Note
See http://jsonpatch.com/
DELETE Entity
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'entities',
method: 'DELETE'
pathParams: [1]
})request generated
DELETE https://myapp.com/api/entities/1resulting redux
Removes item in store at entities.1
Nested Collections
Nested collections behave the same as normal collections, but require a pathParams to have at least one value per nested level.
GET all
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'topLevelEntities.secondLevelEntities',
pathParams: [1]
})request generated
GET https://myapp.com/api/topLevelEntities/1/secondLevelEntitiesresulting redux
{
models: {
topLevelEntities: {
1: {
id: 1,
secondLevelEntities: {
999: {
id: 999,
...,
_metadata: {
isFetching: false,
hasError: false,
fetchedAt: '2017-05-23T20:38:11.103Z'
}
},
...,
_metadata: {
isFetching: false,
hasError: false,
fetchedAt: '2017-05-23T20:38:11.103Z'
}
}
}
}
}
}Stores item in object as key/value pairs in store at topLevelEntities.1.secondLevelEntities
GET item
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'topLevelEntities.secondLevelEntities',
pathParams: [1, 999]
})request generated
GET https://myapp.com/api/topLevelEntities/1/secondLevelEntities/999resulting redux
Updates item in store at topLevelEntities.1.secondLevelEntities.999
POST item
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'topLevelEntities.secondLevelEntities',
pathParams: [1],
method: 'POST',
body: {
name: 'entity name'
}
})request generated
Content-Type: application/json
POST https://myapp.com/api/topLevelEntities/1/secondLevelEntities
{"name": "entity name"}resulting redux
Adds item in store at topLevelEntities.1.secondLevelEntities under the return object's id
Note
During the request, status is stored in topLevelEntities.1.secondLevelEntities under a guid key, which can be provided in the action for tracking
PATCH item
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'topLevelEntities.secondLevelEntities',
method: 'PATCH'
pathParams: [1, 999],
body: {
op: 'replace',
path: 'Name',
value: 'updated group name'
}
})request generated
Content-Type: application/json
PATCH https://myapp.com/api/topLevelEntities/1/secondLevelEntities/999
{"op": "replace", "path": "Name", "value": "updated group name"}resulting redux
Updates item in store at topLevelEntities.1.secondLevelEntities.999
Note
See http://jsonpatch.com/
DELETE Entity
dispatch
store.dispatch({
type: netActions.DATA_REQUESTED,
modelName: 'topLevelEntities.secondLevelEntities',
method: 'DELETE'
pathParams: [1, 999]
})request generated
DELETE https://myapp.com/api/topLevelEntities/1/secondLevelEntities/999resulting redux
Removes item in store at topLevelEntities.1.secondLevelEntities.999
Development
During development of this library, you can clone this project and use
yarn link
to make the module available to another project's node_modules on the same computer without having to publish to a repo and pull to the other project. In the other folder, you can use
yarn link studiokit-net-js
to add studiokit-foo-js to the consuming project's node_modules
Build
Because this is a module, the source has to be transpiled to ES5 since the consuming project won't transpile anything in node_modules
yarn build
will transpile everything in /src to /lib. /lib/index.js is the entry point indicated in package.json
During development, you can run
yarn build:watch
and babel will rebuild the /lib folder when any file in /src changes.
When you commit, a commit hook will automatically regenerate /lib
Deploy
This packaged is deployed via the npm repository. Until we add commit hooks for deployment, it must be published via yarn publish
3 years ago
3 years ago
3 years 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
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
6 years ago
6 years ago
6 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago