@orodio/central
v0.0.2
Published
orodio does central
Downloads
1
Readme
@orodio/central
Install
yarn add @orodio/central
General terms
intent future tense
A request from the user to do something or know about something.
- Only ever called from the view or from another intent.
- The primary way to interact with the api.
- The only thing that ever creates/dispatches an event.
event past tense
Something that did happen.
- Will posibly trigger a change in the applications state.
- Can only be created by the dispatch function.
handler current tense
Takes an event and the current state and gives us the new state.
- Can only be triggered by an event.
- Must return the new state.
Usage
createStore
is passed an initialState
and returns the following functions.
const initialState({
counters:new Map({
"counter_1": new $Counter({
id: "counter_1",
title: "foo",
count: 0,
})
})
})
const { dispatch, getState, connect, handler, intent, $ } = createStore(initialState)
initialState
must be animmutable Map
from ImmutableJS.
dispatch dispatches events into the store
dispatch('counter_set', counterId, counter)
- Should only ever be called from inside of an
intent
(never directly from the view)
handler registers how an event transforms the current state
handler('counter_set', (state, counterId, counter) => {
return state.setIn(['counters', counterId], counter)
})
- Must return the next state
- creates a default
intent
with the same event name. ie:counter_set
intent registers how an event is created
intent('counter_set', (counter) => {
return dispatch('counter_set', counter.id, counter)
})
intent('counter_get', ({ id }) => {
return getCounterFromApi({ id })
.then(normalizeCounter)
.then($('counter_set'))
})
- All async needs to happen in an intent
- the only place you should ever call dispatch is in an intent
intent and $ how you access the intents you registerd
const counter2 = new $Counter({
id: 'counter_2',
title: 'bar',
count: 10,
})
$('counter_set')(newCounter)
const counter3 = new $Counter({
id: 'counter_3',
title: 'baz',
count: 15,
})
const counterSet = $('counter_set')
counterSet(counter3)
$
/intents
andconnect
should be the only parts of a store you call in your view.
getState gets the current state
getState() // Map({ counters:Map({ "counter_1":$Counter({ id:..., title:..., count:... }) }) })
getState(['counters', 'counter_1', 'title']) // "foo"
getState(['counters', 'counter_1', 'doesnt_exist'], 'default value') // "default_value"
- Returns the current state (
Immutable Map
) - If if passed arguments acts like
.getIn([cursor], defaultValue)
connect connects a react component to the store
const inc = $('counter_inc')
const Counter = ({ id, title, count }) =>
<div>
<strong>{ title }:</strong> { count }
<button onClick={() => inc(id)}>+</button>
</div>
const mapStateToProps = (getState, props) => {
const { id } = props
return {
title: getState(['counters', id, 'title'], ""),
count: getState(['counters', id, 'count'], 0),
}
}
export default connect(mapStateToProps)(Counter)
getState
inmapStateToProps
is an actualgetState
function like the one returned fromcreateStore
- You want to do as little computation inside of
mapStateToProps
as possible
Something to keep in mind
dispatch
, intents
and the callback passed into handler
are all variadic, for example:
- Given a
dispatch
call like:dispatch("event_name", a, b, c, d, e, f)
- It will be handled by the
handler
:handler("event_name", (currentState, a, b, c, d, e, f) => nextState)
- Which creates a default intent that is equivalent to:
intent("event_name", (a, b, c, d, e, f) => dispatch('event_name', a, b, c, d, e, f))
- Which can then be called with:
$('event_name')(a, b, c, d, e, f)
In the future the following will be equivalent (using the above as an example)
This is not the case right now.
$('event_name')(a, b, c, d, e, f)
$`event_name`(a, b, c, d, e, f)
$.event_name(a, b, c, d, e, f)
Example
import createStore from '@orodio/central'
import { Map, Record } from 'immutable'
// Implementation detail
const $Counter = new Record({
id: null,
title: "",
count: 0,
})
// our stores initial state
const initialState = new Map({
counters: new Map({
"a": new $Counter({ id:"a", title:"foo", count:"10" }),
"b": new $Counter({ id:"b", title:"bar", count:"15" }),
}),
})
// the creation of our store
const {
dispatch,
getState,
connect,
handler,
intent,
$,
} = createStore(initialState)
// register how an "inc_by" event can transform the state
handler('inc_by', (state, id, delta = 1) =>
state.updateIn(['counters', id, 'count'], count => count + delta))
// register our intents
// our above handler already created an `inc_by` intent under the hood
// but if it didnt the following would do the same thing.
// We could also overload the 'inc_by' intent here if we wanted to, as
// the 'inc_by' handler is only actually called if we dispatch an 'inc_by' event.
// we default the intent to do this as a convenience but its important to
// remember that there isnt always a one-to-one mapping of intents and handlers
// later we can access this intent with `$('inc_by')`
// or call it with `$('inc_by')("a", 10)` which would 'increment counter "a" by 10'
intent('inc_by')
// this intent is calling another intent
// it can be accessed by `$('inc')`
// and be called with `$('inc')("a")` which would 'increment counter "a" by 1'
intent('inc', (id) => $('inc_by')(id, 1))
// this is an intent dispatching a totally different event
// it could just as easily have been done the same way as our 'inc' intent above
// it can be accessed by `$('dec')`
// and be called with `$('dec')("a")` which would 'increment counter "a" by -1'
intent('dec', (id) => dispatch('inc', id, -1))
// if we had async stuff (like hitting an api) we would do it in an intent.
handler('counters_set', (state, counters=new Map()) => {
return state.setIn(['counters'], counters)
})
intent('counters_get', () => {
return getCountersFromApi() // conveniently returns the data structure our store uses :P
.then(counters => dispatch('counters_set', counters))
})
handler('counter_set', (state, id, counter) => {
return state.setIn(['counters', id], counter)
})
intent('counter_get', ({ id }) => {
return getCounterFromApi({ id })
.then(counter => new $Counter(counter))
.then(counter => dispatch('counter_set', counter.id, counter))
})
// we will leverage the fact that our `counters_get` and `counter_get` intents return
// a promise later in our components to show a loading state
//
// In our Components we could do some things like this
//
const inc = id => () =>
$('inc')(id)
const dec = id => () =>
$('dec')(id)
// what does it look like?
export const Counter = ({
id = null,
title = '',
count = 0,
total = 0,
loading = false,
dec = dec, // lets us mock dec for testing
inc = inc, // lets us mock inc for testing
}) => {
if (loading) return <div>Loading...</div>
return <div>
<strong>{ title }:</strong>
<span>{ count }/{ total }</span>
<button onClick={dec(id)}>-</button>
<button onClick={inc(id)}>+</button>
</div>
}
// what is the current state of our component?
// do we need any additional data from the server?
export class XhrCounter extends React.Component {
state = {
loading: true
}
componentDidMount () {
this.mount = true
$('counter_get')(this.props)
.then(() => this.mount && this.setState({ loading:false }))
}
componentWillUnmount () {
this.mount = false
}
render () {
const { loading } = this.state
return <Counter {...this.props}/>
}
}
export const mapStateToProps = (state, { id }) => ({
title: state(['counters', id, 'title'], ''),
count: state(['counters', id, 'count'], 0),
total: state()
.getIn(['counters'], new Map())
.reduce((total, counter) => total + counter.count, 0),
})
export const mapPropsToKey = props =>
props.id
// connect returns a component that can be used as a covariant functor in a map function.
// how do we get the data from our store?
export const ConnectedCounter =
connect(mapStateToProps)(XhrCounter, mapPropsToKey) // mapPropsToKey is completely optional
export default ConnectedCounter
//
// Another Component
//
// what does it look like?
export const Counters = ({ counters = [], loading = false }) => {
if (loading) return <div>Loading...</div>
return <div>
<h1>Counters</h1>
<div>
{ !counters.length
? "No Counters"
: counters.map(ConnectedCounter)
}
</div>
</div>
}
// what is the current state of our component?
// do we need any additional data from the server?
export class XhrCounters extends React.Component({
state = {
loading: true,
}
componentDidMount () {
this.mount = true
$('counters_get')()
.then(() => this.mount && this.setState({ loading:false }))
}
componentWillUnmount () {
this.mount = false
}
render () {
const { loading } = this.state
const { counters=[] } = this.props
return <Counters
counters={ counters }
loading={ loading }
/>
}
})
// how do we get the data from our store?
export const ConnectedCounters = connect(state => ({
counters: state(['counters'], new Map())
.map(counter => ({ id:counter.id }))
.toArray(),
}))(XhrCounters)
export default ConnectedCounters
// Render it! :tada:
ReactDOM.render(<ConnectedCounters/>, domNodeToRenderIn)