redux-fetchers
v1.0.0
Published
Component wrapper to automatically dispatch actions when needed
Downloads
5
Readme
Redux Fetchers
Automatic action dispatching and caching for Redux.
Caching means only dispatching actions when not already executed and value not in store already.
This is especially useful when using actions to perform asynchronous network requests, which should not be retriggered while executing a network request.
Installation
Assuming you use npm as package manager to install
$ npm install --save redux-fetchers
Then simply include it in your preferred way:
// CommonJS
const {
fetches,
wrapActionForFetching
} = require('redux-fetchers');
// ES6 / TypeScript
import {
fetches,
wrapActionForFetching
} from 'redux-fetchers';
// There is no default export, so you have to do this, when you want one object:
import * as fetchers from 'redux-fetchers';
Motivation
In Redux, using actions to initiate network requests and fetch data asynchronously is a common use case.
Like the Redux Documentation states, flags like IS_FETCHING
are usually used to check the status of a request, which amongst other things can be used to prevent redispatching of an action that has already triggered a request.
While this principle works perfectly, it involves a lot of duplicated work when several requests and actions are handled in your application.
Fetchers automatically take care of dispatching and caching an action. This is especially interesting when a network request is performed by the action, which should not be redispatched while the request is running, or when the value is already in the store. Redispatching happens for example when a user changes the route while the request is still ongoing, and the requested route uses the same action.
Usage
- Every action can be used to create a fetcher.
- Besides the action you want to prepare to be dispatched and cached by the fetcher, you pass a further argument (the accessor) which is a function returning the property. The property should be checked for existence to decide if executing the action or not.
- The fetcher is bound to the component you need the data in. When the component is used, the fetcher automatically executes the action if not cached already, and if the property that should be checked for existence is not empty.
// To create a fetcher we use the function wrapActionForFetching
const getUserFetcher = wrapActionForFetching(
// The action which should be dispatched
getUser,
// The accessor function: check the returned property for existence to decide if action should be triggered.
// The parameter 'state' is the state of your store.
// Optionally, you also can use more parameters, if these are needed to check for the property (for example a specific userId for a user)
(state, userId) => state.users[userId]
);
// Your component
function UserScreen({user}) {
return (
<div>
{`Hello ${user.name} ${user.lastName}!`}
</div>
);
}
// Use Redux Connect to connect your component to the properties you want to use
const ConnectedUserScreen = connect(
(state, props) => {
return {
user: state.users[props.userId] || {}
}
}
)(UserScreen);
// Wrap your component with the fetcher.
// When the component is called the fetcher will check if the title is already there or the action has at least been triggered already. If both are false, the fetcher triggers the action to fetch the data.
const WrappedUserScreen = fetches(
getUserFetcher(
// The second argument of the accessor of getUserFetcher
// Necessary in this case as the userId needs to be provided to the accessor
props => props.userId
)
)(ConnectedUserScreen);
- You can find a full example here, the example is only compiled to ES6 currently.
- The source can be found in the
example
folder.
Evaluation if action should be dispatched
The fetcher (the wrapped action) uses two checks to decide if the action should be dispatched.
The fetcher checks the store for the value passed during the creation. If the value exists dispatching won't happen. If the value does not exist then go on with step 2.
A value which was set as condition is not defined yet. As second condition, check the internal fetcher cache if the action has already been dispatched. If the action was not dispatched yet, dispatch it now. If dispatched, do not dispatch it again.
Advanced
Cache reset
Internally, fetchers use a global cache to check if the action already has been dispatched. This cache can be controlled and reset (which normally needs to be done if a user switches accounts for example).
import {
cache
} from 'redux-fetchers';
// Resets the internal fetcher cache
// This cache is only there to not retrigger actions that are async
cache.reset();
Cache action without wrapping fetcher around component
Sometimes you may want to dispatch actions not directly wrapped around a component and still keep the caching functionality of the fetcher.
To achieve this, set the first argument of the fetcher to something falsy.
By doing this the action will definitely be dispatched (even if already in the cache of the fetcher), but won't be dispatched a second time when using a regular fetcher with a component, as it gets still stored in the fetcher cache by using this approach.
getUserFetcher(
dict => dict.userId
)(null, dispatch, {
userId: 'ABC123'
});
Error handling
redux-fetchers itself does not do much in terms of error handling, so you should:
- make sure your accessor-functions do not throw exceptions
- you handle exceptions in actions yourself
It's entirely possible that you have some wonky actions which fail. Currently there is no built-in mechanism that will refetch a failed action, this could be added in the future if there is a need for it. But it's still quite easy and powerful to use fetchers with wonky actions, see the example below on how to do it. You can find a running example here.
First we have an action, that sometimes fails.
// the wonky action
function wonkyAction () {
const shouldFail = Math.random() > 0.1;
return shouldFail ? {
type: 'WONKY_ACTION_FAIL'
} : {
type: 'WONKY_ACTION_SUCCESS'
};
}
For every failure of the action, the reducer will increment a counter that reflects how often the action failed.
// somewhere in the reducer we count a failure counter up
// so the fetcher would refetch it when it failed
// this gives the programmer maximal control, when to refetch and how often
...
switch(action.type) {
case 'WONKY_ACTION_FAIL':
return Object.assign({}, state, {
wonkyCounter: (state.wonkyCounter + 1)
});
case 'WONKY_ACTION_SUCCESS':
return Object.assign({}, state, {
wonkyResult: true
});
}
When we wrap the action, we don't use the wonkyActionCounter
yet, we still check just for existence.
const wonkyActionFetcher = wrapActionForFetching(
wonkyAction,
(state) => state.wonkyActionResult;
);
But when we wrap the component with the fetchers, we use it as one of the optional arguments:
function MyComponent ({wonkyActionCounter, wonkyActionResult}) {
return (
wonkyActionResult ?
<strong>{`I succeded in fetching the wonky action, ${wonkyActionCounter} attempts were needed`}</strong> :
<strong>{`I'm trying to fetch my wonky action, ${wonkyActionCounter} times so far.`}</strong>
);
}
const ConnectedMyComponent = connect(
state => {
return {
wonkyActionCounter: state.wonkyActionCounter,
wonkyActionResult: state.wonkyActionResult
}
}
)(MyComponent);
// we use the wonky fetcher with the provided wonkyActionCounter to refetch every time the counter goes up
// for this, the second argument is used, which only provides arguments for the caching
const MyComponentWithFetchers = const fetches(
wonkyFetcher(() => [], state => state.wonkyActionCounter)
)(ConnectedMyComponent);
License
MIT