Introduction
To write Clean Code with React, it’s better to separate view layer (React) with Business Logic (State). The React Principle is
So it’s better to pass the state rather than having inside React Components via Hooks.
Legend Model
Let’s Create a Basic Model that calls an API to get List of Todos
function getApiStatus(input) {
return {
isPending: input === "pending",
isSuccess: input === "success",
isError: input === "error",
isInit: input === "init",
isLoading: input === "pending" || input === "init"
};
}
class TodosModel {
obs;
constructor() {
this.obs = observable({
apiStatus: "init",
data: undefined,
error: undefined
});
}
private getTodos = async () => {
try {
this.obs.apiStatus.set("pending");
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos"
);
const result = await response.json();
console.log(result);
if (!response.ok) {
this.obs.apiStatus.set("error");
// Maybe extract error data here
} else {
this.obs.set({
apiStatus: "success",
data: result,
error: undefined
});
}
} catch (e) {
this.obs.apiStatus.set("error");
}
};
actions = {
// Actions can be wrapped with debounce
// getTodos: debounce(this.getTodos, 100)
getTodos: this.getTodos
};
views = {
apiStatus: computed(() => getApiStatus(this.obs.apiStatus.get()))
};
}I have created a class TodosModel which is responsible for getting Todos, and handling errors, maybe in future this can be extended to update a Todo or delete it
The core observable contains 3 values
- The API Status
- The Todos List of Success
- Error Status when API is failed
Next, we have the getTodos method, which is responsible for fetching Todos. It will update the API Status and Error based on the Network call
Next, we have actions which contain all the actions that are triggered from an external source like
- Mounting a Component ⇒ Get todos
- Clicking an Element ⇒ update or delete todo
Finally, we have views which contain the derived state from the original obs
I just have little helpers to know the apiStatus that can be used in React Components
Root Store
Any Web App or Mobile App contains some state that needs to be accessed throughout the application’s life cycle. In order to achieve that we can create a rootStore
const rootStore = {
todosStore: new TodosModel()
};React Usage
Now Let’s use this State in React
const Todos = observer(() => {
useMount(() => {
rootStore.todosStore.actions.getTodos();
});
if (rootStore.todosStore.views.apiStatus.isLoading.get()) {
return <p>Loading...</p>;
}
if (rootStore.todosStore.views.apiStatus.isError.get()) {
return (
<>
<p>Failed to Load Todos</p>
<button onClick={rootStore.todosStore.actions.getTodos}>Retry</button>
</>
);
}
return (
<div>
<For each={rootStore.todosStore.obs.data} optimized>
{(todo) => {
return (
<div key={todo.id.get()}>
<p>
{todo.id.peek()}. {todo.title}
</p>
</div>
);
}}
</For>
</div>
);
});
The Todos component contains one Mount hook which is responsible for getting todos, and then based on the API Status, the respective island* will be triggered
There are 3 islands here loading, error and success