Why?
There is a gap between software specification and implementation. If we could write the program flow with a simple DSL and even run it ... This is just an idea, but we can do it. Half joke, half serious.
UI as function
My first thought was if we could write the user interface as an async function. In fact, it’s already we've seen.
if (confirm(“Do you want to send the message?”)) {
somethingHappen()
} else {
alert(“cancelled!”)
}
Pretty simple, right? There are no any callbacks, no flux, no dispatch, no singleton state. It’s easily readable.
Re-invent the UI function
Let’s do as same way by using async/await in TypeScript.
We can define the view as async function that returns user input events.
interface View {
confirmToSend: async () => boolean
}
and then write the business logic.
const main = async (view: View) => {
if (await view.confirmToSend()) {
somethingHappen()
}
}
then implement View. At now I use React. it’s not important anyway.
class App implements View {
async confirmToSend(): boolean {
return new Promise((resolve) => {
this.setState({
isVisibleConfirm: true,
onClickConfirmYes: () => resolve(true),
onClickConfirmNo: () => resolve(false),
})
})
}
public render() {
return <div>
{this.state.isVisibleConfirm &&
<div className="modal confirm">
<p>Do you want to send the message?</p>
<button onClick={this.state.onClickConfirmYes}>Yes</button>
<button onClick={this.state.onClickConfirmNo}>No</button>
</div>}
<div>
}
}
The point is confirmToSend
returns Promise that waits for user interaction.
Run them together.
ReactDOM.render(<App ref={view => view && main(view)} />)
So, this application works according to business logic written in async / await.
Do you understand how we can write business logic in procedural way with React? We might need another example.
To-Do App
So let’s see the to-do app example.
First of all we write the business logic.
export interface Todo {
title: string
description: string
}
export interface Store {
getTodos(): Promise<Todo[]>
addTodo(todo: Todo): Promise<void>
}
export interface View {
showTodos(todos: Todo[]): Promise<["click-todo", Todo] | ["create-todo"]>
showTodo(Todo: Todo): Promise<["close"]>
showCreateForm(): Promise<["cancel"] | ["create", Todo]>
closeCreateForm(): Promise<void>
closeTodo(Todo: Todo): Promise<void>
}
export const mainLoop = async (store: Store, view: View) => {
initial: while (true) {
const todos = await store.getTodos()
const event = await view.showTodos(todos)
switch (event[0]) {
case "click-todo": {
const e = await view.showTodo(event[1])
switch (e[0]) {
case "close":
await view.closeTodo(event[1])
continue initial
}
break
}
case "create-todo": {
const e = await view.showCreateForm()
switch (e[0]) {
case "cancel":
await view.closeCreateForm()
continue initial
case "create":
await view.closeCreateForm()
await store.addTodo(e[1])
continue initial
}
break
}
}
}
}
Looks pretty! This is self-contained, complete behavior definition of this app. We can understand how this app works by just reading this function. And this is a specification for the app.
Let's see some tricks.
Store
Store fetches data from somewhere and storing. We have the view as an interface. So we should write the store as an interface to separate business logic and implementation.
Infinite Loop
Main loop! You probably used an infinite loop long ago to make a game or something ...
This To-Do app should run until the user closes the tab, so it will be written in an infinite loop.
Label and continue
When user closes modal view, app should reset to the first state.
We can use continue to go back to start of while loop.
Events
We used boolean to handle user-interaction in above example.
But in real app, we have to handle many events for one showSomewhat function.
I defined some event types for each show functions using array. We can use the object that have type and payload. But type inference also works so I don't want to type too much.
["click-todo", Todo]|["close-todo"]
maybe you like
interface CloseTodoEvent {
type: "close-todo"
}
interface ClickTodoEvent {
type: "click-todo"
payload: Todo
}
To-Do View
Now let see the View implementation.
import * as React from "react"
import { Todo } from "./main"
interface State {
todos: Todo[]
modalTodo: Todo | null
isCreateFormVisible: boolean
formTitle: string
formDescription: string
onClickTodo: (todo: Todo) => void
onClickCreateNew: () => void
onClickModal: () => void
onClickAdd: () => void
onClickCancelCreation: () => void
}
export class AppView extends React.Component<{}, {}> {
public state: State = {
todos: [],
modalTodo: null,
isCreateFormVisible: false,
formTitle: "",
formDescription: "",
onClickTodo: (todo: Todo) => {},
onClickCreateNew: () => {},
onClickModal: () => {},
onClickAdd: () => {},
onClickCancelCreation: () => {}
}
showTodos(todos: Todo[]) {
return new Promise<["click-todo", Todo] | ["create-todo"]>(resolve => {
this.setState({
todos,
modalTodo: null,
onClickTodo: (todo: Todo) => resolve(["click-todo", todo]),
onClickCreateNew: () => resolve(["create-todo"])
})
})
}
showTodo(todo: Todo) {
return new Promise<["close"]>(resolve => {
this.setState({
modalTodo: todo,
onClickModal: () => resolve(["close"])
})
})
}
closeTodo(todo: Todo): Promise<void> {
this.setState({ modalTodo: null })
return Promise.resolve()
}
showCreateForm() {
return new Promise<["cancel"] | ["create", Todo]>(resolve => {
this.setState({
formTitle: "",
formDescription: "",
isCreateFormVisible: true,
onClickCancelCreation: () => resolve(["cancel"]),
onClickAdd: () =>
resolve([
"create",
{
title: this.state.formTitle,
description: this.state.formDescription
}
])
})
})
}
closeCreateForm() {
this.setState({
isCreateFormVisible: false
})
return Promise.resolve()
}
public render() {
const {
todos,
modalTodo,
isCreateFormVisible,
formTitle,
formDescription,
onClickCreateNew,
onClickTodo,
onClickModal,
onClickCancelCreation,
onClickAdd
} = this.state
return (
<>
<ul>
{todos.map((t, i) => (
<li className="todo" onClick={() => onClickTodo(t)} key={i}>
<p className="title">{t.title}</p>
<p className="description">{t.description}</p>
</li>
))}
</ul>
<button onClick={onClickCreateNew}>Create new To-Do</button>
{modalTodo !== null && (
<div className="modal">
<p className="title">{modalTodo.title}</p>
<p className="description">{modalTodo.description}</p>
<button onClick={onClickModal}>Close</button>
</div>
)}
{isCreateFormVisible && (
<div className="modal create-todo-form">
<label>title</label>
<input
type="text"
value={formTitle}
onChange={e => this.setState({ formTitle: e.target.value })}
/>
<label>description</label>
<input
type="text"
value={formDescription}
onChange={e => this.setState({ formDescription: e.target.value })}
/>
<button onClick={onClickCancelCreation}>Cancel</button>
<button onClick={onClickAdd}>Add</button>
</div>
)}
</>
)
}
}
This is little ugly but works. We may need somewhat library to do this better.
Screenshot
It works!
Let’s say hi again to good old procedural programming!
Further more
- Can we apply this method to real-world application?
- How we handle interrupted events?
- How we handle the application that has multiple pane.
- Does this business logic make tests more efficient?
Top comments (2)
Yes, I love that!
I started thinking this way too, during last year.
Some topics really opened my mind and I am applying them to typescript :
This kind approach can be applied to UIs and to Backend Systems too !
Unfortunately, these approaches and concepts find a lot of mental resistance in most tech teams
Thank you very much! I am glad that you are interested.
I do not see such thought so much, so I would be happy if you could tell me more about your opinion.
Does this topic have a name? Do you know the articles or communities that I should look for?