Simple redux bindings for marko, inspired by react-redux. You check out some example projects here and here.
npm i marko-redux
Because I like marko
and I like redux
. redux
is view layer agnostic,
well supported, easy to learn, and easy to test. react-redux
makes
using redux
easy, it removes the boilerplate and pain of having to pass down your store
to all components to that care about it. This module aims to do the same for marko
.
Depending on the scope of your application, you might not even need marko-redux
.
Apps with relatively simple state will do fine using only a root component's local state.
The SAV Architecture
described
here
will also manage state just fine for small apps that require a little more structure.
This blog post
should help explain things.
This module exposes a <provider>
component and a
connect
function for integrating with redux
.
The <provider>
component is a wrapper component that should used at
the root of an application. It is used to pass down the input redux store
to all components defined using the connect
function.
A store
can be passed to it via the store
attribute.
Example Usage:
provider store=input.store
// other components go here
Much like how redux
recommends only having a single
store
in your application, you should only have a single <provider>
(beware: using more than one can end up causing unexpected behavior).
connect
is a higher order function for binding your store
to your container components.
A store
can be passed down to connected components via the provider
component
(as explained above).
The function accepts a single argument, an object containing
information about how you want the store
to interact with the container.
Here are the available fields you can specify for connectOptions
:
mapStateToInput(state)
- a function expecting the store'sstate
to be passed in as the first argument. An object mapping how thestate
should be mapped to the component's input should be returned.mapDispatchToInput(dispatch)
- a function expecting the store'sdispatch
function to be passed in. An object mapping how action dispatch functions should be mapped to the component's input should be returned.mapDispatchToComponent(dispatch)
- an alternative to the abovemapDispatchToInput
function. This function instead exposes action dispatch functons to the component itself.options
- an additional configuration object that can contain the following fieldsstoreId
- the id of the store to use (registered via theregisterStore
function defined above. If not specified, the defaultstoreId
will be used.
All of the above fields are optional, however it is recommended that mapInputToState
and either mapDispatchToInput
or mapDispatchToComponent
are specified. This allows
for redux
to be used in full effect.
connect
will return a function that can be used for binding the configuration
to a component's definition.
That function accepts the component definition as the only argument.
It is recommended that connected
components
Example usage:
const { connect } = require('marko-redux')
// pull in actions
const {
increment,
decrement,
setCount
} = require('../../actions/counterActions')
// define marko component
class CounterContainer {
onCreate (input) {
// initialize container's state
this.state = {
count: input.count // derived from store's state
}
}
// map input to container state
// NOTE: this is needed to ensure that
// the template defined by this container
// is updated based on changes to the redux store
onInput (input) {
this.state.count = input.count // derived from store state
}
}
// maps redux store state to the component's input
function mapStateToInput (state) {
return {
// component.input.count will equal redux.getState().count
count: state.count
}
}
// maps actions dispatch functions to
// methods that will be defined on the component
function mapDispatchToComponent (dispatch) {
return {
increment: () => dispatch(increment()),
decrement: () => dispatch(decrement())
}
}
// connects the marko-redux configuration to
// the component definition. The result is exported
// to be used by marko
module.exports = connect({
mapStateToInput,
mapDispatchToComponent
})(CounterContainer)
The container's template:
label -- count: ${state.count}
// clicking these buttons will call
// the dispatch functions exposed by
// `mapDispatchToComponent`
button onClick('increment') -- increment
button onClick('decrement') -- decrement
The root component using the container:
// provider will pass down store to counter-container
provider store=input.store
counter-container
The <provider>
attaches the input store
to the async out
object that is used
by marko
to render components for the first time. Components defined with
the connect
function will pull the store from out
.
connect
wraps the component's onCreate
, onInput
, and onDestroy
methods,
to allow for the redux store
to be bound to the component (via the <provider>
component).
If mapDispatchToComponent
is provided, then the functions exposed by it will be
mapped to the component's definition.
When the component is created, it is automatically registered to the store via
store.subscribe
. As changes happen, either from new input being passed to the component
or events coming from the store, the component's onInput
will be invoked.
The mapStateToInput
and mapDispatchToInput
functions will then be applied to the input.
Those values that are mapped to the input can be passed down to child components like normal.
When a dispatch function sends out an action and the store's state changes, the component will react to the change.
When the component is destroyed, it is subsequently unsubscribed from the redux
store.