Included Bundles
We take a "batteries included" approach where you don't have to use any of this stuff but where a pretty complete set of tools required for apps is included out of the box.
createDebugBundle([optionsObject])
createDebugBundle([optionsObject])
This is meant to be leave-in-able in production. It works as follows:
Unless localStorage.debug
is set to something "truthy" it will do nothing.
It takes the following options (none are required):
logSelectors
(default: true): whether or not to log out selectors and their computed value with each action dispatchlogState
(default: true): whether to log state after each dispatchactionFilter
(default: null): a function to call that determines whether or not to log an action (if debug is enabled). For example, if you want hide theAPP_IDLE
actions pass this:(action) => action.type !== 'APP_IDLE'
enabled
(default: HAS_DEBUG_FLAG): explicitly enable/disable. This is helpful in node.js where there's no localStorage flag.ignoreActions
(default: []): an array of actions to ignore when logging.
If enabled:
The store is bound to
window.store
for easy access to all selectors and action creators since they're all bound to the store. This is super helpful for debugging state issues, or running action creators even if you don't have UI built for it yet.On boot, it logs out list of all installed bundles
On each action it logs out:
action object that was dispatched
the current state in its entirety
the result of all selectors after that state change
if there's a reactor that will be triggered as as result, it will log that out too as
next reaction
In order to support use inside a Web Worker which doesn't have localStorage
access debug state is stored in a reducer and it includes doEnableDebug()
and doDisableDebug()
action creators. But most people won't need this. Simply use the localStorage flag.
createUrlBundle([optionsObject])
createUrlBundle([optionsObject])
A complete redux-based URL solution. It binds the browser URL to Redux store state and provides a very complete set of selectors and action creators to give you full control of browser URLs.
Handling in-app navigation: An extremely lightweight in-app navigation approach is to just by rendering normal <a>
tags, add an onClick()
handler on your root component and use internal-nav-helper to inspect the events, calling doUpdateUrl
as necessary. When click events bubble up, it will inspect the event target looking for <a>
tags and then determining whether or not to consider it an internal link based on its href. See internal-nav-helper library for more details.
Sample root component:
Options object:
inert
: Boolean whether or not to bind to the browser. If you make itinert
it will simply maintain state in Redux without trying to update the browser, or listen forpopstate
handleScrollRestoration
: Boolean (defaulttrue
). Whether or not to handle scroll position restoration on document.body. Some browsers handle this for you with the notable exception of FF and IE 11. If you leave this astrue
it should work in latest version of all browsers.
Action creators:
doUpdateUrl(pathname | {pathname,query,hash}, [options])
: Generic URL updating action creator. You can pass it any pathname string or an object withpathname
,query
, andhash
keys. ex:doUpdateUrl('/new-path')
,doUpdateUrl('/new-path?some=value#hash')
. You can pass{replace: true}
as an option to triggerreplaceState
instead ofpushState
. Additionally, you can pass{ maintainScrollPosition: true }
for cases when you do not expect window scroll position to be reset to top as a result of route transition.doReplaceUrl(pathname | {pathname,query,hash})
: just likedoUpdateUrl
but replace is prefilled to replace current URL.doUpdateQuery(queryString | queryObject, [options])
: can be used to update query string in place. Either pass in new query string or an object. It does a replaceState by default but you can pass{replace: false}
if you want to do a push.doUpdateHash(string | object, [options])
: for updating hash value, does a push by default, but can do replace if passed{replace: true}
.
Selectors:
selectUrlRaw()
: returns contents of reducer.selectUrlObject()
: returns an object like what would come fromnew URL()
but as a plain object.selectQueryObject()
: returns query string as an objectselectQueryString()
: returns query string as a stringselectPathname()
: returns pathname, without hash or queryselectHash()
: returns hash value as stringselectHashObject()
: returns hash value as object (if relevant)selectHostname()
: returns hostname as string.selectSubdomains()
: returns array of subdomains, if relevant.
createRouteBundle(routesObject, optionsObject)
createRouteBundle(routesObject, optionsObject)
Takes an object of routes and returns a bundle with selectors to extract route parameters from the routes.
Example:
The value like Home
, UserList
, etc, can be anything. Whatever the current route that matches, calling selectRoute()
will return whatever that is. This could be a root component for that "page" in your app. Or it could be an object with a component name along with a page title or whatever else you may want to link to that route.
Then in your root component in your app you'd simply selectRoute()
to retrieve it.
Options object:
routeInfoSelector
: String (default:'selectPathname'
) used to configure the key that is used for matching the current route on. Set it to'selectHash'
to enable hash-based routing. Note: Currently you need entries for both '' and '/' if you rely on hash-based routing.
Selectors:
selectRouteParams()
: returns an object of any route params extracted based on current route and current URL. In the example above /users/:userId
would return {userId: 'valueExtractedFromURL'}
. selectRouteMatcher()
: returns the route matcher function used. Can be useful for seeing what result a URL would return before actually setting that URL. selectRoutes()
: returns the routes object originally passed in. Can be useful for static sites where you want to pre-render all available pages at build time. selectRoute()
: returns whatever the value was in the routes object for the current matched route. selectRouteInfo()
: returns the key that was passed to the route matcher. By default this is the value of selectPathname
as defined by the createUrlBundle
above.
Action creators:
doReplaceRoutes()
: takes new set of routes. This can be useful if you're using placeholder routes and want to dynamically load and replace them with real ones if you're doing extensive code splitting or using "sub app" type architectures. Note that calling this will trigger a ROUTE_MATCHER_REPLACED
action with a payload
property of {routes: newRoutes, routeMatcher: newRouteMatcher}
. If you really want a different name for the action that is triggered, you can add {replaceAction: "OTHER_ACTION_NAME"}
to the options passed when calling createRouteBundle
.
createReactorBundle(optionsObject)
createReactorBundle(optionsObject)
This is the functionality that allows for the reactX
pattern in your bundles. Manual configuration here is entirely optional.
This bundle is included by default when you use composeBundles
. If you want to pass it custom options you'll have to use composeBundlesRaw
instead.
Available options:
idleTimeout
: Number (default30000
). Idle timeout is time to fire anAPP_IDLE
event.idleAction
: String (default'APP_IDLE'
). Action type to dispatch on idle.cancelIdleWhenDone
: Boolean (defaulttrue
). In certain cases this can be useful. For example, if you're using reactors in a node process and you want it to be able to exit when there's no pending reactions. In browsers, this will be ignored.doneCallback
: Function (defaultnull
). If you want to pass a callback to call when there are no more pending reactions, you can do so.stopWhenTabInactive
: Boolean (defaulttrue
). By default if a given tab is in the background we don't want to keep wasting cycles. But, in certain cases you don't want it to stop just because its in the background. Gives you that option. Note: this is implemented by taking advantage of behavior ofrequestAnimationFrame
. So it relies on the browser for this logic.reactorPermissionCheck
: Function (default: null). This function, if passed, will be given the name of the reactor, and the result of having called it that would normally have lead to a reaction being queued. If you returnfalse
from this function the reactor will not be queued. This allows you to implement rate limiting, etc. It also makes it possible to build in safe-guards for infinite reaction loops or other development tools.
appTimeBundle
appTimeBundle
This simply tracks an appTime
timestamp that gets set any time an action is fired. This is useful for writing deterministic selectors and eliminates the need for setting timers throughout the app. Any selector that uses selectAppTime
will get this time as an argument. It's ridiculously tiny at only 5 lines of code, but is a nice pattern. Just be careful to not do expensive work in reaction to this changing, as it changes with each action.
asyncCountBundle
asyncCountBundle
This bundle takes no options, simply add it as is. It uses action naming conventions to track how many outstanding async actions are occurring.
It works like this:
If an action contains STARTED
it increments, if it contains FINISHED
or FAILED
it decrements. It adds a single selector to the store called selectAsyncActive
. This is intended to be used to display a global loading indicator in the app. You may have seen these implemented as a thin colored bar across the top of the UI.
createCacheBundle(optionsObject)
createCacheBundle(optionsObject)
Adds support for local caching of bundle data to the app. Other bundle can declare caching when this has been added to the app.
This bundle takes one required option: cacheFn
a function to use to persist data. The function has to take two arguments: the key and the value and return a Promise
. Suggested caching lib: money-clip.
Once the caching bundle has been added, other bundles can indicate that their contents should be persisted by exporting a persistActions
array of action types. Any time one of those action types occur, the contents of that bundle's reducer will be persisted lazily. Again, see the example app for usage.
Two other options are supported:
enabled: [Boolean]
: by default, it will be enabled in the browser only. Passingenabled: [Boolean]
allows explicitly specifying whether it should be enabled or not. So if you're wanting to persist things in node.js, make sure you're passingtrue
.logger: [Function]
: by default it logs nothing. If you want to log a success message after things have been persisted. Pass a function here, for example:{ logger: console.log.bind(console), cacheFn: () => { ... } }
Example usage:
createAsyncResourceBundle(optionsObject)
createAsyncResourceBundle(optionsObject)
Not in main index, be imported directly: import createAsyncResourceBundle from 'redux-bundler/dist/create-async-resource-bundle'
(note, this requires inclusion of redux-bundler/dist/online-bundle
in your app as well).
Returns a pre-configured bundle for fetching a remote resource (like some data from an API) and provides a high-level abstraction for declaring when this data should be considered stale, what conditions should cause it to fetch, and when it should expire, etc.
This bundle requires appTimeBundle
and onlineBundle
to be added as well (order doesn't matter) as long as both are included.
Options:
name
(required): name of reducer. Also used in action creator names and selector names. For example if the name isusers
you'll end up with a selector named:selectUsers()
.getPromise
(required): A function that should return a Promise that gets the data. If this throws, it will automatically be retried. If you want to consider it a permanent error that should not be retried throw an error object with aerror.permanent = true
property. note: this function will be called with the same arguments as you get when writing a thunk action creator:({dispatch, getState, store, ...extraArgs }) => {}
actionBaseType
(optional): This is used to build action types. So if you passUSERS
, it will use action types likeUSERS_FETCH_STARTED
andUSERS_EXPIRED
. Default:name.toUpperCase()
.staleAfter
(optional): Length of time in milliseconds after which the data should be considered stale and needing to be re-fetched. Default: 15 minutes.retryAfter
(optional): Length of time in milliseconds after which a failed fetch should be re-tried after the last failure. Default: 1 minute.expireAfter
(optional): Length of time in milliseconds after which data should be automatically purged because it is expired. Default:Infinity
.checkIfOnline
(optional): Whether or not to stop fetching if we know we're offline. This is imperfect because it's listens for the globaloffline
andonline
events from the browser which are good for things like airplane mode, but not for "lie-fi" situations. Default:true
persist
(optional): Whether or not to include thepersistActions
required to cache this reducers content. Simply setting this totrue
doesn't mean it will be cached. You still have to make sure caching is setup usingcreateCachingBundle()
and a persistance mechanism like money-clip. Defaulttrue
Action creators:
Names are built dynamically using the name
of the bundle with first letter upper-cased:
doFetch{Name}
: what is used internally to trigger fetches, but you can trigger it manually too.doMark{Name}AsOutdated
: used to forcibly mark contents as stale, which will not clear anything, but will cause it to be re-fetched as if it's too old.doClear{Name}
: clears and resets the reducer to initial state.doExpire{Name}
: should mostly likely not be used directly, but it used internally when items expire. This is a bit likedoClear{Name}
except it does not clear errors and explicitly denotes that the contents are expired. So, if an app is offline and the content was wiped because it expired, your UI can show a relevant message.
Selectors:
select{Name}Raw
: get entire contents of reducer.select{Name}
: getdata
portion of reducer (ornull
).select{Name}IsStale
: Boolean. Is data stale?select{Name}IsExpired
: Boolean. Is it expired?select{Name}LastError
: Timestamp in milliseconds of last error ornull
select{Name}IsWaitingToRetry
: Boolean. If there was an error and it's in the period where it's waiting to retry.select{Name}IsLoading
: Boolean. Is it currently trying to fetch.select{Name}FailedPermanently
: Boolean. Was aerror.permanent = true
error thrown? (if so, it will stop trying to fetch).select{Name}ShouldUpdate
: Boolean. Based on last successful fetch, errors, loading state, should the content be updated?
Defining the state that should trigger the fetch:
Rather than manually calling doFetch{Name}
from a component, you can use a reactor to define the scenarios in which the action should be dispatched. The simplest way is to add it to your bundle after generating it and then using the select{Name}ShouldUpdate
as an input selector. The following code would cause the fetch to happen right away and the data to be kept up to date not matter what state the rest of the app was in or URL/Route was being displayed.
If instead you wanted to only have the fetch occur on a certain URL or route, or based on other conditions, you can check for that as well by adding and checking for other conditions in your reactor:
onlineBundle
onlineBundle
Not in main index, be imported directly: import onlineBundle from 'redux-bundler/dist/online-bundle'
Tiny little (18 line) bundle that listens for online
and offline
events from the browser and reflects these in redux. Note that browsers will not detect "lie-fi" situations well. But these events will be fired for things like airplane mode. This can be used to suspend network requests when you know they're going to fail anyway.
Exports a single selector:
selectIsOnline
: Returns current state.
Last updated