baconify
Reactive state management using Bacon.js
Installation
npm install --save baconify
Then just either import the main baconify
function, the Store
class (controlling bus you need to instantiate), or both (depending what you need on each file).
import baconify, {Store} from 'baconify';
Motivation and Proposed Architecture
Just a lil bit of context first re: functional reactive programming. The most fundamental concept of Functional Reactive Programming (FRP) is the event stream. Streams are like (immutable) arrays of events: they can be mapped, filtered, merged and combined. The difference between arrays and event streams is that values (events) of the event stream occur asynchronously. Every time an event occurs, it gets propagated through the stream and finally gets consumed by the subscriber.
We have Flux (and other implementations such as Redux and MobX) to handle our app state, and in fact they do a great job abstracting our views from the business logic and keeping our data flow unidirectional. However, Reactive programming is what React was made for. So, what if we delegate the app state handling to FRP libraries like Bacon.js or RxJS instead of using Redux? Well, that actually makes a lot of sense:
- Actions happen eventually and they propagate through event streams.
- The combination of these event streams result in the app's state.
- After an event has propagated through the system, the new state is consumed by the subscriber and rendered by the root level React component.
This makes the data flow super simple:
The fundamental idea behind this approach is that every user-triggered action gets pushed to the appropriate event stream, which is then merged in to the application state stream. Events take place at different points in time, and they cause the application state to change. Finally the updated state triggers a re-render of the root component, and React's virtual DOM takes care of the rest
TL;DR: Usage Example
Have a look at this example.
Quick Start Guide
1) Define your action types
export const SHOW_SPINNER = 'SHOW_SPINNER';
I usually define all actions within a single file for convenience.
2) Create your reducers
Reducers are pure functions that derive the next application state for a particular action, based on the current state and the payload the action provides. The first parameter reducers take is always the current state for the app, whereas the rest of the arguments are whatever data your reducer needs and you pass on to them.
Reducers and action types have a 1:1 relationship. You need to name your reducers after the action type they are bound to
export default {
[SHOW_SPINNER]: (state) => {
return assign({}, state, { loading: true });
},
[HIDE_SPINNER]: (state) => {
return assign({}, state, { loading: false });
}
}
Of course reducers don't need to be inline functions, you can define them elsewhere and then bind them together in the format Baconify needs them to be, something in the lines of this chunk of code... But this is totally up to you and depends on your preferred code style.
export default {
[SHOW_SPINNER]: showSpinnerReducer,
[HIDE_SPINNER]: hideSpinnerReducer
}
3) Define your initial state
const initialState = {
loading: false
};
4) Instantiate your store
const store = new Store();
5) Initialise your application state
baconify(initialState, store, reducers, (props) => {
ReactDOM.render(<App {...props} />, document.getElementById('app'));
});
About Side Effects
Side effects allow your application to interact with the outside world, i.e.: fetching data from an API, getting/setting data from/to localStorage
/sessionStorage
, talking to a database, etc.
Unlike reducers, effects are not pure functions.
Naturally effects may (and most usually do) trigger actions to update the application state once they are done making asynchronous operations.
For instance, consider this effect called getUserDetails
that fetches a list of users from an API. Provided the Ajax request completes successfully, the effect will trigger the RECEIVE_USER_DETAILS
action which simply updates the application state with those user details. This allows for a separation of concerns between hitting an API and updating the app state.
export function getUserDetails() {
store.push(SHOW_SPINNER); // triggers an action
const ajaxCall = fetch('//api.github.com/users/fknussel')
.then((response) => response.json());
const userDetailsStream = Bacon
.fromPromise(ajaxCall)
.onValue((user) => {
store.push(GET_USER_DETAILS, user); // triggers an action
store.push(HIDE_SPINNER); // triggers an action
});
}
Development Tasks
Command | Description |
---|---|
npm install |
Fetch dependencies and build binaries for any of the modules |
npm run clean |
Remove lib directory |
npm run build |
Build lib/baconify.js file |
npm test |
Run test suite |
Complementary Readings, Inspiration and Credits
I've first used a somewhat similar architecture while at Fox Sports Australia and it made perfect sense. This was probably before or at the same time Redux and MobX became popular.
Matti Lankinen proposes the same idea on his article on Medium. I've made tweaks and enhancements to this library after some of his comments and ideas.