npm install react-redux-provide --save
And then at the very beginning if your app:
import 'react-redux-provide/lib/install';
This is only necessary until React has a better
context
API.
If you'd rather not use the install
method, you must wrap your top component with the provide
function so that providers
can be retrieved via context
, assuming you follow convention and actually pass your providers
to the top component.
This library allows you to:
-
Build your applications in an extremely declarative manner. Think of it as the Redux equivalent of GraphQL/Relay, but slightly easier to get started (you decide).
-
Share and manipulate application state across any number of components. Updates are extremely efficient! Check the comments in the source.
-
Think of provider-specific
propTypes
as any otherimport
statement. The only difference is thatprops
are synchronized with the current state and obtained viacontext
. It's easiest to think about it like this: If a component isn't passed somepropType
as aprop
, it will be automatically provided to it viacontext
from the matching provider. -
Save, restore, and replicate the state of anything within your application. Providers can be used to represent and manage anything! Users, pages, themes, posts, comments, votes, maps, etc., etc., etc. The only limit is your imagination.
-
Use multiple Redux stores. Why multiple stores? This allows for a maximum separation of concerns while also making it possible to (re)use providers when manipulating the state of multiple provider instances of the same type (e.g., users).
-
Compose many different actions, reducers, etc., into a single provider.
-
Compose many small applications and/or services into a single application.
-
Maximally separate your concerns, which leads to fewer bugs and a better experience for developers.
-
Eliminates the need for any extraneous dependencies within components, which makes all components and providers instantly distributable and reusable. Components get the relevant Redux stores' states and action creators as
props
via React'scontext
. -
Hot reload your providers in development with no extra configuration.
If you're already familiar with Redux's Provider
component, that's not what this is, but it's where the inspiration came from. A provider can do essentially anything Redux can do, but it is encapsulated, reusable, extremely declarative, and mostly automatic.
For example, you might have a theme provider and a user provider. These providers are automatically instantiated as needed, so in the case of the user provider, you can have many different instances at the same time to represent multiple users. Each instance has its own "store" and works completely independently of one another, but you can also configure providers to automatically subscribe to and/or find/create/interact with others if necessary. "Stores" are responsible for holding the current state and handling actions to update that state. The current state and action creators (and any props
which might be derived from a combination of state and component props
) are automatically mapped to React components as needed.
A provider is a plain object composed of a handful of different properties (see below). These properties are designed to work together to represent and manipulate the state of anything within your application. You can also declaratively replicate the state of any provider instance's store to some database and/or to other clients. Redux stores are automatically created based on these properties and assigned to each provider instance.
Example:
export default {
// the basics
actions: { ... },
reducers: { ... },
merge: { ... }, // optional
middleware: [ ... ], // optional
enhancer: () => {}, // optional
// initialization
state: { ... }, // optional, typically set via app config
onInstantiated: () => {}, // optional, sometimes set via app config
onReady: () => {}, // optional, sometimes set via app config
// replication
key: () => {}, // optional, typically set via app config
replication: { ... }, // optional, typically set via app config
clientStateKeys: [ ... ], // optional, sometimes set via app config
// advanced
subscribeTo: { ... }, // optional, typically set via app config
subscribers: { ... }, // optional, typically set via app config
wait: () => {}, // optional, typically set via app config
clear: () => {}, // optional, typically set via app config
isGlobal: Boolean // optional, typically set via app config
};
Object containing Redux actions.
const PUSH_ITEM = 'PUSH_ITEM';
const actions = {
pushItem(item) {
return { type: PUSH_ITEM, item };
}
};
Note: When providing each action creator to React components, the result of the action creator is automatically wrapped with providerInstance.store.dispatch
, so with the above example, when you call this.props.pushItem(item)
, you're ultimately calling providerInstance.store.dispatch(actions.pushItem(item))
.
Object containing Redux reducers.
const reducers = {
list(state = [], action) {
switch (action.type) {
case PUSH_ITEM:
return state.concat(action.item);
default:
return state;
}
}
};
Note: Components will only be updated whenever a relevant reducer returns a new state - i.e., when state !== nextState
.
Optional object used for deriving props
which depend on the state of the store, possibly merged with the component's props
and/or context
. For example, suppose one of the reducers
returns an array, and some component only wants some item at a specific index.
const merge = {
item: {
keys: ['list'], // run `get` function only when list changes
get(state, props, context) {
// this is run only for components with an item propType
// and components will be updated only when the item has changed
const { list } = state;
const { itemIndex } = props;
return list[itemIndex] || null;
}
}
};
Optional Redux middleware specific to your provider. This can be either a single middleware or an array of middlewares.
import createLogger from 'redux-logger';
const middleware = createLogger();
// or const middleware = [ createLogger() ];
Note: Every provider is given its own custom thunk middleware. It works the same as redux-thunk
but always includes a special third providerApi
argument. See the thunk API section.
Optional Redux store enhancer specific to your provider. This can be either a single enhancer or an array of enhancers.
// from `provide-theme`
// ensures current theme is always initialized
// mainly for when store.setState or store.setKey is called
const enhancer = next => (reducer, initialState, enhancer) => {
const store = next(reducer, initialState, enhancer);
let currentThemeName = null;
function setTheme(state) {
const { themes, themesFiles, themeName } = state;
const theme = themes && themes[themeName] || state.theme;
const themeFiles = themesFiles && themesFiles[themeName];
let initAction = null;
currentThemeName = themeName;
if (theme || themeFiles) {
initAction = theme && actions.initTheme(themeName, themeFiles, theme);
if (initAction && (!canUseDOM || initAction.link && initAction.script)) {
store.dispatch(initAction);
} else if (canUseDOM) {
actions.loadTheme(themeName, themeFiles, theme)(store.dispatch);
} else {
store.dispatch(actions.loadTheme(themeName, themeFiles, theme));
}
}
}
setTheme(initialState || {});
store.subscribe(() => {
const nextState = store.getState();
if (nextState.themeName !== currentThemeName) {
setTheme(nextState);
}
});
if (process.env.NODE_ENV !== 'production') {
if (canUseDOM) {
window.themeReloaders.push((reloadedThemeName, theme) => {
const { themeName, themeFiles } = store.getState();
if (themeName === reloadedThemeName) {
actions.loadTheme(themeName, themeFiles, theme)(store.dispatch);
}
});
}
}
return store;
};
Optional string or function. Defaults to its respective key
within the providers
object. See the example below for more about the providers
object.
Providers are instantiated based on their key
:
-
If the
key
is a string, it's considered a static provider and is instantiated immediately at the top level. Thiskey
becomes the provider instance's identifyingproviderKey
. -
If the
key
is a function, it's considered a dynamic provider and is instantiated only as needed. The function should accept some component instance as an argument and should return a string which becomes the provider instance's identifyingproviderKey
. If a provider instance with thatproviderKey
already exists, the same instance will be used. The function may also return a falsy value, which will disable replication for the instance and theproviderKey
will default to its respectivekey
within theproviders
object.
Note: Replication won't work without a key
, since a falsy value indicates disabled replication.
Example:
providers.user.key = ({ props }) => props.userId
? `userId=${props.userId}`
: null;
The above key
will default to this. You typically don't need to set this yourself since it will come from the providers
object.
Optional object used for overriding the store's initial state. This is typically used only when the initial state can be derived from the current request and/or session, as the default initial state should come from the reducers
themselves. It works best to copy and extend providers using the spread operator.
renderApp({
providers: {
...providers,
theme: {
...providers.theme,
state: themeState
}
}
});
Optional function or array of functions to be called immediately after the provider has been instantiated and before its optional replicators have initialized. The provider instance will be passed to the function(s).
function onInstantiated(providerInstance) {
console.log(providerInstance);
}
Optional function or array of functions to be called immediately after the provider and its optional replicators have initialized. The provider instance will be passed to the function(s).
function onReady(providerInstance) {
console.log(providerInstance);
}
Additionally: During the very first tick of the app only on the client, the root provider component will look for a window.clientStates
object and convert it to provider instances which are cached within the current context
. Each state is removed from the window.clientStates
object as a result. If you're using the official page provider, you won't ever need to worry about this. See provide-page/src/defaultRenderDocumentToString.js
for an example where the client states are extracted from the server's provider instances' stores, stringified (using JSON.stringify
) and sent to the client for the initial render.
Replication is a key concept when building any stateful application where you need to store, retrieve, and/or share information. When implemented correctly, it allows you to decouple data initialization, storage, and retrieval from your application so that your only concern is rendering its state. It allows you declaratively connect application state to data sources and create efficient, scalable, and reliable software with relatively minimal effort. It allows you to instantly swap one database for another, one websocket implementation for another, or any other use case you can think of. If you're unfamiliar with replication, Wikipedia's State machine replication page is a good place to start!
Creating replicators for Redux and using them with providers should hopefully be pretty straightforward. Similar to providers, a replicator is a plain object with a few optional properties that describe how the state should be initialized and/or replicated - i.e., stored within a database or sent to other clients via websockets.
Optional object or array of objects. Uses redux-replicate
(a Redux store enhancer) under the hood. Each object should contain keys that match the arguments expected by redux-replicate
- i.e., { key, reducerKeys, replicator, queryable (optional), clientState (optional) }
. The key
will default to the provider instance's providerKey
.
Example where some user
provider instance's states are replicated to flat files whenever they change:
import fs from 'redux-replicate-fs';
const replication = {
// only these reducers are watched/replicated
reducerKeys: [
'userId',
'userName',
'userPasswordHash'
],
// and we want to be able to retrieve user instances by `userName`
queryable: [
'userName'
],
// and here we'll use node's file system API to store states in flat files
replicator: fs
};
See redux-replicate
for the full documentation which can be used within the replication
object. Also see redux-replicate-fs
for an example replicator implementation that gets the initial state, stores states upon changes, and handles queries.
Optional object that can be used as the base of each query. For example, if you know you almost always want to search for accounts that have not been deactivated, you would set the baseQuery
within your replication
object as follows:
const replication = {
reducerKeys: [
'accountName',
'accountDeactivated'
],
queryable: true,
baseQuery: {
accountDeactivated: false
}
};
Optional object that can be used as the base of each query's options. For example, if you want to limit the query results to some number by default:
const replication = {
reducerKeys: [
'userId',
'userName',
'userPasswordHash'
],
queryable: [
'userName',
'userDeleted'
],
baseQuery: {
userDeleted: false
},
baseQueryOptions: {
limit: 100
}
};
Optional array of reducerKeys
. This isn't used directly by react-redux-provide
, but it is the recommended way to specify which reducerKeys
should be sent to the client by the server. See provide-page
for an example of how this is used.
Optional object which will automatically subscribe to any matching providers by key
. Each value within the object should be a function that accepts the subscribed to provider instance as the first argument and the subscribing instance as the second argument.
For example, suppose you have a static theme
provider instance and you want its state to depend on the currently logged in user (to represent their selected theme). And suppose you're using the page
provider to contain the current userId
within its requestSession
state object. You could set the theme
provider to subscribe to the page
provider such that it updates the theme
store's key like this:
import theme from 'provide-theme';
function getThemeKey(pageState) {
const { userId } = pageState.requestSession;
return userId ? `theme&userId=${userId}` : null;
}
// disable replication by default
theme.replication.key = null;
theme.subscribeTo = {
page({ store: pageStore }, { store: themeStore }) {
// stores have a `setKey` method that allows you to change their key,
// which will reinitialize the store (i.e., state) based on the new key;
// only the replication key is changed, not the key used for identifying
// the provide instance
if (themeStore.setKey) {
const themeKey = getThemeKey(pageStore.getState());
if (themeKey !== themeStore.key) {
themeStore.setKey(themeKey);
}
}
}
};
export default theme;
Optional object which will automatically subscribe any matching providers by key
. Each value within the object should be a function that accepts the subscribed to provider instance as the first argument and the subscribing instance as the second argument.
Works exactly like subscribeTo
but in the opposite direction. The following is equivalent to the subscribeTo
example above (but don't do both because that would be silly):
import page from 'provide-page';
function getThemeKey(pageState) {
const { userId } = pageState.requestSession;
return userId ? `theme&userId=${userId}` : null;
}
page.subscribers = {
theme({ store: pageStore }, { store: themeStore }) {
if (themeStore.setKey) {
const themeKey = getThemeKey(pageStore.getState());
if (themeKey !== themeStore.key) {
themeStore.setKey(themeKey);
}
}
}
};
export default page;
Optional function that is called whenever a provider instance begins doing something asynchronously. This is used in conjunction with the clear
property below.
Optional function that is called whenever a provider instance finishes doing something asynchronously. The function will be called with true
as the first argument if the provider instance's store's state has changed.
The wait
and clear
functions are useful for things like server rendering, where you need to wait for asynchronous operations to complete before continuing.
Quick example:
import { pushWait, pushClear } from 'react-redux-provide';
import * as providers from './providers/index';
let rerender = false;
let waitCount = 0;
function wait() {
waitCount++;
console.log(`Waiting for async operation (${waitCount}) to finish...`);
}
function clear(stateChanged) {
if (stateChanged) {
rerender = true;
}
console.log(`Async operation (${waitCount}) done!`);
if (stateChanged) {
console.log(`The state changed during this operation.`);
} else {
console.log(`The state did not change during this operation.`);
}
if (--waitCount === 0) {
respondOrRerender();
}
}
pushWait(providers, wait);
pushClear(providers, clear);
// etc.
See provide-page/src/createMiddleware.js for a full example.
Optional boolean value. Useful for cases where you want to use the same provider instances across multiple requests on the server. An example of this is an ID generator provider which needs an atomic store.
All providers are given a special thunk middleware which includes a providerApi
object as a third argument to the action's returned function. The providerApi
contains a handful of useful functions for getting provider instance(s), setting provider states, dispatching actions, and finding data and/or getting instantiated providers based on that data. Any sorting/grouping is left entirely up to the replicator and the options passed to its handleQuery
function.
For example, suppose you want unique user names.
You would start by setting the user
provider's replication
property such that the userName
state is queryable:
const replication = {
// only these reducers are watched/replicated
reducerKeys: [
'userId',
'userName',
'userPasswordHash'
],
// and we want to be able to retrieve user instances by `userName`
queryable: [
'userName'
],
// and here we'll use node's file system API to store states in flat files
replicator: fs
};
And then the createUser
action would look like this:
const actions = {
createUser(state, genUserId, onSuccess) {
if (canUseDOM) {
// we want this to occur only on the server
return { type: CREATING_USER };
}
const { userName, userPassword } = state;
if (!userName) {
return actions.setUserError('Enter a name!');
}
if (!userPassword) {
return actions.setUserError('Enter a password!');
}
return (dispatch, getState, { createInstance, find }) => {
// here we need to ensure the user name is not already in use
find({ query: { userName }, options: { select: 'userId' } }, results => {
if (results && results.length) {
dispatch(actions.setUserError('User exists!'));
return;
}
const defaultState = getState();
const {
hasher = defaultState.hasher,
saltRounds = defaultState.saltRounds
} = state;
hasher.hash(userPassword, saltRounds, (error, userPasswordHash) => {
if (!userPasswordHash) {
dispatch(actions.setUserError('Error hashing password!'));
return;
}
// here we instantiate the user instance within the current context
// using the proper key - i.e., `userId=state.userId`
// and then the CREATE_USER action sets the proper userId, userName, etc.
// which is then replicated to your database of choice
state.userId = genUserId();
createInstance(state, userInstance => {
userInstance.store.dispatch({
...state,
userPasswordHash,
type: CREATE_USER
});
dispatch({
...state,
type: CREATED_USER
});
if (onSuccess) {
onSuccess(state.userId);
}
});
});
});
};
}
};
Uses instantiateProvider
to get some provider instance based on some state
- i.e., same method used when providing some instance to components.
A shortcut to get multiple instances at once. Uses getInstance
under the hood.
Just like getInstance
but ensures the initial states are replicated.
A shortcut to create multiple instances at once.
This should typically only be used client-side when the server sends the client states that it needs to properly perform some action. This function sets the states of multiple provider instances, or if they don't exist yet, their states are cached on the window.clientStates
object.
If you're using the official page provider, you won't ever need to use this function yourself. See the getPageStates
and submitForm
methods in provide-page/src/index.js
for exact usage.
Queries replicators with a handleQuery
method to retrieve an array of states representing any provider instance partially matching the state
argument. If doInstantiate
is true
, providers will be instantiated based on each state and passed to the callback
. If doInstantiate
is false
or omitted, only the array of states is passed to the callback
.
These properties are passed down via context
and are used interally by this library. You can override them with your own props at any time, but in most cases you should never have to do that.
Object containing all of the providers used throughout the app. The top level component should receive this prop. You can also pass a new providers
object (along with a new providerInstances
object) at any level, but you typically shouldn't do that.
Object containing all of the instantiated providers.
Object containing any queries being handled at the time.
Object containing the latest query results.
There are a handful of props you can use for finding provider instances whose states have been replicated to (stored within) data sources.
An object or a function which should return an object containing the query for the component. The query should typically contain a map of reducer keys to states you're searching for, but it can be anything supported by your replicator's handleQuery
method. The function will receive the component instance. If the function returns null
, the query will not be handled and the result
will be null
.
Example:
import React, { Component, PropTypes } from 'react';
class FooFinder extends Component {
static propTypes = {
fooName: PropTypes.string.isRequired,
query: PropTypes.any,
result: PropTypes.any
};
static defaultProps = {
query({ props: { fooName } }) {
return fooName ? { fooName } : null;
}
};
render() {
const { result } = this.props;
const numFound = result && result.length;
if (numFound) {
return (
<div className="foo-finder foo-found">
{JSON.stringify(result)}
</div>
);
} else {
return (
<div className="foo-finder foo-not-found">
No Foos found!
</div>
);
}
}
}
And so if the component is declared like this:
<FooFinder fooName="bar" />
And a couple of Foo
provider instances have "bar" as the state of their fooName
, the result
prop might look like this:
[
{
"fooId": 0,
"fooName": "bar",
"includes": "entire state by default"
},
{
"fooId": 1,
"fooName": "bar",
"etc": "etc"
}
]
The result
prop comes from your replicator's handleQuery
function. It should typically be an array of states.
Alternatively, you could simply pass a query
prop to the component (usually either a function or an object), rather than have it derived from defaultProps
.
Note: If the query
isn't compatible with any provider, it will be assumed that it isn't a provider query and will be treated as a normal prop.
Only used in conjunction with a valid query
. These options are passed to your replicator's handleQuery
function and should typically be specific to the options supported by the replicator.
Coming soon: A standard/recommended set of query options that replicators should support. Please see https://github.com/loggur/react-redux-provide/issues/33 and discuss!
Only used in conjunction with a valid query
. The result returned by your replicator's handleQuery
function. Defaults to null
. Only exists if the query
prop exists.
Works exactly the same as the query
prop, but should only be used if you know the base provider key for each query, as it is a map of base provider keys to queries. You typically just want to use the query
prop, as the queries will be mapped to their providers automatically.
The following will produce the same functionality and output as the above query
example:
import React, { Component, PropTypes } from 'react';
class FooFinder extends Component {
static propTypes = {
fooName: PropTypes.string.isRequired,
queries: PropTypes.any,
results: PropTypes.any
};
static defaultProps = {
queries({ props: { fooName } }) {
return {
foo: fooName ? { fooName } : null
};
}
};
render() {
const { results } = this.props;
const numFound = results && results.foo && results.foo.length;
if (numFound) {
return (
<div className="foo-finder foo-found">
{JSON.stringify(results.foo)}
</div>
);
} else {
return (
<div className="foo-finder foo-not-found">
No Foos found!
</div>
);
}
}
}
And the results
object would simply be:
{
"foo": [
{
"fooId": 0,
"fooName": "bar",
"includes": "entire state by default"
},
{
"fooId": 1,
"fooName": "bar",
"etc": "etc"
}
]
}
Note: If queries
isn't compatible with any provider, it will be assumed that it isn't provider queries and will be treated as a normal prop.
Only used in conjunction with a valid queries
. Works exactly like queryOptions
but with the options pre-mapped to each provider.
Only used in conjunction with a valid queries
. See queries
example above. Only exists if the queries
prop exists.
The enhancer adds the following to the store
object.
The current key
.
Sets the current key
. The readyCallback
is called after all of the replicators have fully initialized based on the new key
.
You typically shouldn't need to use this, as state changes should almost always occur as a result of store.dispatch(action)
. But it may be useful for keeping a store's state synchronized with some data source which doesn't rely on actions. If using reducerKeys
, the nextState
is expected to be an object and is merged into the current state, similar to React's setState
. If not using reducerKeys
, the nextState
replaces the current state entirely.
Mostly used internally for watching for whenever some reducer returns a different state so that components can efficiently know when they should update.
You can use this if you know your replicator(s) asynchronously initialize the store's state and would like to do something immediately after initialization. The readyCallback
will receive the key
and store
as arguments.
If for some reason you need to know whether or not getInitialState
has completed, you can check this boolean property. It will be true
after initialization.
Let's create a provider that can synchronously and asynchronously increment some count. We'll control the count with one component and display the current count with another.
// src/providers/counter.js
// import thunk from 'redux-thunk'; // for async actions
// as of v6.0, thunk middleware is no longer required
// since all providers are given their own custom thunk middleware
// which includes the provider API as a 3rd argument
const INCREMENT = 'INCREMENT';
const actions = {
increment(by = 1) {
return { type: INCREMENT, by };
},
incrementAsync(by = 1, delay = 1000) {
return dispatch => setTimeout(
() => dispatch(actions.increment(by)),
delay
);
}
};
const reducers = {
count(state = 0, action) {
switch (action.type) {
case INCREMENT:
return state + action.by;
default:
return state;
}
}
};
export default { actions, reducers };
Now let's see how easy it is to manipulate the count
state! We simply declare propTypes
for actions
that we want, and components will receive action creators as props
. For example, to dispatch the INCREMENT
action asynchronously, all we need to do is declare incrementAsync
within our propTypes
and eventually call this.props.incrementAsync()
.
Note: When providing each action creator to React components, the result of the action creator is automatically wrapped with providerInstance.store.dispatch
, so with the following example, when you call this.props.increment()
, you're ultimately calling providerInstance.store.dispatch(actions.increment())
.
// src/components/IncrementButtons.js
import React, { Component, PropTypes } from 'react';
export default class IncrementButtons extends Component {
static propTypes = {
increment: PropTypes.func.isRequired,
incrementAsync: PropTypes.func.isRequired
};
render() {
return (
<div className="IncrementButtons">
<button onClick={() => this.props.increment()}>
Increment
</button>
<button onClick={() => this.props.incrementAsync()}>
Increment asynchronously
</button>
</div>
);
}
};
Then to display the current count
within some component, simply declare count
as a propType
! The component will automatically re-render whenever the count
reducer returns a different state.
// src/components/CurrentCount.js
import React, { Component, PropTypes } from 'react';
export default class CurrentCount extends Component {
static propTypes = {
count: PropTypes.number.isRequired
};
render() {
return (
<div className="CurrentCount">
Current count is {this.props.count}!
</div>
);
}
};
Now how do we tie everything together? Easy! Two methods:
- The easiest and recommended method is to use this library's
install
module, which automatically wraps all of your React classes with theprovide
function. This allows all of your components to be packaged independently of this library. There's also a chance that future React versions will support this type of enhancement, so this is definitely the recommended method!
All you do is import 'react-redux-provide/lib/install'
at the top of your main entry (or entries).
// src/renderApp.js
import 'react-redux-provide/lib/install';
import React from 'react';
import { render } from 'react-dom';
import { App } from './components/index';
import defaultProps from './defaultProps';
function renderApp(props, element = document.getElementById('root')) {
return render(<App { ...props } />, element);
}
renderApp(defaultProps);
export default renderApp;
- If you would rather not use the
install
module, you can manually wrap each component with theprovide
function. In this case, your components will look something like this:
// src/components/CurrentCount.js
import React, { Component, PropTypes } from 'react';
import provide from 'react-redux-provide';
class CurrentCount extends Component {
static propTypes = {
count: PropTypes.number.isRequired
};
render() {
return (
<div className="CurrentCount">
Current count is {this.props.count}!
</div>
);
}
};
export default provide(CurrentCount);
Or if you would prefer to use it as an ES7 decorator:
// src/components/CurrentCount.js
import React, { Component, PropTypes } from 'react';
import provide from 'react-redux-provide';
@provide
export default class CurrentCount extends Component {
static propTypes = {
count: PropTypes.number.isRequired
};
render() {
return (
<div className="CurrentCount">
Current count is {this.props.count}!
</div>
);
}
};
And now when rendering the application, all we have to do is include a providers
prop.
import React from 'react';
import { render } from 'react-dom';
import * as providers from './providers/index';
import { App } from './components/index';
function renderApp(props, element = document.getElementById('root')) {
return render(<App { ...props } />, element);
}
renderApp({ providers });
export default renderApp;
We can also customize each provider's initial state.
renderApp({
providers: {
...providers,
counter: {
...providers.counter,
state: {
count: 42
}
}
}
});
-
For most providers, everything might fit within a single file, but you can structure your imports and exports however you want since each provider is ultimately just a single object.
-
Use provider factories to create providers with unique keys for common use cases. The
array
andmap
providers are good examples.// src/providers/todoList.js import provideArray from 'provide-array'; const todoList = provideArray('todoList', 'todoItem', 'todoItemIndex'); export default todoList;
-
Providers are composable objects! You can combine as many providers as you need. This is great when you have core functionality you would like to implement within multiple providers.
// src/providers/privateResource.js import provideResource from 'provide-resource'; import provideAuthentication from 'provide-authentication'; const privateResourceKey = 'privateResource'; const resource = provideResource(privateResourceKey); const authentication = provideAuthentication(privateResourceKey); const actions = { ...resource.actions, ...authentication.actions }; const reducers = { ...resource.reducers, ...authentication.reducers }; const middleware = [ ...resource.middleware, ...authentication.middleware ]; export default { actions, reducers, middleware };
-
Apps typically only need a
components
directory and aproviders
directory. Athemes
directory is also recommended! Seeprovide-theme
. Also see Lumbur for some boilerplate (with the Lumburjack generator) to start using this architecture in a matter of minutes. -
Components should have no knowledge of constants used within providers, which leads to a maximum separation of concerns and is always the best design. Everything (states and action creators) should be obtainable via
props
such that components simply render the application state and create actions. -
The more people who adopt providers, the better! The provider paradigm makes it insanely easy to integrate each other's services, build apps around specific providers, or even combine multiple apps and/or services into one.
-
There's a shortcut for dispatching actions, which might be useful when writing tests or inter-provider communication.
Instead of:
providerInstance.store.dispatch(providerInstance.actions.doSomething('fun'))
You can just do this:
providerInstance.actionCreators.doSomething('fun')
-
Check out
react-devtools
for a closer look at what exactly is going on when using providers!
Other than the provide
function, nearly all of the exports are utility functions.
The default export which accepts a React class and returns the class wrapped with a higher-order component designed to share and manage application state via context
. Only use this if you aren't using the install
module.
Useful for hot reloading stateless function components. The state of your app and your stores will remain intact!
See bloggur/src/renderApp.js
for an example of how to hot reload providers using webpack
's hot module replacement.
Useful for hot reloading of providers. The state of your app and your stores will remain intact!
See bloggur/src/renderApp.js
for an example of how to hot reload providers using webpack
's hot module replacement. All you have to do is import { reloadProviders } from 'react-redux-provide'
and pass your updated providers
to it.
if (process.env.NODE_ENV !== 'production') {
if (module.hot) {
module.hot.accept('./defaultProps', () => {
reloadProviders(require('./defaultProps').default.providers);
});
}
}
instantiateProvider (Optional Object fauxInstance, Object provider, Optional String|Function providerKey, Optional Function readyCallback, Optional Boolean createReplication)
Returns an instance of some provider. Each provider instance is assigned its own providerKey
and has its own store. And each instance is cached within the context
's provideInstances
object by its providerKey
. If the instance already exists, it will be returned. You probably won't ever need to use this.
The fauxInstance
should resemble a component instance - i.e., { props, context }
.
createProviderStore (Object providerInstance, Optional Mixed storeKey, Optional Boolean createReplication)
Creates and returns a store specifically for some provider instance. You probably won't ever need to use this.
Gets the state from window.clientStates
for the providerInstance
based on its providerKey
, if it exists. Returns null
if not. You probably won't ever need to use this.
Merges and returns the state
property with the result of getClientState
. Returns an empty object by default. You probably won't ever need to use this.
Gets a new fauxInstance
with the same context
but with new props
.
Gets and caches some property from the fauxInstance
's context
or props
.
Gets the providers
object from the fauxInstance
's context
or props
.
Gets the providerInstances
object from the fauxInstance
's context
or props
.
Gets the activeQueries
object from the fauxInstance
's context
or props
.
Gets the queryResults
object from the fauxInstance
's context
or props
.
Gets and caches some property from the fauxInstance
's context
or props
. If its a function, it will be called as value(fauxInstance)
and the result cached.
Gets the queries
object (using getFunctionOrObject
) from the fauxInstance
's context
or props
.
Gets the query
object (using getFunctionOrObject
) from the fauxInstance
's context
or props
.
Gets the queryOptions
object (using getFunctionOrObject
) from the fauxInstance
's context
or props
.
Gets the queriesOptions
object (using getFunctionOrObject
) from the fauxInstance
's context
or props
.
Gets all handleQuery
functions within replicators and returns the relevant { handleQuery, reducerKeys }
.
Merges the result
into the mergedResult
and returns the new mergedResult
.
Returns true
if the result
essentially matches the previousResult
.
Performs any queries which may exist within the fauxInstance
s props
and assigns the results
to props.results
if props.queries
, or assigns the flattened result
to props.result
if props.query
.
Returns a function that can be used for concatenating/normalizing certain provider properties. The push____
and unshift____
functions (below) were created using this function. You probably won't ever need to use this.
Example:
const middlewareKey = 'middleware';
export const pushMiddleware = createKeyConcat(middlewareKey);
export const unshiftMiddleware = createKeyConcat(middlewareKey, true);
Compares object A's top level values to object B's top level values and returns true if equal.
Adds middleware(s) to each provider's chain of middlewares. Useful when you want to apply middleware to many providers at once, specific to your application.
import { pushMiddleware, unshiftMiddleware } from 'react-redux-provide';
import firstMiddleware from 'first-middleware';
import lastMiddleware from 'last-middleware';
import { theme, user } from './providers/index';
unshiftMiddleware({ theme, user }, firstMiddleware);
pushMiddleware({ theme, user }, lastMiddleware);
Adds enhancer(s) to each provider's chain of enhancers. Useful when you want to apply enhancers to many providers at once, specific to your application.
import { pushEnhancer, unshiftEnhancer } from 'react-redux-provide';
import firstEnhancer from 'first-enhancer';
import lastEnhancer from 'last-enhancer';
import { theme, user } from './providers/index';
unshiftEnhancer({ theme, user }, firstEnhancer);
pushEnhancer({ theme, user }, lastEnhancer);
Adds function(s) to each provider's array of onInstantiated
callbacks. Useful when you want to do something as each provider is instantiated, specific to your application.
import { pushOnInstantiated } from 'react-redux-provide';
import * as providers from './providers/index';
pushOnInstantiated(providers, providerInstance => {
const { providerKey, store } = providerInstance;
console.log(providerKey, store);
});
Adds function(s) to each provider's array of onReady
callbacks. Useful when you want to do something as soon as each provider and its optional replicators are ready, specific to your application.
import { pushOnReady } from 'react-redux-provide';
import * as providers from './providers/index';
pushOnReady(providers, providerInstance => {
const { providerKey, store } = providerInstance;
console.log(providerKey, store);
});
Adds replication to each provider. Useful when you want to apply replication to many providers at once, specific to your application.
import { pushReplication, unshiftReplication } from 'react-redux-provide';
import firstReplication from './replication/first';
import lastReplication from './replication/last';
import { theme, user } from './providers/index';
unshiftReplication({ theme, user }, firstReplication);
pushReplication({ theme, user }, lastReplication);
Adds replicator(s) to the each provider's replication
property, if it exists. Especially useful when client and server have nearly the same replication but require different replicators. It's common for packaged providers to include a replication
property without a replicator
so that it's easy to know which reducerKeys
should typically be replicated while leaving the actual (and optional) replicator up to the developer.
// client
import { pushReplicator } from 'react-redux-provide';
import localforage from 'redux-replicate-localforage';
import * as providers from './providers/index';
pushReplication(providers, localforage);
// server
import { pushReplicator } from 'react-redux-provide';
import rethink from 'redux-replicate-rethink';
import * as providers from './providers/index';
pushReplication(providers, rethink);
Adds function(s) to each provider's array of wait
and/or clear
functions. Useful when you want to wait for an asynchronous operation to finish.
import { pushWait, pushClear } from 'react-redux-provide';
import * as providers from './providers/index';
let rerender = false;
let waitCount = 0;
function wait() {
waitCount++;
console.log(`Waiting for async operation (${waitCount}) to finish...`);
}
function clear(stateChanged) {
if (stateChanged) {
rerender = true;
}
console.log(`Async operation (${waitCount}) done!`);
if (stateChanged) {
console.log(`The state changed during this operation.`);
} else {
console.log(`The state did not change during this operation.`);
}
if (--waitCount === 0) {
respondOrRerender();
}
}
pushWait(providers, wait);
pushClear(providers, clear);
// etc.