Deep Dive into AppRun State

Yiyi Sun
6 min readSep 9, 2017

--

AppRun is a 3K library for developing applications using the Elm style model-view-update architecture and event pub and sub pattern. In order to demonstrate AppRun, I built the AppRun RealWorld Example App following the specifications of the RealWorld project.

The RealWord is meant to be the new TodoMVC, but is a full stack application specification, including querying & persisting data to a database, an authentication system, session management, and full CRUD for resources. It is a specification of a blogging site — a clone of medium.com, called Conduit.

By implementing RealWorld/Conduit, we can explore and demonstrate AppRun’s application architecture and compare it against other frameworks and libraries, so that AppRun goes head to head with React/Redux, React/MobX, Angular, Elm, Vue and etc.

Overall, I am satisfied with the AppRun RealWorld Example App. It has about 1000 lines source that can be gziped to 18K. React/Redux has about 2000 lines/211K. React/MobX has 1900 lines/122K, Angular has 2000 lines/570K and Elm has 4000 lines/101K.

In this post I will describe three steps I used to design the State. In the sister post, titled Deep Dive into AppRun Events, I will explain event pub sub patterns in AppRun applications. Both using the AppRun RealWorld Example App as example.

Introduction

AppRun is inspired by Elm-style model-view-update architecture that is explained in this introductory post. Essentially, there are two important concepts: State and the control structure to display and manipulate the state, such as view template and event trigger.

The State is unique in each application. The State is a blank piece of paper at beginning of any application.

state: any = {};

The control structure, on the other hand, is the same in applications with the same architecture. The control structure, just needs to learn one time; it can apply it to all applications.

However, the State is the most important in application architecture design, in my opinion.

Let’s understand where the State lives.

State Scope

AppRun is different to Elm in that AppRun application state is component scope, where Elm application state is global scope. Using AppRun, each component has a model-view-update architecture that manipulates the component state. E.g., in the RealWorld Example App, the component displays the article is called ArticleComponent:

import app, { Component } from 'apprun';class ArticleComponent extends Component{
state = {}
view = (state) => <div></div>
update = {
'#/article': async(state, slug) => {}
}
}

Dividing application functions into components is the common practice, because developing and maintaining components separately is simpler than having one giant application.

AppRun makes the state into component scope. Compare to Elm RealWorld Example App, which manages the state globally, managing component states in AppRun is easier.

AppRun component is based on routes. E.g., the RealWorld Routing specification is as following:

* Home page (URL: /#/ )
- List of tags
- List of articles pulled from either Feed, Global, or by Tag
- Pagination for list of articles
* Sign in/Sign up pages (URL: /#/login, /#/register)
- Uses JWT (store the token in localStorage)
- Authentication can be easily switched to session/cookie based
* Settings page (URL: /#/settings )* Editor page to create/edit articles (URL: /#/editor, /#/editor/article-slug-here )* Article page (URL: /#/article/article-slug-here)
- Delete article button (only shown to article’s author)
- Render markdown from server client side
- Comments section at bottom of page
- Delete comment button (only shown to comment’s author)
* Profile page (URL: /#/profile/:username, /#/profile/:username/favorites)
- Show basic user info
- List of articles populated from author’s created articles or author’s favorited articles

In the AppRun RealWorld Example app, there is a Home component, Sign In component, Settings component, Editor component, Article component and Profile component to match the routing specification. Each of them has a state. Next, you will see how to design the State.

State Design

In model-view-update architecture, the State is the snapshot of the data applications needed at any given time.

I follow the following steps to design the State.

1) Design State based on dynamic screen content

State in component is the data that is required to render the screen. The Home component has the following specification.

* Home page (URL: /#/ )
- List of tags
- List of articles pulled from either Feed, Global, or by Tag
- Pagination for list of articles

The initial state of the Home component is:

state = {
tags: [],
articles: [],
type: '', // Feed, Global or Tag
count: 0, // count of articles
page: 1 // page number
}

2) Design the State with a update function

The state can be changed by type, page number and tag. A function takes in these as parameters is needed. It also needs to takes in existing states so that some data in an older state can be reused, such as tags. The tags are loaded once and cached; no need to load tags on each request.

updateState = async (state, type: '' | 'feed' | 'tag', page, tag?: string) => {,
// get articles from API
return { ...state, articles }
}

3) Verify with the routes and user events

Although the RealWorld specification currently only has the top level route definition for the Home page as /#/, It can be easily extended.

/ - redirect to /#/
/#/ - default home page
- display global feed and page 1 for anonymous user
- display user feed and page 1 for signed in user
/#//{pageno} - Global feed with page number
/#feed/{pageno} - User feed with page number
/#tag/{pageno} - Articles by tag and page number

With the above routes designed, the app will fully support browser back and forward keys. AppRun converts the document location hash changes into events that can be handled just like the buttons on the screen.

We need to verify that the State designed can handle the routes before we wrap up the design. It looks good so far. All scenarios are covered.

State Type

Once the shape of the State is identified, I would suggest to make it typed State. This is an optional step.

declare interface IState {
type: '' | 'feed' | 'tag'
articles: Array<IArticle>
tags: Array<string>
max: number
page: number
}
state: IState = {
tags: [],
articles: [],
type: '',
count: 0,
page: 1
}

By adding type information, the other part of the code will have the benefit of type checking, auto code complete / intellisense, and easy refactoring.

view = (state: IState) => <div></div>update = {
'#/': (state: IState, page: number) => { ...state, page }
}

IState is the model, state: IState is the State in strongly typed world (TypeScript). Otherwise model and state are interchangeable in non-strongly typed world (JavaScript).

Unit Test

Because the State is the snapshot of the application data of any given time, unit tests are to verify the state. The following test case is to test the specification: /#tag/{pageno} — Articles by tag and page number.

it('should update state: #/tag/t3/20', (done) => {
app.run('route', '#/tag/t3/20');
setTimeout(() => {
const state = home.state;
expect(state.type).toBe('tag');
expect(state.tag).toBe('t3');
expect(state.page).toBe(20);
done();
})

All routes in the Home component are tested using the above style in the AppRun RealWorld Example App. It is easy to understand and write.

So far, I covered the Home component state design and unit test. Guess what would be the Article component state and the Profile component state. Check out the AppRun RealWorld repo.

Conclusion

Coding using the model-view-update architecture is state focused. Design a good state/model first, then code display logic; and code the date query and the state update logic against the state. They are done separately and decoupled from each other.

In traditional multi-layered architecture, we use the Data Transfer Object (DTO) to decouple presentation logic, business logic and persistent logic. I feel the State decouples the display logic and the update logic in frontend applications.

Please visit the RealWorld repo and AppRun RealWorld repo and AppRun to study, compare, comment, provide feedback, and contribute.

Comments and pull requests are welcome. Happy coding.

--

--