Awesome
<h1 align="center" style="color: #343a40"> <img src="https://cdn.rawgit.com/tkh44/smitty/master/smitty.png" alt="smitty" width="200"> <br> smitty <br> <br> </h1> <p align="center" style="font-size: 1.2rem;">Tiny flux implementation built on <a style="color: #9B86FF;" href="https://git.io/mitt">mitt</a></p>smitty
- Install
- Basic Usage
- Demos
- Usage with Preact and React
- API
- Store
- Action Creator Detailed Example
- Class As Reducer
Install
npm install -S smitty
Basic Usage
import { createStore } from 'smitty'
// Create a store with initial state
const initialState = { count: 0 }
const store = createStore(initialState)
store.createActions({
add: 'count/ADD'
})
// add a reducer
store.handleActions({
[store.actions.add]: (state, e, type) => {
// increment foos by amount
return Object.assign({}, state, { count: state.count + e.amount })
},
'*': (state, e, type) => {
// '*' can be used for all kinds of fun stuff
console.log(e, type)
if (type === 'count/ADD') {
//...do something
}
return state
}
})
store.actions.add({ amount: 5 })
console.log(store.state) // logs `{ count: 5 }`
Demos (v2)
- Photo Booth Demonstrates async api and saving parts of the store with localforage
Demos (v1)
- Basic
- Async
- Fun
Usage with Preact and React
-
Preact bindings - preact-smitty
npm install preact-smitty
-
React bindings - react-smitty
npm install react-smitty
API
createStore(initialState: any)
Arguments
initialState: any
required: Determines the shape and initial state of your store. Can be of any type that you choose.
Returns
Store: Store
Store
Store
emit: (function)
arguments
type: (string | function)
-
[string],
type
determines which reducers are called.const store = createStore(0) store.handleActions({ add: function (state, payload) { return state + payload } }) console.log(store.state) // logs 0 store.emit('add', 1) console.log(store.state) // logs 1
-
[function]
type
becomes an action creator that is passed 1 argument- store: Store
This is useful to emit multiple actions from a single emit call.
const store = createStore(0) store.handleActions({ add: function (state, payload) { return state + payload } }) function asyncAction (emit, state) { emit('add', 1) console.log(state) // logs 1 setTimeout(() => { emit('add', 1) console.log(state) // logs 3 }, 100) emit('add', 1) console.log(state) // logs 2 } ```
payload: (any) optional
payload to pass to your reducer
const store = createStore({ name: 'Arrow' })
store.handleActions({
'update/NAME': function (state, payload) {
// I really don't care if you return a new state
// Nobody is judging. Do what your ❤️ tells you.
// Just be consistent
return Object.assign({}, state, payload)
}
})
console.log(store.state) // logs { name: 'Arrow' }
store.emit('update/NAME', { name: 'River' })
console.log(store.state) // logs { name: 'River' }
createActions(): (function)
arguments
actionMap: (object)
Object where key is the action creator's name and the value can be of type string
or function
.
If the value is a string
, an action creator is attached to store.actions
as a function that accepts one argument, payload
.
store.createActions({
add: 'count/ADD'
})
// The following are functionally equivalent
store.actions.add(1)
store.emit('count/ADD', 1)
Action creators with a string value can be used as the key in your actionMap
in handleActions
.
store.createActions({
add: 'count/ADD'
})
// add a reducer
store.handleActions({
[store.actions.add]: (state, e, type) => {
// increment foos by amount
return Object.assign({}, state, { count: state.count + e.amount })
}
})
store.actions.add({ amount: 5 })
console.log(store.state) // logs `{ count: 5 }`
If the value is a function
, it must be a function that returns an action creator. For async action creators.
store.createActions({
add: (amount) => {
return (store) => {
setTimeout(() => {
store.emit('count/ADD', amount)
}, 16)
}
}
})
store.actions.add(1)
handleActions(): (function)
arguments
handlerMap: (object)
Object with keys that correspond to action types passed to emit
When an event is emitted and the key matches the type the reducer is invoked with 3 arguments.
- state: (any) the store's state getter
- payload (any) the payload that was emitted
- type (string) the type that was emitted
const store = createStore({ color: 'blue', hovered: false })
store.handleActions({
'merge': function (state, payload) {
return Object.assign({}, state, payload)
},
'overwrite': function (state, payload) {
return payload
},
// Could do the same in one
// If you really miss redux do this and put a switch statement
'*': function(state, payload, type) {
return type === 'merge' ? Object.assign({}, state, payload) : payload
}
})
console.log(store.state) // logs { color: 'blue', hovered: false }
store.emit('merge', { color: 'red' })
console.log(store.state) // { color: 'red', hovered: false }
store.emit('overwrite', { color: 'green', hovered: true, highlighted: false })
console.log(store.state) // { color: 'green', hovered: true, highlighted: false
actions: (object)
Map of all the actions created in store.createActions
This is convenient so that you do not have to deal with action imports across your app.
on: (function)
Convenience shortcut for mitt.on.
off: (function)
Convenience shortcut for mitt.off.
Action Creator Detailed Example
You can pass a function to emit
in order to create an action creator
import { createStore } from 'smitty'
// Create a store with initial state
const initialState = {}
const store = createStore(initialState)
// add our reducer
store.handleActions({
'api/GET_ROOM': (state, { id, res }) => {
return {
...state,
[id]: {
...state[id],
...res.data
}
}
}
})
// create our action creators
const actions = {
requestRoom (id) {
return async (emit, state) => {
emit('REQUEST_ROOM', { id, res: { data: { id } } })
const res = await window.fetch(`https://api.mysite.com/${id}`)
res.data = await res.json()
emit('REQUEST_ROOM', { id, res })
}
}
}
// When calling emit with a function argument, the function will be called with `emit` and `state` as arguments
const result = store.emit(actions.requestRoom('1a'))
// Return whatever you like from your action creator
console.log(result) // logs "Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}"
// After the fetch call, `REQUEST_ROOM` is fired a second time with our response data
result.then(() => console.log(store.state)) // logs `{ 1a: { id: '1a', title: 'My Room' }``
Class As Reducer
Reducers are iterated with for (let type in reducer) {...}
with no obj.hasOwnProperty
check so this works.
const store = createStore({ foo: 5 })
class HistoryReducer {
constructor (initialHistory = []) {
this.history = createStore(initialHistory)
this.history.handleActions({
update: (state, e) => {
state.push(e)
}
})
}
onUpdate (state, e, type) {
this.history.emit('update', { state, e, type })
}
}
HistoryReducer.prototype['foo/ADD'] = function (state, e, type) {
state.foo += e.foo
this.onUpdate(state, e, type)
}
const historyReducer = new HistoryReducer([])
store.handleActions(historyReducer)
store.emit('foo/ADD', { foo: 5 })
console.log(store.state.foo) // logs 10
store.emit('foo/ADD', { foo: 7 })
console.log(store.state.foo) // logs 17
console.log(historyReducer.history.state)
// logs
// [
// { state: { foo: 10 }, e: { foo: 5 }, type: 'foo/ADD' },
// { state: { foo: 17 }, e: { foo: 7 }, type: 'foo/ADD' }
// ]