Plugins
Plugins are a way to add or change the behavior of a
resourceReducer
.
There are two main use cases for plugins:
adding additional functionality to the reducer, such as custom action types
extending the behavior of the reducer for the built-in action types
Adding Additional Functionality
You will often want to use more action types than the built-in action types. For instance, if your interface allows users to "select" resources with checkboxes, then you want may to add support for some selection action types.
Plugins allow you to add support for custom action types to support features like this.
Extending Built-in Action Types
The notion of a "request" in Redux Resource is intentionally generic. It's not tied to any specific protocol, such as HTTP. What this means is that when a request fails, the only information that you have is that the request failed, and not that it failed with, say, a 404 status code. That's because 404 status codes are a feature of HTTP requests. Consequently, if you're using HTTP requests, then you'll likely want to use a plugin to give you more information about the requests that you make.
Similarly, if you're using GRPC, or some other system, then you'll want a plugin to give you more information about those types of requests and responses.
Furthermore, if you're using a system like JSON API, then you can use plugins to add support for features such as relationships or response metadata.
There's an official HTTP Status Codes plugin that makes working with HTTP requests even better.
Officially maintained plugins for other common protocols are on our to do list, but in the meantime, we've done our best to make it straightforward to write your own.
Using a Plugin
You define plugins per-resource when you call
resourceReducer
. The second
argument to that function is an options
options, and within it you can pass
plugins
as an array:
import resourceReducer from 'redux-resource';
import somePlugin from './some-plugin';
import anotherPlugin from './another-plugin';
export default resourceReducer('books', {
plugins: [somePlugin, anotherPlugin]
});
Writing a Plugin
A plugin is a function that with the following signature:
(resourceName, options) => reducerFunction
Where resourceName
and options
are the same values that you passed to
resourceReducer
.
The return value, reducerFunction
, is also a function. This returned function
has the same signature as a Redux reducer:
(previousState, action) => newState
where state
is the value of the state after running it through the built-in
reducer and action
is the action that was dispatched.
The simplest plugin then (which doesn't do anything), would look like this:
function myPlugin(resourceName, options) {
return function(state, action) {
return state;
}
}
If you prefer using arrow functions, you might choose to write this like so:
const myPlugin = (resourceName, option) => (state, action) => state;
This plugin isn't very exciting, so let's look at more realistic examples.
Custom Action Types
Let's build a plugin that lets a user select resources from a list. The code for this plugin looks like this:
import { setResourceMeta } from 'redux-resource';
import myActionTypes from './my-action-types';
export default function(resourceName, options) {
return function(state, action) {
// Ignore actions that were dispatched for another resource type
if (action.resourceName !== resourceName) {
return state;
}
if (action.type === myActionTypes.SELECT_RESOURCES) {
return {
...state,
meta: setResourceMeta({
resources: action.resources,
meta: state.meta,
newMeta: {
selected: true
},
initialResourceMeta: options.initialResourceMeta
})
};
} else if (action.type === myActionTypes.UNSELECT_RESOURCES) {
return {
...state,
meta: setResourceMeta({
resources: action.resources,
meta: state.meta,
newMeta: {
selected: false
},
initialResourceMeta: options.initialResourceMeta
})
};
} else {
return state;
}
}
}
You would then use this plugin like so:
import { createStore, combineReducers } from 'redux';
import { resourceReducer } from 'redux-resource';
import selectResources from './plugins/select-resources';
let store = createStore(
combineReducers({
books: resourceReducer('books', {
plugins: [selectResources]
}),
})
);
Changing Built-In Action Type behavior
Interestingly, the built-in behavior of Redux Resource is itself just a plugin.
Additional plugins are run after this built-in plugin runs, so you can write plugins that make further adjustments to the state after the built-in plugin. In the following plugin, we set a property on the store anytime a successful read occurs:
export default function(resourceName, options) {
return function(state, action) {
// Only take action if the resource name of the Action matches the
// resource this plugin is registered for
if (action.resourceName !== resourceName) {
return state;
}
if (action.type === 'READ_RESOURCES_SUCCEEDED') {
return {
...state,
readSucceeded: true
};
} else {
return state;
}
};
}
Customizable Plugins
You can write plugins that can be customized per-slice by taking advantage of
the fact that the resourceReducer
's options are passed into plugins. For
instance, if you had a plugin like the following:
export default function customizablePlugin(resourceName, options) {
return function(state, action) {
if (options.useSpecialBehavior) {
// Perform a computation
} else {
// Do some other computation here
}
};
}
then you could trigger the special behavior by passing
useSpecialBehavior: true
as an option to resourceReducer
:
import resourceReducer from 'redux-resource';
import customizablePlugin from './customizable-plugin';
export default resourceReducer('books', {
plugins: [customizablePlugin],
useSpecialBehavior: true
});
If this API isn't to your liking, then you can also just wrap the plugin itself in a function, like so:
export default function(pluginOptions) {
return function customizablePlugin(resourceName, options) {
return function(state, action) {
if (pluginOptions.useSpecialBehavior) {
// Perform a computation
} else {
// Do some other computation here
}
};
};
}
which would be used in the following way:
import resourceReducer from 'redux-resource';
import customizablePlugin from './customizable-plugin';
export default resourceReducer('books', {
plugins: [
customizablePlugin({ useSpecialBehavior: true})
]
});
You may dislike this approach due to the tripley-nested functions. That's fine, because either way works. Use the version that makes the most sense to you.
Best Practices
Because plugins are so similar to reducers, you can use a switch
statement
and support multiple action types within each plugin. This is usually a
good thing, but be mindful of keeping each plugin limited to a single
responsibility.
For example, in the above example of a plugin for selecting resources, it supports two Action types – one for selection, and one for deselection. This plugin encapsulates that one responsibility, and it isn't responsible for any other Action types.
We recommend having a plugin for each distinct responsibility.