DEV Community

JJ
JJ

Posted on

How to treat component as TypeSafe in Redux + Atomic Design

It rewritten what I made an article on Nihon Qiita technology sharing service to English.
The article of Japan is as follows.

俺の考えたRedux + Atomic DesignでTypeSafeにコンポーネントを扱う方法

It is poem. Let's take notice of browsing :)

First of all apologize because you may think you do not understand Atomic Design in the first place.
Just as a common recognition, I also write it in the document, so I will use this method to operate.

Directory structure

- src/
    - actions/
        - hoge.ts
    - api/
        - client.ts
    - components/
    - model/
        - type.ts
    - store/
        - index.ts
        - reducer-type.ts
        - sagas/
        - reducers/
Enter fullscreen mode Exit fullscreen mode

ActionCreator is described inactions.
The API surroundings (communication using axios) are summarized in client.ts.

The components subordinate is modeled on Atomic Design.

model/type.ts is managed by separating them with namespace Payload Response Domain.
For example, if you use API to login with email and password Payload.Signin

like

interface Signin {
  email: string;
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

This is a type to be used as Payload of Action later.

The response of that API is summarized in Response.Signin.

example

interface Signin {
  name: string;
  icon: string;
  token: string;
}
Enter fullscreen mode Exit fullscreen mode

store in addition to index.ts doing combineReducer etc, there is reducers which defines Reducer, sagas which writes saga, and so on.

It is reducer-type.ts that helps the shape that I created in this project.

What is contents of reducer-type.ts

reducer-type.ts has the following contents.

export interface ReduxAPIError {
  statusCode: string;
  message?: string;
}

interface ReduxAPIStruct<T> {
  isLoading: boolean;
  status: "success" | "failure";
  data: T;
  error: ReduxAPIError;
}

export const defaultSet = <T>(defaultValue?): ReduxAPIStruct<T> => ({
  isLoading: false,
  status: null,
  data: defaultValue || null,
  error: errorDefault()
});

export const errorDefault = (): ReduxAPIError => ({
  statusCode: null,
  message: ""
});

export default ReduxAPIStruct;
Enter fullscreen mode Exit fullscreen mode

Inside the application I read this useful tool as ReduxAPIStruct.

What is important is the interface below.

interface ReduxAPIStruct<T> {
  isLoading: boolean;
  status: "success" | "failure";
  data: T;
  error: ReduxAPIError;
}
Enter fullscreen mode Exit fullscreen mode

Each describes the motion when communicating with the application.

isLoading is managed by boolean and deals with whether or not it is loading.
status is created to facilitate fallback to the error component later.
data is the data body when actually obtained from the API. Here is defined as generic with T. When using this type later, pass the Response type of model/type.ts just as it is.
error has a type called ReduxAPIError.

export interface ReduxAPIError {
  statusCode: string;
  message?: string;
}
Enter fullscreen mode Exit fullscreen mode

ReduxAPIError has statusCode and message.
statusCode sets the statusCode of the response when accessing that API.
Message has a type to insert it in the response body from the server and later in saga.

A world with ReduxAPIStruct

First of all, as an important point of what imitated Atomic Design, we separated the clear responsibilities of pages and template.
For example, connect redux only to components of paegs class.

We define pages as a single page which has data and is linked to the URL .
On the other hand, template was created as a UI based on data .

By separating this clear obligation, the pages component looks like this.

For example, I would like to create an article list page that receives the following data.

{
  "items": [
    {
      "title": "タイトル1",
      "body": "本文1"
    },
    {
      "title": "タイトル2",
      "body": "本文2"
    },
    {
      "title": "タイトル3",
      "body": "本文3"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Suppose that the endpoint of this API is [GET] /items and the axios API client is an implementation as follows.

import axios from "axios";

class Client {
  public getItems() {
    return axios.get("/items");
  }
}
Enter fullscreen mode Exit fullscreen mode

The client implements it to return Promise .

The saga code will end, but let's say the result is in the key items of article reducer .

import * as React from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router";
import { requestGetItems } from "~/actions/article";
import ErrorHandler from "~/ErrorHandler";

// templates
import ItemsPageTemplate from "~/components/templates/ItemsPage";
import LoadingTemplate from "~/components/templates/Loading";

// type
import { Response } from "~/model/type";
import ReduxAPIStruct from "~/store/reducer-type";

interface Props {
  items: ReduxAPIStruct<Response.Article.Items>;
}

class ItemPage extends React.Component {
  public render() {
    const { items } = this.props;
    if(items.status === "failure") {
      return <ErrorHandler errorStatus={items.error.statusCode}/>;
    }

    return items.isLoading ? <LoadingTemplate /> : <ItemsPageTemplate items={items.data} />;
  }
}

export default wituRouter(connect(
  state => ({ items: state.articleReducer.items })
)(ItemPage) as any);
Enter fullscreen mode Exit fullscreen mode

If data is being acquired with this, LoadingTemplate will fail and fetch of data will fail.
If it fails to ErrorHandler it succeeds and ItemsPage will be mounted.

You can manage these three states regardless of which page you write or not thinking about anything.

Thanks to ReduxAPIStruct you can easily understand whether that data is in fetch or not, and failing fetch can also be understood by looking at status.

When saga is written by cutting this state management to another state, it is possible to manage the state without thinking about the component.

Afterword

I translate my English using Google Translate. If there is a mistake as a description of English, please point out with comments :(

Twitter: @konojunya
GitHub: konojunya

Top comments (0)