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

  1. The API Status
  2. The Todos List of Success
  3. 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

  1. Mounting a Component Get todos
  2. 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