@deppi/state
redux but without redux and with RxJS
Usage
Note rxjs
is a peer dependency, meaning you will need to include it yourself in your project in order for this package to work properly.
import {
/**
* createStore is the main interface for @deppi/state
* everything else are just helpers and might/will be
* moved to other packages
*
* createStore creates a new store instance. A Store
* conforms to the interface
* {
* // updates$ is a stream of events
* // that have been `dispatched`
* updates$: Observable<Action>;
* // state$ is a stream of new state
* // values returned from the reducer
* state$: Observable<State>;
* // dispatch is the _only_ way to update
* // the store at all
* dispatch: (a: Action) => void;
* // getState returns the current state
* // at time of calling
* getState: () => State;
* }
*/
createStore,
/**
* logger is an extra that will be moved
*
* It is a MiddlewareFactory function that
* logs every action and state
*/
logger,
/**
* epics is a function that, given a list of Epics,
* returns a MiddlewareFactory function
*
* Example:
*
* epic$ = (store) => store.updates$.pipe(
* ofType('SOME_TYPE'),
* mapTo({
* type: 'SOME_OTHER_TYPE'
* })
* )
*
* middleware: [epics([epic])]
*/
epics,
/**
* Helper function for dealing with updates$
* useful when making epics
*
* Example:
*
* updates$.pipe(
* ofType('SOME_TYPE', /OTHER_TYPE/)
* )
*
* is equivalent to
*
* updates$.pipe(
* filter(({ type }) => type === 'SOME_TYPE' || /OTHER_TYPE/.test(type))
* )
*/
ofType,
/**
* A way for us to connect to the store and observer changes
* based on a Properties object
*/
connect
} from '@deppi/store'
/**
* We create a single reducer for our
* whole state object. This is responsible
* for taking a state and action and returning
* the next state
*/
const reducer = (state, action) => {
if (action.type === 'UPDATE_USER') {
return ({ ...state, user: ...action.paylaod })
}
return state
}
export const store = createStore({
/**
* Each store needs a reducer
*/
reducer,
/**
* We can choose to give it a default state
* or we can choose to let it set it as
* {} if not given
*/
initialState: {
user: {
firstName: 'Tim',
lastName: 'Roberts',
location: {
city: 'Cookeville',
state: 'TN'
}
}
},
/**
* We can also add middleware. middleware is
* an array of MiddlewareFactory functions.
*
* Example:
*
* middlewareF = store => Observable<void>
*
* {
* middleware: [middlewareF]
* }
*/
middleware: [epics([
({ updates$ }) => updates$.pipe(
ofType('Some_Type', /and_reg_ex/),
map(action => ({
type: 'New action'
}))
)
]), logger]
})
const subscription = connect({
store,
properties: {
name: (state, store, props) =>
Promise.resolve(`${state.user.firstName} ${state.user.lastName}`)
}
}).subscribe(
updatedValues => {
// these are the updated values mapped from properties
// ex: { name: `John Smith` }
}
)
/**
* At some point in the future,
* we emit an event into our system
*/
setTimeout(() =>
store.dispatch({
type: 'UPDATE_USER',
payload: {
firstName: 'John',
lastName: 'Smith'
}
})
)
Using with React
Inside of extras
, there is a file called connectHoC
. It is a general structure for how to create a Higher-Order Component to connect to deppi
. Using that Component looks like this:
// store.js
import { connect as connectProperties, ... } from '@deppi/store'
/*
we create our store
*/
const store = createStore(...)
const connectReact = store => (properties, defaultState = {}) => Comp =>
class Wrapper extends Component {
constructor(props) {
super(props)
this.state = defaultState
}
componentDidMount() {
this.subscriber = connectProperties({
store,
properties,
context: this.props,
}).subscribe(newState => this.setState(oldState => Object.assign({}, oldState, newState)))
}
componentWillUnmount() {
this.subscriber && this.subscriber.unsubscribe()
}
render() {
return (
<Comp { ...this.state} { ...this.props} />
)
}
}
export const connect = connectReact(store)
// component.js
import React from 'react'
// import our custom connect function
// defined inside of `store.js`
import { connect } from './store'
import { of } from 'rxjs'
const User = ({ name, city, state }) => (
<div>
<h2>Hello, {name}</h2>
<p>You live in {city}, {state}</p>
</div>
)
// and we connect our component,
// passing in our properties config
export default connect({
// each key will be given as a prop
// with the resolved value
name: (state, props, { updates$, state$, dispatch, getState }) =>
// you can return a Promise
Promise.resolve(`${state.user.firstName} ${state.user.lastName}`),
state: state =>
// an observable,
of(state.user.location.state),
city: state =>
// or a regular value
`${state.user.location.city}`,
},{
/**
* You can also give it default props,
* and the system will start with these as
* the given props while resolving the
* properties
*/
name: 'John Smith',
city: 'Other',
state: 'State'
})(User)
Types
Name | Type | Description |
---|---|---|
Reducer | (State, Action) -> State |
A function that returns a new state given a pair of state, action |
InitialState | any |
The initial starting state of the Deppi Store |
Middleware | Store -> Observable<void> |
A function that returns an observable but the return value is never used |
StoreConfig | { reducer: Reducer, initialState?: InitialState, middleware?: [Middleware]} |
The configuration object for the Deppi Store |
Store | { updates$: Observable<Action>, state$: Observable<State>, dispatch: Action -> void, getState: () -> State } |
The Store object |
Epic | Store -> Observable<Action> |
Interface for create new actions due to current ones |
Transformers | `(State, Context, Store) -> Observable | Promise |
Properties | { [string]: Transformer } |
Config for mapping properties to values |
ConnectConfig | { store: Store, properties: Properties, context: Any } |
Our configuration for the connection |
API
Name | Type | Description |
---|---|---|
createStore | StoreConfig -> Store |
Returns a Deppi Store |
connect | ConnectConfig -> Observable<Properties> |
Returns an Observable |
epics | [Epic] -> Middleware |
A way to create a Middleware Factory given a list of epics |
logger | Middleware |
A generic logging middleware |