Counter Tutorial
In this guide, we’ll walk through the process of creating a simple Counter app.
Source Code
You can get the 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 a button and count up!
End.
📝 Notes: Recommendation
1 UseCase = 1 file
UseCase
We start implementing the UseCase.
- Press a 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 the counter app state when the IncrementalCounterUseCase
is executed.
Simply, put the 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.
💭 Image:
IncrementalCounterUseCase
dispatches "increment" event.CounterStore
receives the dispatched "increment" event and updates own state.
This pattern is the same Flux architecture.
In flux:
- dispatch "increment" action via ActionCreator
- Store receives "increment" action and updates 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"
}
The above is a minimal payload object.
Of course, you can include other properties to the payload.
{
"type": "show",
"value": "value"
}
So, IncrementalCounterUseCase
dispatches the "increment" payload.
UseCase -> Store received
Next, We want to add the feature that can receive the "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 the state!
State
We have created CounterState.js
.
CounterState
s main purpose is
- 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
returns 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;
}
}
📝 Note: Testing
We can test the above classes independently.
View Integration
This example uses React.
index.js
We will create index.js
that is the root of the application.
First, we create a 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"))
The following is the 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
that is the root of component aka. Container component.
It receives appContext
from index.js
and uses 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 {
constructor(props) {
super(props);
// set initial state
this.state = props.appContext.getState();
}
componentDidMount() {
const appContext = this.props.appContext;
// 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 syncs 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
does update App
component's state.
Counter component
Counter component receives 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
receives 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 a 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.