An implementation of redux with meteor for full stack reactivity and unidirectional data flow. Components are designed to work in isolation, taking state from the redux store or from the database directly, ensuring drop-in code resuability across different views.
The redux store is a singleton and stores almost all relevant data.
- Skips the middleware and gets dispatched directly to the local reducer, changing the store
- Uses thunk to send data to the server in an async call. Which might:
- Dispatch a local action to optimistically change the store
- Calls an async method (usually Meteor.call) to send data to the server
- Upon change of the server DB, any trackers listening will dispatch actions with the new data.
- The store will be changed from the db update, as the single source of truth (overwriting any local change)
!!insert image here
We need to display slides on every device during a presentation, all of which are synced based on several rules:
- svgs for the presentation are kept in the store, so slide changes are instant.
- The projector always displays the slide which the presenter is on
- The audience and view all previous slides that the presenter has visited, but not further.
To render a slide in jsx, all views simply call the <Slide />
component from modules/sub_slide
.
This is the same across the presenter, presenter remote, projector and audience views.
sub_slide
is also used to show the presenter the next slide in the deck, by passing in<Slide slideIndex={1+presenterIndex} />
- this optional prop is also used to display thumbnails of all slides by displaying an array of
<Slide slideIndex={i++} />
components.
Redux actions then are written to intelligently choose the appropriate slideIndex
.
Without an overriding index, the <Slide />
component is just:
<div dangerouslySetInnerHTML={() => ({__html: store.deck[slideIndex]})} />
Presenter or audience even call the same functions, increment()
or decrement()
from anywhere in the app, and the centralized logic in redux handles the rest.
This means that a simple presenter / audience / projector view and controller could be excatly the same and still work as intended.
render: function () {
const {increment, decrement, setIndex} = this.props
return (
<div>
<div>
<button onClick={() => increment()}> ++ </button>
<button onClick={() => decrement()}> -- </button>
</div>
<Slide />
</div>
)
}
We take DRY principles even further:
export function increment() {
return setIndex(null, 1)
}
export function decrement() {
return setIndex(null, -1)
}
both increment()
or decrement()
actually call one function, setIndex()
, which also takes in a first argument for explicitly setting the slides to a particular index.
So all logic is in one single redux action:
// action to manually set index using the first arg
export function setIndex(index, operator) {
return function(dispatch, getState){
// get the desired index if !index
if (index === null){ ... }
// check if out of bounds
if (index < 0 || index >= show.numSlides){ ... break }
if (/* current user is the presenter */){
// update DB, send info to everyone
}
// increment currentIndex locally
dispatch(setSlide(index))
}
}
finally, we recieve all updates from the DB at one point:
// track the maxIndex and presenterIndex from the server
export function trackPresenter (id) {
return Tracker.autorun(function (computation) {
let show = Shows.findOne({_id: id})
// update the store
dispatch(setPresenter(show.presenterIndex))
dispatch(setMax(show.maxIndex))
if (show.ownerId === Meteor.userId()){
// set the current slide to presenterIndex if current user is the presenter
dispatch(setSlide(show.presenterIndex))
}
})
}