every event type gets an aggregate file. As declaratively as possible we describe what an action does.
// src/aggregates/poll_playlist_response.js
import { GET } from "../tools/http";
import { curryDispatch } from "../tools/curry_dispatch";
export var type = "POLL_PLAYLIST_RESPONSE";
export var shape = {
type,
videos: Array,
};
export var pollForPlaylist = ( collectionId, gid) => dispatch => {
GET(`${ __ROOT__ }/api/v1/collections/${ collectionId }?resource_id=${ gid }`)
.then(res => dispatch({
type,
videos: res.data.resources,
}))
};
export var actions = curryDispatch({ pollForPlaylist })
export var reducer = (state, { videos }) => ({
...state,
playlist: {
...state.playlist,
videos,
}
});
we collect these aggregates together, and validate that they export a reducer, type, and shape.
validateAggreates
will throw a descriptive error telling our juniors how to make their aggregate
conform to our aggregate standard.
// src/aggregates/__aggregates.js
import { validateAggregates } from "../tools/validate_aggregates"
import * as poll_playlist_response from "./poll_playlist_response";
export default validateAggregates([
// ... other aggregates
poll_playlist_response,
// ... other aggregates
])
The reducer we pass into Reduxs createStore
function builds a map of key function pairs, where the key is the exported type paird with the exported function from the aggregate.
In the near future we will be checking the shape in here as well against the shape defined by the aggregate, if the shape is wrong we will give a descriptive error in devmode.
The step after that will also be adding a declarative permission to the aggregate that will be validated in here as well.
We have used this pattern in some of our experimental event sourced node applications and it worked quite well.
// src/aggregates/__reducer.js
import initial_state from "./__initial_state";
import aggregates from "./__aggregates";
import { isDevMode } from "../tools/dev";
var reducers = aggregates.reduce((acc, { type, reducer }) => ({ ...acc, [type]: reducer }), {})
export default (state, action) => {
if (!state) state = initial_state;
var { type } = action;
if (!!reducers[type]) {
// validate shape will go here
// validate permission will go here
return reducers[type](state, action);
}
if (isDevMode() && type !== "@@redux/INIT") console.error(`%c-- No Matching Action Found For: ${ type } --`, "color:tomato;font-weight:bold;")
return state;
}
Our command components in react are your standard connected react-redux components, with the exception of binding the dispatch
function to actions
// components/PlaylistViewer/index.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { actions as playlistActions } from "../../aggregates/poll_playlist_response";
import PlaylistItem from "./PlaylistItem";
var stateToProps = state => ({
videos: state.playlist.videos,
})
var actionsToProps = dispatch => ({
actions: playlistActions(dispatch)
})
@connect(stateToProps, actionsToProps)
export default class PlaylistViewer extends Component {
static propTypes = { /* ... */ }
componentWillMount () {
var {
gid,
collectionId,
actions: { pollForPlaylist }
} = this.props;
pollForPlaylist(collectionId, gid)
}
render () {
var {
videos,
} = this.props;
return <div className="PlaylistViewr">
{ videos.map(PlaylistItem) }
</div>
}
}