shadowflare v0.1.1
Shadowflare is a minimalistic state management framework for web apps inspired by hyperapp and the Elm architecture.
The architecture consists of a view that dispatches actions that update the state which is rendered into a new view. Actions are supposed to be pure and synchronous, but they can return a list of effects (for example, executing an HTTP request or setting a timer), and effects can dispatch actions to hook back into the application.
Example
We will create a page with a button to effect calls to the icanhazdadjoke API. Every time the button is pressed, the API will be invoked and the resulting joke will be displayed to the user.
To use the framework, first we need to provide a patch
function, since it doesn't handle the VDOM. In this example we'll use superfine, but snabbdom or any other library that exposes a similar patch
function should work as well.
import { h, patch } from 'superfine'
import shadowflare from 'shadowflare-core'
const start = shadowflare(patch)
start
is a function that receives an object with three properties:
init
: a function returning the initial state and optionally a list of effects to be executed.view
: a function that receives the state and returns a v-dom representation.node
: the HTML element where the application will be mounted.
The init
function
Let's start by implementing our init
function.
const init = () => {
const state = {
joke: 'Press the button to fetch a joke...',
}
const effects = []
return [ state, effects ]
}
The view
function
The view
function receives the current state and a function dispatch
used to trigger actions.
const view = (state, dispatch) =>
h('div', {}, [
h('div', {}, state.error != null
? state.error
: state.joke
),
h('button', {}, 'Get joke!'),
])
Now that we have the init
and view
functions implemented, we han pass them to the start
function.
start({
init,
view,
node: document.querySelector('#app'),
})
Implementing actions and effects
Now the application will render, but it is not loading jokes yet. Let's fix that.
First, we will implement an action called fetchJoke
that we can trigger when the button is clicked.
const fetchJoke = ({ error, ...state }) => {
const _state = {
...state,
joke: 'Fetching joke...',
}
const effects = []
return [ _state, effects ]
}
const view = (state, dispatch) =>
h('div', {}, [
...
h('button', {
onclick: () => dispatch([ fetchJoke ]),
}, 'Get joke!'),
])
An action is a function that receives the current state plus any optional arguments it has been dispatched with and returns a new copy of the state and optionally a list of effects to execute.
Now the text is changing, but we are still not feching the joke. We need to add an effect for that and return it from the action. Let's get down to it.
const fetchJson = ([ url, { method, data }, onSuccess, onError ], dispatch) => {
const options = {
credentials: 'same-origin',
mode: 'cors',
method,
headers: {
Accept: 'application/json',
},
body: data != null
? JSON.stringify(data)
: undefined,
}
return fetch(url, options)
.then(res => res.ok
? res.json()
: Promise.reject(res.status)
)
.then(data => dispatch([ onSuccess, data ]))
.catch(error => dispatch([ onError, error ]))
}
const fetchJoke = ({ error, ...state }) => {
const _state = {
...state,
joke: 'Fetching joke...',
}
const effects = [
[ fetchJson, 'https://icanhazdadjoke.com/', { method: 'GET' }, setJoke, setError ],
]
return [ _state, effects ]
}
An effect is a function that receives a list of arguments and the same function dispatch
that the view receives, which can be used to dispatch new actions once the effect has been executed.
Now we need to implement two more actions that are dispatched from our effect: setJoke
and setError
.
const setJoke = ({ error, ...state }, { joke }) => {
const _state = {
...state,
joke,
}
return [ _state ]
}
const setError = (state, error) => {
const _state = {
...state,
error,
}
return [ _state ]
}
Note that if the action doesn't generate any effects, we don't need to add them to the returned array.
You can check the full example in the examples folder.