frint-store
Frint ships with a frint-store
package, which enables you to create reducer based stores, and subscribe to state changes using an Observable.
The concepts are all borrowed from Redux. And frint-store
happens to be a more minimal implementation of that supporting observables, for nicely integrating with the rest of the framework.
You can find more documentation about the frint-store
package here.
Store is the main object that holds everything related to state for you. You can dispatch something to it for making changes, while subscribing to state too.
State happens to be the actual object that represents the current state of your Store, and it can keep streaming as changes are made to it.
Let's say we want to build an App, that has a plus and minus button for increasing and decreasing a counter value.
First install the packages:
$ npm install --save frint frint-react frint-store
Define constants for action types, that we will reuse in Actions and Reducers:
// constants/index.js
export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
// actions/counter.js
function incrementCounter() {
return { type: INCREMENT_COUNTER };
}
function decrementCounter() {
return { type: DECREMENT_COUNTER };
}
Our reducer for counter:
// reducers/counter.js
import {
INCREMENT_COUNTER,
DECREMENT_COUNTER
} from '../constants';
const INITIAL_STATE = {
value: 0
};
export default function counter(state = INITIAL_STATE, action) {
switch (action.type) {
case INCREMENT_COUNTER:
return Object.assign({}, {
value: state.value + 1
});
case DECREMENT_COUNTER:
return Object.assign({}, {
value: state.value - 1
});
default:
return state;
}
}
As your state grows over time, you will end up having lots of code in your reducer. That's why it is recommended that you split your responsibilities across multiple reducers, and then combine them all into a single root reducer that can be passed to your Store:
// reducers/index.js
import { combineReducers } from 'frint-store';
import counterReducer from './counter';
export default combineReducers({
counter: counterReducer
});
This is where we would define our Store and pass it to our App as a provider:
// app/index.js
import { createApp } from 'frint';
import { createStore } from 'frint-store';
import rootReducer from '../reducer';
export default createApp({
name: 'MyApp',
providers: [
{
name: 'store',
useFactory: function () {
const Store = createStore({
reducer: rootReducer
});
return new Store();
}
}
]
});
What if you would like to support async actions in your App?
We can achieve that by changing our provider a bit:
export default createApp({
name: 'MyApp',
providers: [
{
name: 'store',
useFactory: function ({ app }) { // the `app` instance via `deps`
const Store = createStore({
reducer: rootReducer,
deps: { app }
});
return new Store();
},
deps: ['app'] // gives you the `app` instance in `useFactory`
}
]
});
Doing this now allows our Action Creators to asynchronously dispatch further actions:
// actions/counter.js
export function incrementCounterAsync() {
// instead of returning an object, we return a function
return function (dispatch, getState, { app }) {
// `dispatch(actionPayload)` can dispatch another action
// `getState()` returns the current state object
// `app` is available because of `deps`
setTimeout(function () {
dispatch(incrementCounter()); // increment after 2 seconds
}, 2000);
}
}
Now that we have the store ready, it's time to write our Component:
// components/Root.js
import React, { Component } from 'react';
import { of } from 'rxjs/observable/of';
import { map } from 'rxjs/operators/map';
import { merge } from 'rxjs/operators/merge';
import { scan } from 'rxjs/operators/scan';
import { observe } from 'frint-react';
// our action creators
import { incrementCounter, decrementCounter } from '../actions/counter';
// React component
class Root extends Component {
render() {
return (
<div>
<p>Counter value: {this.props.counter}</p>
<button onClick={() => this.props.increment()}>+</button>
<button onClick={() => this.props.decrement()}>-</button>
</div>
);
}
}
// make our Component reactive
export default observe(function (app) {
const store = app.get('store'); // the Store instance
const state$ = store.getState$(); // state as an observable
// Observable that maps state to props
const stateProps$ = state$
.pipe(
map(function (state) {
return {
counter: state.counter.value
};
})
);
// Observable for our dispatchable action creators as props
const actionProps$ = of({
increment: (...args) => store.dispatch(incrementCounter(...args)),
decrement: (...args) => store.dispatch(decrementCounter(...args)),
});
// merge the two Observables into one, and return
return stateProps$
.pipe(
merge(actionProps$),
scan((props, emitted) => {
return {
...props,
...emitted,
};
})
);
})(Root);
The last thing we need to do is to assign this component to our App as a provider:
// app/index.js
import { createApp } from 'frint';
import { createStore } from 'frint-store';
export default createApp({
name: 'MyApp',
providers: [
{
name: 'component',
useValue: Root
},
{
name: 'store',
// useFactory: ...
// deps: ...
}
]
});
What we did in the example above for Component, was to create a single Observable that keeps emitting props, and then passed to the Root component when rendered.
Your components can become complex over time, and may need to handle multiple observables together.
We ship a handy streamProps
function in frint-react
package, to make your code shorter. The same component above can be written this way:
import React, { Component } from 'react';
import { observe, streamProps } from 'frint-react';
import { incrementCounter, decrementCounter } from '../actions/counter';
class Root extends Component {
render() {
// ...
}
}
export default observe(function (app) {
return streamProps()
// state
.set(
app.get('store').getState$(),
state => ({ counter: state.counter.value })
)
// dispatchable actions
.setDispatch({
incrementCounter,
decrementCounter
}, app.get('store'))
// generate and return final observable
.get$();
});
You can read more about streamProps
in the API documentation here.
Now just render your App, and you can see it live in your browser:
// index.js
import { render } from 'frint-react';
import App from './app';
window.app = new App();
render(window.app, document.getElementById('root'));