redux-apis v2.0.0.alpha.2
Helpers for creating Redux-aware APIs
Installation
npm install --save redux-apis
Usage
- Use Apis shared by the community
- Create your own APIs
- Compose existing APIs into new ones
- Link the top-level Api to a Redux store
- Use redux-apis with React components
- Scoped isomorphic fetch with redux-fetch-api
- Async actions with redux-async-api
- Server-side rendering with redux-load-api
- Use redux-apis with Hot Module Replacement
Use Apis shared by the community
I'm hoping for redux-apis
to become a community-driven project to further the use
of Redux and React. Read Share your Api at the bottom of this page
to learn how to contribute and get your Api listed here.
Create your own APIs
redux-apis
let's us create APIs that will feel very natural to consuming code and that completely
hide that redux is being used under the hood.
Extend from Api
To create an API, we extend from the class Api
or one of it's descendants.
; // ...
Create properties
An Api automatically gets it's own private slice of the Redux state tree, accessible via
this.getState()
. But that state can often be very 'raw'. It should be in it's most
'normalized' form (e.g. no derived state), which can make it hard to work with.
By implementing properties that inspect, but do not manipulate, the state tree,
we can make custom properties that are interesting to the outside world. We can make
these properties read-only by only providing a getter for them, and we can make the
'public' by specifying that they should be enumerable
:
{ superstate; Object; }
The code above assumes that our state has a boolean flag open
... But we need to provide
that initial state.
Provide initial state
We provide initial state by setting a default value for the state
parameter of the constructor:
{ superstate; Object; }
If we intend our Api class to be derived from, we can publish the initial state so the extending Api can make use of it:
static INITIAL_STATE = base: 'basic' ; { superstate; } static INITIAL_STATE = ...BaseINITIAL_STATE derived:'extended' ; { superstate; } ; // { base: 'basic', derived:'extended' }
Create manipulators
To manipulate the state tree, we provide methods that create and dispatch an action:
{ superstate; Object; } { this; }
Note the second pair of braces in the call to createAction
. This pattern is copied from
redux-actions... Heck, the code itself is
copied from redux-actions
! ;) The resulting actions follow the
Flux Standard Action format.
Register action handlers
We then register handlers for these actions in the constructor:
{ superstate; Object; this; } { this; }
Note that handleOpen
returns a new object. We never mutate the existing state, but always
either return it unchanged, or return a new object. We initialize the new object with the current
state using the ES6 spread operator ...
to copy all state properties to the new object. Then we
overwrite the open
property with the new value. Copying the current state is not strictly needed
in this example as there are no other properties, but writing your code like this ensures it will
keep working when you decide to add more state properties later, or when the class is derived from.
Finishing up our first Api
Let's add a close command to finish our first Api. We'll also use arrow functions to make the code a bit more elegant:
{ superstate; Object; this; this; } { this; } { this; }
The cool thing is that we now already have a working API. No redux store is needed to make it work, because Api objects will maintain their own state when not connected to a Redux store. This is helpful when testing:
var api = ;
Note how we call init()
on the new instance. This initializes the state tree by sending it an
INIT action. If you use Api objects independently from Redux, you are responsible
for initializing them yourself, by calling init
on the top-level Api. If you bind your Api to
a Redux store, this is done automatically by the Redux store.
Now that we have an initialized Api, we can use it:
console;api;console;api;console;
Because the Api
constructor accepts a state argument, we can also initialize it manually:
const leftDrawer = open: true;console;
Compose existing APIs into new ones
Because each Api has it's own state slice, we can easily use the same Api multiple times.
We use the function link(parent, child, link = apiLink)
for this purpose.
;; { superstate; thisleftDrawer = ; thisrightDrawer = ; } const app = ;
That's it! app
now automatically has a state tree that looks like this:
leftDrawer: open: false rightDrawer: open: false
We can use app
like this:
appleftDrawer;console;
Action types are namespaced
The example above works, because Api.createAction
, Api.dispatch
and Api.reducer
all follow
the same naming conventions and are aware of the (state) hierarchy via the link created with the
link
function. When you call createAction
with an actionType
argument, it checks for a link
to a parent Api and, if it's set, prepends the parent Api's name and a slash to the actionType
before passing it on to it's parent. So in the example above, when we called
appApileftDrawer
it actually resulted in an action being created with actionType
equal to
'leftDrawer/OPEN'
dispatch
then passes that action along to it's parent, all the way to the top-level. From there,
reducer
is invoked and is doing the opposite; it's breaking the actionType
apart into separate
parts and routing the action to it's destination. Along the way, 'reducer' will be invoked on all
nodes of the state tree. If it finds a registered handler for the action, it invokes that and returns
it's result. Otherwise, it returns the current state, or the (default) initial state. The side effect
of this is that we can initialize the entire state tree by just sending it an action that it does not handle.
This is exactly what init()
does; it dispatches an action with type '@@redux/INIT'
. Redux itself
does it exactly the same way.
Api as a mini-store
In fact, Api
mimics the api of a redux store, so that conceptually we are building a hierarchy of
redux-like mini-stores
that maps onto the state tree, each store in the hierarchy managing it's own
private slice.
link
parameter
The link
has a third parameter, also called link
, that allows us to customize how the state slice
of the child is extracted from the parent state. By default, it's using apiLink
, which dynamically
finds the name of the child api within it's parent and uses that name to select the right property
from the state slice. This creates a 1 to 1 mapping:
{ superstate; thisleftDrawer = ; thisrightDrawer = ; }
maps to
leftDrawer: open: false rightDrawer: open: false
However, we can customize this by creating a function that accepts two arguments, parentState
and childState
and that performs the mapping. This function should be read/write; meaning it should
be able to 'select' the child state from the parentState
, as well as 'update' the parent state
based on the childState
. Let's look at an example. Suppose we want to map the leftDrawer Api onto
the state key drawer
instead of leftDrawer
. We could do this:
{ superstate; thisleftDrawer = ; thisrightDrawer = ; }
maps to
drawer: open: false rightDrawer: open: false
Inside the link function, you have access to the child element via the this
keyword, so we could also have done this:
{ superstate; thisleftDrawer = ; thisleftDraweralias = 'drawer'; thisrightDrawer = ; }
Finally, for this common scenario, there is a helper function namedLink
that
will create the linker function for us . We could have used it like this:
{ superstate; thisleftDrawer = thisrightDrawer = ; }
Easy isn't it?
Link the top-level Api to a Redux store
Until now, Redux never came into play. And in fact you don't need it.
redux-apis
has no runtime dependency on redux and can actually be
used without Redux itself! The handlers you registered are reducers
and Api.reducer
acts like a generic reducer routing the incoming
actions to the registered handlers. This is very convenient for testing.
But using Redux has many advantages. There is a lot of middleware
available for it; small pieces of code that hook into the redux control
flow. Things like redux-thunk
to allow us to dispatch functions (for async handling for example) and
redux-logger that allows us to
log all dispatched actions. Also, redux gives us a subscription model
for listening to store events.
Here is how you link an Api to a redux store:
;;; // First, create your top-level Api object, but don't initialize it// the redux store will take care of thatconst app = ;
The cool thing is that app.reducer
is a Redux reducer! It is auto-bound
to the app
instance, so we can just pass it along to createStore
to
make redux call it:
// We can create a Redux store just like we always do. Just pass app.reducer!let initialState = some: 'state' ; // optionalconst store = ;
Almost there. In fact, our app has already been initialized with either the supplied initial state, or the initial state encoded in our Api components. We just need to link back the app object to the redux store, so dispatched actions will be routed to the redux store:
;
There you go!
In this example we are linking the app reducer as the sole root reducer.
But in fact it's a reducer just like any other so we are able to combine
it together with other reducers using redux's combineReducers
, or any
of the other methods available.
const reducer = ; const store = ;
When the root Api is not mounted to the root of the redux store state tree,
as in the example above where it is linked to the 'app'
key, we need to
inform it on how to get it's own slice of the state tree. We can do this by
providing a third argument to link
, which is a function that gets/sets the
child state from/into the parent state. We use namedLink
here, which is a
function that returns such a linker function based on a simple name:
// link app to store, getting state from key 'app';
When we don't supply a third argument to link
, the default apiLink
is used.
It just tries to find the child object among the parent's properties and then
uses the name of that property as the key of the state slice. We can use this
fact to simplify the above code to:
// link(store, app, namedLink('app'));// equavalent to:storeapp = ;// key name 'app' is implied from the fact that the app// is on a property named 'app' on the parent object.
We are currently using the vanilla createStore
from redux, but we could
boost that with middleware in exactly the same way we always do with redux.
redux-api
is just another component, latching onto the redux store with a
plain old reducer function. Simple!
Use redux-apis with React components
The standard way of linking React components to a redux store is with
the connect
decorator from react-redux
and that does not change if you use redux-apis. But Api's do offer
a convenience method connector
, which, like reducer
, is auto-bound
to it's Api instance, making connecting a Redux Api instance to a
React component as simple as:
;;; // exports some Api instance @Component { // when we get here, `connect` will have called `app.connector`, which will // have made all enumerable properties of `app` available in `this.props` const some api properties = thisprops; }```js `appconnector` returns an object with the enumerable properties from `app` init, making them available for use in the React component. But by default, Apiinstances have no enumerable properties. So in order to have the properties`some`, `api` and `properties` from the example above be added to props, we shouldmake them available on our Api. As an example, let us implement `/appjs` fromthe example above: ```js; { superstate; ; ; ; }
This is a very simple example, but the use of enumerable properties will both
give us fine grained control on the one hand, as well as very convenient and
natural use on the other hand. For example, manually passing all enumerable
properties of an instance of DrawerApi
, to a React component named Drawer
is as simple as:
;;; const drawer = ; { return <Drawer ...drawer>; // equavalent to // return <Drawer open={drawer.open} /> }
I've found that many React components accept event handlers for things like
some button being pressed. Assume our React Drawer
component accepts a
listener for when the darkened background is clicked in order to close the
drawer, and that it has a property onCancel
that can be used to set that
listener. Knowing this, we can change our DrawerApi to reflect this:
{ superstate; Object; // Add a property `onCancel`, which is a version of `closeDrawer`, auto-bound to `this` Object; this; this; } { this; } { this; }
Now, this line of code in the React App
component will pass both the open
and onCancel
properties to the React Drawer
component:
<Drawer ...drawer>;
I think you can that in this manner we can easily create Api objects that are loosely coupled to their counterpart React components, but still integrate with them very nicely.
NOTE: In versions of redux-apis prior to v1.1.0, connector
used to return an object
containing the api state plus a property api
, referencing the Api object itself. From 1.1.0
going forward, the enumerable properties of the Api object are being added to this. This
will continue to be like this in the 1.x branch, but when version 2.x arrives, the old behavior
will be removed and only the enumerable properties will remain. So consider the old behavior to
be deprecated.
Scoped isomorphic fetch with redux-fetch-api
Often, we want to fetch data from a remote server. Our api should be mapped to some remote endpoint. We also see that remote endpoints tend to be hierarchically structured:
http://example.com/api
http://example.com/api/products
http://example.com/api/products/details
http://example.com/api/products/create
http://example.com/api/products/update
http://example.com/api/people
http://example.com/api/people/search
etc
redux-fetch-api
is a small library designed to work well with redux-apis,
that allows you to decorate your apis with a scoped, isomorphic fetch
method.
Using the tools from redux-fetch-api
, we can map our api hierarchy onto a
remote endpoint very easily:
@remote { return this; } @ { superstate; thismoduleA = ; thismoduleB = ; } const app = ;appmoduleA; // fetches 'http://example.com/modA/something'appmoduleB; // fetches 'http://example.com/modB/something'
Check out redux-fetch-api for more details.
Async actions with redux-async-api
redux-async-api is a good fit for when you need to perform async actions, such as remote server calls (also keep an eye on redux-fetch-api for isomorphic fetch). In the spirit with the rest of this library and redux, we build on top of the basic building blocks for async with redux: thunk and Promise.
Using onload
and load
from redux-load-api
(see next chapter) we can attach loader functions
to (React) components and wait for the promises they return to fulfill. All we need then, is
some mechanism to keep track of the state of the async operation in the redux store.
Enter Async
:
static INITIAL_STATE = ...AsyncINITIAL_STATE result:'pending...' ; { superstate; this; Object; } { return this; } { this; this; return { // do async work, here simulated with `setTimeout` ; }; }
As you can see we only need to implement our business-specific logic and can
just call Async
's setters to update the async state flag.
For more information refer to redux-async-api.
Server-side rendering with redux-load-api
The community package redux-load-api offers two
functions designed to simplify server-side rendering of React components: @onload
and load
.
These allow us to do this:
{}const app = someState:'some state'; // decorate the `App` component with an onload// function that calls `app.someFunc`@@Component { // ... }
Later, we can fire all load actions and wait for them to complete before we render the page, assuring we sent a fully hydrated page to the client. Assuming we are using react-router, it might look something like this:
;
Naturally, @onload
and load
mesh well with redux-async-api
(previous chapter).
Refer to the documentation of redux-load-api for more details.
Use redux-apis with Hot Module Replacement
The Redux store holds all the state of the application, so we don't want to
destroy it, as this is essentially the same as completely reloading our
(single page) application. But we want to be able to replace the Api bound
to the store, because that will contain our work-in-progress application code.
Fortunately for us, Redux stores have a replaceReducer
method that will let
us replace the root reducer with a different one, without having to recreate
the store. This means we can make a const
store object and publish it to the
world, as it won't change during the lifetime of the application.
Using this information, here is how we can do Hot Module Replacement with redux-apis
:
;; // use require i.s.o import// don't use const as these components will be replacedlet AppApi = default;let app = ; // store object can be const. We can share it with the world, export it etcconst store = ;;; if modulehot modulehotaccept'./AppApi' { // re-create and re-link app object AppApi = default; app = ; store; ; };
Easy isn't it? Our store's state survives! The app will continue exactly where it left off, but with our new code loaded into it.
Examples
Check out the examples
folder for some examples including tests. Invoke
npm run examples
to run them.
Start hacking
I invite you to hack on the examples a bit. It's probably the easiest way to get
started. Clone / fork this repo, then cd
into the project root and invoke
npm install
Then, start a webpack development server by invoking
npm run examples-dev > redux-apis@1.1.0 examples-dev c:\ws\redux-apis> webpack-dev-server --context examples --output-filename redux-apis.examples.js "mocha!./index.jsx" --content-base examples --port 8889 http://localhost:8889/webpack-dev-server/webpack result is served from /content is served from c:\ws\redux-apis\examplesHash: 3dfff65b899455e3b0c9Version: webpack 1.12.12Time: 2531ms Asset Size Chunks Chunk Names redux-apis.examples.js 579 kB 0 [emitted] mainredux-apis.examples.js.map 700 kB 0 [emitted] mainwebpack: bundle is now VALID.
(Don't worry about those file sizes, that is due to the debug
/ hot reloading code. redux-apis.min.js
weighs in at just ~2kB
minified and gzipped)
Point your browser to http://localhost:8889/webpack-dev-server/ and you should see the mocha test suite, with all tests passing.
Now open the source files in the examples
folder in your favourite
text editor. Make some changes and save. You should see the page hot-reload.
Start hacking away!
Share your api
Built something great? Share it with the community and get it listed on this repo.
- Name your package
redux-PACKAGENAME-api
- Add the keyword
redux-apis
to thekeywords
section inpackage.json
- Publish your package to NPM
- Create a new issue here saying 'add package XYZ' or something like that
- Fork this repo and clone it to your machine
- Create and switch to a new branch
- Update this README, adding your package to the list
- Commit your changes, mentioning the full URL to the issue you created before in the commit comment
- Push to GitHub
- Create a Pull Request, mentioning the issue in the PR comment
- I will review your PR and merge it in
Documentation
Please have a look at the API documentation. Work in progress. Pull requests welcome!
Feedback, suggestions, questions, bugs
Please visit the issue tracker for any of the above. Don't be afraid about being off-topic. Constructive feedback most appreciated!
Credits
My thanks goes out to these people. They are the giants on whose shoulders this library stands.
- Dan Abramov for inventing the fantastic redux state container and his great work on hot-reloading.
- Andrew Clarke for redux-actions and flux-standard-action, defining a convention for action objects that this library follows.
- Athan Clark for asking very good questions and contributing valueable feedback, over Discord as well as the issue tracker. Thanks Athan!
- Tim Dorr, Ryan Florence, Michael Jackson and Jimmy Jia for their awesome work on react-router.
- Tobias Koppers for the wonderful and versatile webpack, and for his patience answering n00b webpack questions.
- David Lents for his constructive criticism that lead me to rethink the API and resulted in a much cleaner, better thought out approach.
- Erik Rasmussen for react-redux-universal-hot-example, which provided great insight into setting up a universal app with react and redux.
- Paul O’Shannessy and countless other individuals giving us react.
- Mateusz Zatorski for helping me on Discord.
- Yaroslav Sergievsky, for giving me feedback and contributing the TwoStores example.
Copyright
© 2016, Stijn de Witt. Some rights reserved.