react-data-subscription
v4.0.0
Published
Data provider for React applications. Simplifies data flow by handling component's props and state changes, automatically fetching new data if necessary and applying it directly to component's state. Forget about promises, sagas, callbacks etc. This highe
Downloads
18
Maintainers
Readme
About
This React HOC simplifies and optimizes communication with API and solves multiple problems at once:
- It doesn't rely on Saga, Thunk and Redux in general. No learning curve, no extra dependencies.
- It eliminates the overhead of using promises, callbacks, async/await.
- It optimizes network, memory usage and as a result - application performance.
- With this HOC you can make every single component a self-sufficient one with no harm to application performance.
- Everything data-related is handled in one place.
Key points
- Each subscription is bound to a component that initializes it. It lives with and it dies with it.
- There has been a data store manager implemented that does all the optimization and synchronization.
- If a subscription is trying to make the same call that an already existing subscription made before - it won't produce a new request. It will refer to the existing request instead. Example: three components which have the same subscription parameters (
endpoint
+requestParams
) will share a single network request. - This module allows creating components dependent on API data without worrying about excessive amount of network requests.
- Forget about duplication of network requests
- Forget about thinking of when to make a request and how to handle it
Installation
yarn add react-data-subscription
or
npm install react-data-subscription
Usage
Config
Provides with an option to configure how the underlying fetch
should process requests and responses globally.
get( path: string ): any
- get config property at its path; see lodash.get for details on path formatset( pathOrObject: string|object, value: any )
- set config property at its path or merge the default config with a given object; see lodash.set for details on path format
Default settings:
request: {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
transformPayload: (url, params, requestParams) => {
if (requestParams.method !== "GET" && requestParams.method !== "HEAD") {
return JSON.stringify(params.body || {});
}
},
transformUrl: (url, requestParams) => {
if (requestParams.method === "GET" || requestParams.method === "HEAD") {
return `${url}?${QueryString.stringify(requestParams.body)}`;
}
return url;
},
},
response: {
transformResponse: response => response.json(),
transformError: error => error,
}
Any of these settings can be overwritten and new ones can be added to be used by fetch
function.
import { createConfig } from "react-data-subscription";
const config = createConfig();
config.set({
request: {
credentials: "include",
redirect: "follow",
transformUrl: (url, requestParams) => {
const prefix = "/api";
if (requestParams.method === "GET" || requestParams.method === "HEAD") {
return `${prefix}${url}?${QueryString.stringify(requestParams.body)}`;
}
return `${prefix}${url}`;
},
},
});
NOTE: This is a global config. It controls the way the module will behave for all components
HOC
Adds an extra prop called subscribe
to your component.
The HOC is still to be created using the proper config. This allows having multiple data transports within the app. Each data transport will have its own HOC.
Creating the HOC:
// withDataSubscription.jsx
import { createConfig, createDataSubscription } from "react-data-subscription";
const config = createConfig();
// set up config here
// config.set(...)
//
export default createDataSubscription(config);
import React from "react";
import withDataSubscription from "./withDataSubscription";
class MyComponent extends React.Component {
componentDidMount() {
this.props.subscribe(
this,
"/getTodos"
() => {
return {
body: {
subject: this.props.subject,
count: this.state.count,
perPage: this.state.perPage,
}
};
},
data => {
return {
todos: data,
};
}
);
}
render() {
const todos = this.state.todos || {};
return (
<div>
{todos.isLoading && <LoadingIndicator />}
{todos.isRefreshing && <RefreshingIndicator />}
{todos.isError && <Error error={todos.error} />}
{todos.isLoaded &&
<Yikes>
{JSON.stringify(todos.payload || [])}
</Yikes>
}
</div>
);
}
}
export default withDataSubscription(MyComponent);
This is a basic example, but what might be not obvious here is:
- Any time
this.props.subject
,this.state.count
orthis.state.perPage
is changed the data is going to be updated automatically. - We can handle the request lifecycle.
- When the data is loaded it's available from out component's state.
Parameters
componentInstance: object
- component instance. Is needed for hooking into components lifecycle events. Typically,this
has to be passed.endpoint: string
- an endpoint to make a request to.requestParams: function
- a function that returns request params for underlyingfetch
call. Every time the value returned by this function is different from the previous one a newfetch
will be made. These parameters will get merged with default parameters set in globalconfig
and get passed tofetch
.responseCallback(response: object): function
- a function that returns changes to component's state. Based on the current state or response returned by the endpoint you can make a decision around what should go into your component's state. The returned value gets passed to your component'ssetState()
if it's different from what's already there. Only the keys returned by this function get compared to the existing state. Comparison is done by lodash.isEqualshouldRun: function
- a function that indicates whether the component is ready tofetch
the data or not. Default value is() => true
. This parameter can be used to delay the request until some condition is met, like some payload parameter may not be set at the time whencomponentDidMount
is invoked.
The contents of responseCallback
argument are:
isLoading: boolean
-true
if request is being currently in progresss.isRefreching: boolean
-true
if request is being currently refreshed.isLoaded: boolean
-true
if there's no error and request was successfull. Usually means that its payload contains the response data.isError: boolean
-true
if there's an error while making this requestisFinished: boolean
-true
if eitherisLoaded
orisError
istrue
.payload: object
- response body if there was no error. Default:undefined
,error: object
- error detail if there was an error. Default:undefined
.refresh: function
- a function that triggers re-fetch of data no matter what.
HOW-TOs
Polling
Polling means the same request needs to be made with some interval. Data subscription only fetches new data if either endpoint or request param have changed. However, the response object contains refresh
function, which can be used for forcing the subscription to rerun with unchanged parameters.
import React from "react";
import withDataSubscription from "./withDataSubscription";
class MyComponent extends React.Component {
componentDidMount() {
this.props.subscribe(
this,
"/getTodos"
() => {
return {
body: {
subject: this.props.subject,
count: this.state.count,
perPage: this.state.perPage,
}
};
},
data => {
if (data.isFinished) {
setTimeout(() => {
data.refresh();
}, 6e4);
}
return {
todos: data,
};
}
);
}
render() {
const todos = this.state.todos || {};
return (
<div>
{todos.isLoading && <LoadingIndicator />}
{todos.isRefreshing && <RefreshingIndicator />}
{todos.isError && <Error error={todos.error} />}
{todos.isLoaded &&
<Yikes>
{JSON.stringify(todos.payload || [])}
</Yikes>
}
</div>
);
}
}
export default withDataSubscription(MyComponent);
Now our todos
data is going to be refreshed every minute.
Multiple data subscriptions on a single component
Sometimes there's a need to make several requests in order to gather all required data. This is not a problem at all because components can have as many subscriptions as needed.
import React from "react";
import withDataSubscription from "./withDataSubscription";
class MyComponent extends React.Component {
componentDidMount() {
this.props.subscribe(
this,
"/getTodos"
() => {
return {
body: {
subject: this.props.subject,
count: this.state.count,
perPage: this.state.perPage,
}
};
},
data => {
return {
todos: data,
};
}
);
this.props.subscribe(
this,
"/getEvents"
() => {
return {
body: {
date: this.state.selectedDate,
}
};
},
data => {
return {
events: data,
};
}
);
}
render() {
const todos = this.state.todos || {};
const events = this.state.events || {};
return (
<div>
{todos.isLoading && <LoadingIndicator />}
{todos.isRefreshing && <RefreshingIndicator />}
{todos.isError && <Error error={todos.error} />}
{todos.isLoaded &&
<Yikes>
{JSON.stringify(todos.payload || [])}
{JSON.stringify(events.payload || [])}
</Yikes>
}
</div>
);
}
}
export default withDataSubscription(MyComponent);
Handle a subscription that's dependent on another one
Sometimes one request is dependent on the data returned by another one. Since all subscriptions are hooked into components componentDidUpdate
this is a trivial task.
import React from "react";
import { get } from "lodash";
import withDataSubscription from "./withDataSubscription";
class MyComponent extends React.Component {
componentDidMount() {
this.props.subscribe(
this,
"/getUser"
() => {
return {
body: {
email: this.state.email,
password: this.state.password,
}
};
},
data => {
return {
token: get(data, "payload.token"),
};
},
() => this.state.email && this.state.password,
);
this.props.subscribe(
this,
"/getTodos"
() => {
return {
headers: {
Authorization: `Bearer ${this.state.token}`,
},
body: {
subject: this.props.subject,
count: this.state.count,
perPage: this.state.perPage,
}
};
},
data => {
return {
todos: data,
};
},
() => this.state.token,
);
}
login = () => {
this.setState({
email: this.email.value,
password: this.password.value,
});
}
render() {
const todos = this.state.todos || {};
return (
<div>
<input type="text" ref={input => this.email = input} />
<input type="password" ref={input => this.password = input} />
<input type="button" onClick={this.login} />
{todos.isLoaded &&
<Yikes>
{JSON.stringify(todos.payload || [])}
</Yikes>
}
</div>
);
}
}
export default withDataSubscription(MyComponent);
Initially no requests ill be made. When email
and password
get set to the component's state the first /getUser
request will be made.
If the response contains a token it will be set to the component's state. The second /getTodos
request will be made since this.state.token
is truthy.
Ignore request lifecycle updates
If you're not interested in handling request's isLoading
, isRefreshing
etc. states you can ignore them in order to decrease the number of times the component will render
.
import React from "react";
import withDataSubscription from "./withDataSubscription";
class MyComponent extends React.Component {
componentDidMount() {
this.props.subscribe(
this,
"/getTodos"
() => {
return {
body: {
subject: this.props.subject,
count: this.state.count,
perPage: this.state.perPage,
}
};
},
data => {
if (data.isFinished) {
return {
todos: data,
};
}
}
);
}
render() {
const todos = this.state.todos || {};
return (
<div>
{todos.isLoaded &&
<Yikes>
{JSON.stringify(todos.payload || [])}
</Yikes>
}
</div>
);
}
}
export default withDataSubscription(MyComponent);
In this example we will only update our component's state once when the request is finished and won't re-render on the request's state changes.
Transform the data
In most cases the response data needs to be reshaped in order to be rendered properly. It's possible to transform it inside render
method, but it may be called more times than your data is actually changed.
The best place to invoke data transformation is inside responseCallback
because this callback will only be called when the data is actually changed.
import React from "react";
import withDataSubscription from "./withDataSubscription";
class MyComponent extends React.Component {
componentDidMount() {
this.props.subscribe(
this,
"/getTodos"
() => {
return {
body: {
subject: this.props.subject,
count: this.state.count,
perPage: this.state.perPage,
}
};
},
data => {
return {
todos: {
...data,
payload: this.transformPayload(data.payload)
},
};
}
);
}
transformPayload = payload => {
if (!payload) return payload;
let reshapedData;
// reshape the data here
return reshapedData;
}
render() {
const todos = this.state.todos || {};
return (
<div>
{todos.isLoaded &&
<Yikes>
{JSON.stringify(todos.payload || [])}
</Yikes>
}
</div>
);
}
}
export default withDataSubscription(MyComponent);
Contributions
Feel free t submit a PR for any enhancements. The repo contains a playground for testing:
- Demo server:
./test/server.js
- React app:
./test/client