Counter Tutorial
In this guide, we’ll walk through the process of creating a simple Counter app.
Source Code
You can get source code for counter app from here
git clone https://github.com/almin/almin.git
cd almin/examples/counter
npm install
npm start
# manually open
open http://localhost:8080/
The purpose of counter
- Press button and count up!
End.
:memo: Notes: Recommendation
1 UseCase = 1 file
UseCase
We start implementing the UseCase.
- Press button and count up!
Start to create IncrementalCounterUseCase
class.
"use strict";
import {UseCase} from "almin"
export default class IncrementalCounterUseCase extends UseCase {
// UseCase should implement #execute method
execute() {
// Write the UseCase code
}
}
We want to update counter app state, if the IncrementalCounterUseCase
is executed.
Simply, put counter app state to a Store.
Store
Second, We create CounterStore
class.
"use strict";
import {Store} from "almin";
export class CounterStore extends Store {
constructor() {
super();
// receive event from UseCase, then update state
}
// return state object
getState() {
return {
count: 0
}
}
}
Almin's Store
can receive the dispatched event from a UseCase.
:thought_balloon: Image:
- IncrementalCounterUseCase dispatch "increment" event.
- CounterStore receive the dispatched "increment" event and update own state.
This pattern is the same Flux architecture.
In flux:
- dispatch "increment" action via ActionCreator
- Store receive "increment" action and update own state
UseCase dispatch -> Store
Return to IncrementalCounterUseCase
and add "dispatch increment event"
"use strict";
import { UseCase } from "almin";
export class IncrementalCounterUseCase extends UseCase {
// IncrementalCounterUseCase dispatch "increment" ----> Store
// UseCase should implement #execute method
execute() {
this.dispatch({
type: "increment"
});
}
}
A class inherited UseCase
has this.dispatch(payload);
method.
payload
object must have type
property.
{
"type": "type"
}
is a minimal payload object.
Of course, you can include other property to the payload.
{
"type": "show",
"value": "value"
}
So, IncrementalCounterUseCase
dispatch "increment" payload.
UseCase -> Store received
Next, We want to add the feature that can received "increment" payload to CounterStore
.
A class inherited Store
can implement receivePayload
method.
"use strict";
import { Store } from "almin";
export class CounterStore extends Store {
constructor() {
super();
// initial state
this.state = {
count: 0
};
}
// receive event from UseCase, then update state
receivePayload(payload) {
if(payload.type === "increment"){
this.state.count++;
}
}
// return the state
getState() {
return this.state;
}
}
All that is updating CounterStore
's state!
But, We can separate the state
and CounterStore
as files.
It means that we can create CounterState
.
Store
- Observe dispatch events and update state
- Write state:
receivePayload()
- Read state:
getState()
- Write state:
State
- It is state!
State
We have created CounterState.js
.
CounterState
s main purpose
- receive "payload" and return state.
"use strict";
// reduce function
export class CounterState {
/**
* @param {Number} count
*/
constructor({ count }) {
this.count = count;
}
reduce(payload) {
switch (payload.type) {
// Increment Counter
case "increment":
return new CounterState({
count: this.count + 1
});
default:
return this;
}
}
}
You may have seen the pattern. So, It is reducer in the Redux.
Store -> State: NewState
Finally, we have added some code to CounterStore
- Receive dispatched event, then update
CounterState
CounterStore#getState
return the instance ofCounterState
A class inherited Store
has this.setState()
method that update own state if needed.
"use strict";
import { Store } from "almin";
import { CounterState } from "./CounterState";
export class CounterStore extends Store {
constructor() {
super();
// initial state
this.state = new CounterState({
count: 0
});
}
// receive event from UseCase, then update state
receivePayload(payload) {
this.setState(this.state.reduce(payload));
}
// return own state
getState() {
return this.state;
}
}
:memo: Note: Testing
We can test above classes independently.
View Integration
This example use React.
index.js
We will create index.js
is the root of the application.
First, we create Context
object that is communicator between Store and UseCase.
import {Context, Dispatcher} from "almin";
import {CounterStore} from "./store/CounterStore";
// a single dispatcher
const dispatcher = new Dispatcher();
// initialize store
const counterStore = new CounterStore();
// create store group
const storeGroup = new StoreGroup({
// stateName : store
"counter": counterStore
});
// create context
const appContext = new Context({
dispatcher,
store: storeGroup
});
Second, We will pass the appContext
to App
component and render to DOM.
ReactDOM.render(<App appContext={appContext} />, document.getElementById("js-app"))
Full code of index.js
:
Source:
counter/src/index.js
"use strict";
import React from "react";
import ReactDOM from "react-dom";
import { Context, Dispatcher, StoreGroup } from "almin";
import App from "./component/App";
import { CounterStore } from "./store/CounterStore";
// a single dispatcher
const dispatcher = new Dispatcher();
// a single store
const counterStore = new CounterStore();
// create store group
const storeGroup = new StoreGroup({
// stateName : store
counter: counterStore
});
// create context
const appContext = new Context({
dispatcher,
store: storeGroup,
options: {
// Optional: https://almin.js.org/docs/tips/strict-mode.html
strict: true
}
});
ReactDOM.render(<App appContext={appContext} />, document.getElementById("js-app"));
App.js
We will create App.js
is the root of component aka. Container component.
It receive appContext
from index.js
and use it.
Source:
counter/src/component/App.js
"use strict";
import React from "react";
import PropTypes from "prop-types";
import { Context } from "almin";
import { CounterState } from "../store/CounterState";
import { Counter } from "./Counter";
export default class App extends React.Component {
componentWillMount() {
const appContext = this.props.appContext;
// set initial state
this.setState(appContext.getState());
// update component's state with store's state when store is changed
const onChangeHandler = () => {
this.setState(appContext.getState());
};
this.unSubscribe = appContext.onChange(onChangeHandler);
}
componentWillUnmount() {
if (typeof this.unSubscribe === "function") {
this.unSubscribe();
}
}
render() {
/**
* Where is "CounterState" come from?
* It is a `key` of StoreGroup.
*
* ```
* const storeGroup = new StoreGroup({
* "counter": counterStore
* });
* ```
* @type {CounterState}
*/
const counterState = this.state.counter;
return <Counter counterState={counterState} appContext={this.props.appContext} />;
}
}
App.propTypes = {
appContext: PropTypes.instanceOf(Context).isRequired
};
App's state
Root Component has state that sync to almin's state.
Focus on onChange
:
// update component's state with store's state when store is changed
const onChangeHandler = () => {
this.setState(appContext.getState());
};
appContext.onChange(onChangeHandler);
If CounterStore
's state is changed(or emitChange()
ed), call onChangeHandler
.
onChangeHandler
do update App
component's state.
Counter component
Counter component receive counterState
and appContext
via this.props.
.
CounterComponent.propTypes = {
appContext: React.PropTypes.instanceOf(Context).isRequired,
counterState: React.PropTypes.instanceOf(CounterState).isRequired
};
Execute UseCase from View
We can execute IncrementalCounterUseCase
when Counter's Increment button is clicked.
incrementCounter() {
// execute IncrementalCounterUseCase with new count value
const context = this.props.appContext;
context.useCase(new IncrementalCounterUseCase()).execute();
}
Execute IncrementalCounterUseCase
and work following:
- Execute
IncrementalCounterUseCase
CounterStore
is updated(create newCounterState
)App
Component's state is updated viaonChangeHandler
Counter
receive newCounterState
, refresh view
Source:
counter/src/component/Counter.js
"use strict";
import React from "react";
import PropTypes from "prop-types";
import { IncrementalCounterUseCase } from "../usecase/IncrementalCounterUseCase";
import { Context } from "almin";
import { CounterState } from "../store/CounterState";
export class Counter extends React.Component {
incrementCounter() {
// execute IncrementalCounterUseCase with new count value
const context = this.props.appContext;
context.useCase(new IncrementalCounterUseCase()).execute();
}
render() {
// execute UseCase ----> Store
const counterState = this.props.counterState;
return (
<div>
<button onClick={this.incrementCounter.bind(this)}>Increment Counter</button>
<p>Count: {counterState.count}</p>
</div>
);
}
}
Counter.propTypes = {
appContext: PropTypes.instanceOf(Context).isRequired,
counterState: PropTypes.instanceOf(CounterState).isRequired
};
End
We have created simple counter app.
Writing the pattern in this guide is the same of Flux pattern.
Next: We learn domain model and CQRS pattern while creating TodoMVC app.