LoginSignup
6
5

More than 3 years have passed since last update.

[TypeScript]GitHubにGraphQLでアクセスするServerlessプログラムを作る

Posted at

[TypeScript]GitHubにGraphQLでアクセスするServerlesssプログラムを作る

※同じ記事をこちらにも書いています

動作画面・ソースコード

.
├── README.md
├── dist [出力ディレクトリ]
│   ├── index.html
│   └── js
│       ├── bundle.js
│       └── bundle.js.map
├── front
│   ├── public
│   │   └── index.html [トップページ定義]
│   ├── src
│   │   ├── App.tsx [Application初期設定]
│   │   ├── GitHub
│   │   │   ├── FirebaseGitAuthModule.ts  [FirebaseGit認証用モジュール]
│   │   │   ├── GitHubModule.ts           [Githubアクセス用モジュール]
│   │   │   └── GraphQLgetRepositories.ts [GraphQLクエリ]
│   │   ├── Parts [サブパーツコンポーネント]
│   │   │   ├── CircleButton [ボタンコンポーネント]
│   │   │   │   └── index.tsx
│   │   │   ├── FlexParent.tsx [配置スタイル定義用]
│   │   │   └── LodingImage [ローディングアニメーション]
│   │   │       ├── index.tsx
│   │   │       └── loading.svg
│   │   ├── RepositorieList [リポジトリリスト表示用]
│   │   │   └── RepositorieList.tsx
│   │   ├── TopArea [トップエリア]
│   │   │   ├── TopArea.tsx
│   │   │   └── Window [ログイン/ログアウトウインドウ]
│   │   │       ├── LoginWindow.tsx
│   │   │       ├── LogoutWindow.tsx
│   │   │       ├── WindowModule.tsx
│   │   │       └── WindowStyle.tsx
│   │   ├── config.ts [GitHub/FirebaseのAPIキー]
│   │   ├── index.tsx  [Store設定等]
│   │   ├── resource.d.ts [画像リソース定義用]
│   │   └── tsconfig.json
│   └── webpack.config.js
└── package.json

前提条件

 サーバレスのフロントエンドアプリケーションとしてGitHubのリポジトリデータを表示するアプリケーションを作成します

 GitHubのGraphQL APIを使用するには、アプリケーションキーを発行する必要がありますが、フロントエンドにシークレットキーを埋め込むわけにはいかないので、OAuthを行う方式をとります。

 GitHubのOAuthはバックエンドのリダイレクトを必要とするため、認証にはFirebaseを使用します。

使用パッケージ

 フロントエンドにはReactを使用します。@jswf系は自作ライブラリをnpmに登録したものなのでだれも知らないと思います。

項目 内容
TypeScript 使用言語
React フロントエンド構築用
firebase OAuth2認証用
redux-persist Storeデータをブラウザに保存
@jswf/react React用仮想ウインドウライブラリ
@jswf/redux-module Redux簡略化ライブラリ
@jswf/adapter 通信用ライブラリ

アプリケーションキーの発行

FirebaseとGitHubにそれぞれ設定が必要となります
この作業をすることによって、GitHubのAPIを呼び出すのに必要なtokenが受け取れるようになります

Firebaseで認証用プロジェクトの作成

image.png

https://console.firebase.google.com/ でプロジェクトを一つ作成
Authentication -> ログイン方法 -> GitHubを有効にする
認証コールバックURLを取得(ClientIDとClientSecretは後で記入)

GitHubにアプリケーションを登録

image.png

https://github.com/settings/developers でアプリケーションを作成
アプリケーション名とHomepageURL(仮アドレスでOK)と認証コールバックURLを設定
発行されたClientIDとClientSecretをFirebase側に設定

アプリケーション上で必要となるデータ

発行したキーの中から、プログラム上では以下の情報が必要となります

Firebase -> APIキー、AuthDomain
GitHub -> clientId

認証部分のプログラムを作成

 OAuth認証にはFirebaseを利用しているので、アプリケーション登録のための初期設定さえ完了していれば、プログラム的には単純に書くことが出来ます。

 まずは発行済みのキーを用意します。

config.ts
export const firebaseConfig = {
  apiKey: "",
  authDomain: ""
};
export const githubConfig = {
  clientId: ""
};

Firebaseから送られてきたcredentialの中からaccessTokenを取り出します。
これがGitHubのAPIを呼び出すときに必要となるキーになります。

また、スコープはログイン表示処理の方から送られてくるのですが、
  無指定
  "repo" プライベートリポジトリの情報を取得
  "read:org" 組織データを取得する
を選択するようになっています。

 データ入出力は@jswf/redux-moduleを使って、this.setStateとthis.getStateでStoreデータを書き換えています。コンポーネント間のやり取りは全部この方式が用いられているので、表示用のコンポーネントの中でpropsやstateは出てこない形となります。

FirebaseGitAuthModule.ts
import { ReduxModule } from "@jswf/redux-module";
import * as firebase from "firebase/app";
import "firebase/auth";
import { firebaseConfig } from "../config";

/**
 *保存ステータス
 *
 * @interface State
 * name GitHubユーザ名
 * token GitHubAPIアクセストークン
 */
interface State {
  name: string | null;
  token: string | null;
}

/**
 *Firebase用GitHub認証モジュール
 *
 * @export
 * @class FirebaseGitAuthModule
 * @extends {ReduxModule<State>}
 */
export class FirebaseGitAuthModule extends ReduxModule<State> {
  static defaultState: State = { name: null, token: null };
  static app?: firebase.app.App;
  /**
   *GitHubApiログイン処理
   *
   * @memberof FBGitAuthModule
   */
  public login(scopes: string[]) {
    //Firebaseの初期化
    if (!FirebaseGitAuthModule.app)
      FirebaseGitAuthModule.app = firebase.initializeApp(firebaseConfig);
    //認証スコープの定義
    const provider = new firebase.auth.GithubAuthProvider();
    scopes.forEach(scope => provider.addScope(scope));
    //Firebase経由のGitHubへの認証
    firebase
      .auth()
      .signInWithPopup(provider)
      .then(({ credential, additionalUserInfo }) => {
        if (additionalUserInfo && credential) {
          const name = additionalUserInfo.username;
          const token = (credential as firebase.auth.AuthCredential & {
            accessToken: string;
          }).accessToken;
          if (name && token) {
            if (
              this.getState("name") !== name &&
              this.getState("token") !== token
            )
              this.setState({ name, token });
          }
        }
      });
  }
  /**
   *GitHubAPIログアウト処理
   *
   * @memberof FBGitAuthModule
   */
  public logout() {
    this.setState({ name: null, token: null });
    if (FirebaseGitAuthModule.app) firebase.auth().signOut();
  }
  /**
   *GitHubアクセス用トークンの取得
   *
   * @returns
   * @memberof FirebaseGitAuthModule
   */
  public getToken() {
    return this.getState("token");
  }
  /**
   *GitHubユーザ名の取得
   *
   * @returns
   * @memberof FirebaseGitAuthModule
   */
  public getUserName() {
    return this.getState("name");
  }
}

GitHubへのアクセス

 organizationsの組織データを含むものと、そうで無いものに分けてGraphQLクエリーを作成しています。ちなみにデータが100件を超えた場合などは想定していません。

GraphQLgetRepositories.ts
/// GraphQLアクセス用クエリーデータ
export const getRepositories = `
{
  viewer {
    name: login
    repositories(last: 100) {
      ...rep
    }
}

fragment rep on RepositoryConnection {
  nodes {
    id
    url
    name
    owner {
      login
    }
    branches: refs(last: 10, refPrefix: "refs/heads/") {
      totalCount
      nodes {
        name
        target {
          ... on Commit {
            committedDate
            message
          }
        }
      }
    }
    stargazers {
      totalCount
    }
    watchers {
      totalCount
    }
    isPrivate
    createdAt
    updatedAt
    description
  }
}
`;
export const getRepositoriesOrg = `
{
  viewer {
    name: login
    repositories(last: 100) {
      ...rep
    }
    organizations(last: 100) {
      nodes {
        name
        repositories(last: 100) {
          ...rep
        }
      }
    }
  }
}

fragment rep on RepositoryConnection {
  nodes {
    id
    url
    name
    owner {
      login
    }
    branches: refs(last: 10, refPrefix: "refs/heads/") {
      totalCount
      nodes {
        name
        target {
          ... on Commit {
            committedDate
            message
          }
        }
      }
    }
    stargazers {
      totalCount
    }
    watchers {
      totalCount
    }
    isPrivate
    createdAt
    updatedAt
    description
  }
}
`;
/// クエリー結果の構造
export type QLRepositories = {
  nodes: {
    id: string;
    name: string;
    owner: { login: string };
    url: string;
    isPrivate: boolean;
    branches?: {
      totalCount: number;
      nodes: {
        name: string;
        target: { committedDate: string; message: string };
      }[];
    };
    watchers: { totalCount: number };
    stargazers: { totalCount: number };
    createdAt: string;
    updatedAt: string;
    description: string;
  }[];
};
export type QLRepositoryResult = {
  data: {
    viewer: {
      name: string;
      organizations?: {
        nodes: ({ name: string; repositories: QLRepositories } | null)[];
      };
      repositories: QLRepositories;
    };
  };
};

GraphQLクエリーで取得したデータを扱いやすい形に変換し、Storeに保存します。

GitHubModule.ts
import { ReduxModule } from "@jswf/redux-module";
import { Adapter } from "@jswf/adapter";
import { hasProperty } from "hasproperty-ts";
import {
  getRepositories,
  QLRepositoryResult,
  QLRepositories,
  getRepositoriesOrg
} from "./GraphQLgetRepositories";
import { FirebaseGitAuthModule } from "./FirebaseGitAuthModule";

//リポジトリ情報の構造
export type GitRepositories = {
  id: string;
  name: string;
  url: string;
  owner: string;
  stars: number;
  watchers: number;
  private: boolean;
  branche: {
    count: number;
    name: string;
    message: string;
    update: string;
  };
  createdAt: string;
  updatedAt: string;
  description: string;
}[];

/**
 *Reduxのストア保存ステータス
 *
 * @interface State
 */
interface State {
  repositories?: GitRepositories;
  loading: boolean;
  scopes: string[];
}

/**
 *GitHubアクセス用Reduxモジュール
 *
 * @export
 * @class GitHubModule
 * @extends {ReduxModule<State>}
 */
export class GitHubModule extends ReduxModule<State> {
  static includes = [FirebaseGitAuthModule];
  //Storeの初期状態
  static defaultState: State = { loading: false, scopes: [] };

  /**
   *ユーザ名の取得
   *
   * @returns
   * @memberof GitHubModule
   */
  public getLoginName() {
    const firebaseModule = this.getModule(FirebaseGitAuthModule);
    return firebaseModule.getUserName();
  }
  public setScopes(scopes: string[]) {
    this.setState({ scopes });
  }
  public getScopes() {
    return this.getState("scopes")!;
  }
  public isScope(scope: string) {
    return this.getState("scopes")!.indexOf(scope) >= 0;
  }
  /**
   *GitHubApiログイン処理
   *
   * @memberof GitHubModule
   */
  public login() {
    const firebaseModule = this.getModule(FirebaseGitAuthModule);
    firebaseModule.login(this.getState("scopes")!);
  }
  /**
   *GitHubAPIログアウト処理
   *
   * @memberof GitHubModule
   */
  public logout() {
    const firebaseModule = this.getModule(FirebaseGitAuthModule);
    firebaseModule.logout();
    this.setState({ repositories: [] });
  }
  /**
   *情報取得状況を返す
   *
   * @returns
   * @memberof GitHubModule
   */
  public isLoading() {
    return this.getState("loading")!;
  }
  /**
   *リポジトリの情報を返す
   *
   * @returns
   * @memberof GitHubModule
   */
  public getRepositories() {
    this.setState({ loading: true });
    return this.sendGitHub(
      this.isScope("read:org") ? getRepositoriesOrg : getRepositories
    )
      .then(e => {
        if (hasProperty<QLRepositoryResult["data"]>(e, "data")) {
          const repositories: { [key: string]: GitRepositories[0] } = {};

          const repPush = (_name: string, node: QLRepositories["nodes"][0]) => {
            const branche = node.branches
              ? node.branches.nodes.sort(
                  (a, b) =>
                    new Date(b.target.committedDate).getTime() -
                    new Date(a.target.committedDate).getTime()
                )[0]
              : undefined;
            repositories[node.id] = {
              id: node.id,
              name: node.name,
              url: node.url,
              private: node.isPrivate,
              branche: {
                count: node.branches ? node.branches.totalCount : 0,
                name: branche ? branche.name : "",
                message: branche ? branche.target.message : "",
                update: branche ? branche.target.committedDate : ""
              },
              stars: node.stargazers.totalCount || 0,
              watchers: node.watchers.totalCount || 0,
              owner: node.owner.login,
              createdAt: node.createdAt,
              updatedAt: node.updatedAt,
              description: node.description
            };
          };
          e.data.viewer.repositories.nodes.forEach(node =>
            repPush(e.data.viewer.name, node)
          );
          e.data.viewer.organizations &&
            e.data.viewer.organizations.nodes.forEach(org => {
              org &&
                org.repositories.nodes.forEach(node => repPush(org.name, node));
            });
          const rep = Object.values(repositories).sort((a, b) => {
            const av = a.branche.update
              ? new Date(a.branche.update).getTime()
              : 0;
            const bv = b.branche.update
              ? new Date(b.branche.update).getTime()
              : 0;
            return bv - av;
          });
          this.setState(rep, "repositories");
        } else this.setState({ repositories: [] });
      })
      .finally(() => {
        this.setState({ loading: false });
      });
  }

  /**
   *GitHubAPIに情報を要求する
   *
   * @param {(string | object)} params
   * @returns
   * @memberof GitHubModule
   */
  public async sendGitHub(params: string | object) {
    const firebaseModule = this.getModule(FirebaseGitAuthModule);
    const token = firebaseModule.getToken();
    if (token) {
      return Adapter.sendJsonAsync(
        "https://api.github.com/graphql",
        {
          query:
            typeof params === "object"
              ? (params as { loc: { source: { body: string } } }).loc.source
                  .body
              : params
        },
        { Authorization: `bearer ${token}` }
      ).catch(({ status }) => {
        if (status === 401) this.logout();
      });
    }
    return null;
  }
}

トップエリア表示用コンポーネント

 上部の表示やログイン処理の部分です
 表示はReactで書いていますが、このアプリで書いたコンポーネントはpropsや標準stateは一切使っていません
 コンポーネント間のやりとりは@jswf/redux-moduleを使って、ReduxのStore経由になっています

 以下はタイトルやログインボタンの表示処理です

TopArea.tsx
import React from "react";
import styled from "styled-components";
import { LogoutWindow } from "./Window/LogoutWindow";
import { WindowModule } from "./Window/WindowModule";
import { CircleButton } from "../Parts/CircleButton";
import { GitHubModule } from "../GitHub/GitHubModule";
import { useModule } from "@jswf/redux-module";
import { WindowState } from "@jswf/react";
import { LoginWindow } from "./Window/LoginWindow";

const Root = styled.div`
  z-index: 100;
  display: flex;
  padding: 0.5em;
  background-color: #aaffdd;
  > #title {
    flex: 1;
    font-size: 250%;
    font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
      "Lucida Sans", Arial, sans-serif;
    font-weight: bolder;
    color: white;
    -webkit-text-stroke: 1px rgba(30, 80, 60, 0.6);
  }
  > #buttons {
    padding: 0.2em;
    > * {
      margin-left: 0.5em;
    }
  }
`;

export function TopArea() {
  const gitHubModule = useModule(GitHubModule);
  const loginWindowModule = useModule(WindowModule, "Login", true);
  const logoutWindowModule = useModule(WindowModule, "Logout", true);
  const loginName = gitHubModule.getLoginName();
  const title = "GitHub Manager";
  return (
    <Root>
      <div id="title">{title}</div>
      <div id="buttons">
        <CircleButton onClick={() => gitHubModule.getRepositories()}>
          更新
        </CircleButton>
        <CircleButton
          onClick={() => {
            loginName
              ? logoutWindowModule.setWindowState(WindowState.NORMAL)
              : loginWindowModule.setWindowState(WindowState.NORMAL);
          }}
        >
          {loginName || "未ログイン"}
        </CircleButton>
      </div>
      <LoginWindow />
      <LogoutWindow />
    </Root>
  );
}

仮想ウインドウライブラリを使ったポップアップ式のウインドウを表示します

LoginWindow.tsx
import React from "react";

import { useModule } from "@jswf/redux-module";
import { CircleButton } from "../../Parts/CircleButton";
import { GitHubModule } from "../../GitHub/GitHubModule";
import { JSWindow, WindowState } from "@jswf/react";
import { githubConfig } from "../../config";
import { WindowStyle } from "./WindowStyle";
import { WindowModule } from "./WindowModule";

export function LoginWindow() {
  const gitHubModule = useModule(GitHubModule);
  const windowModule = useModule(WindowModule, "Login");
  const windowState = windowModule.getWindowState();
  const scopes = new Set(gitHubModule.getScopes());

  return (
    <>
      <JSWindow
        width={400}
        windowState={windowState}
        title="ログイン"
        clientStyle={{ backgroundColor: "#aaeeff" }}
        onUpdate={e =>
          windowState !== e.windowState &&
          windowModule.setWindowState(e.windowState)
        }
      >
        <WindowStyle>
          <div id="link">
            <a
              target="_blank"
              href={`https://github.com/settings/connections/applications/${githubConfig.clientId}`}
            >
              GitHubの権限設定
            </a>
          </div>
          <div id="link">
            <a target="_blank" href="https://github.com/logout">
              GitHubのログアウト
            </a>
          </div>
          <div id="message">
            <div>ログインしますか?</div>
            <div id="option">
              <div>
                <label>
                  <input
                    type="checkbox"
                    checked={scopes.has("repo")}
                    onChange={e => {
                      e.target.checked
                        ? scopes.add("repo")
                        : scopes.delete("repo");
                      console.log(scopes);
                      gitHubModule.setScopes(Array.from(scopes));
                    }}
                  />
                  プライベートリポジトリにアクセス
                </label>
              </div>
              <div>
                <label>
                  <input
                    type="checkbox"
                    checked={scopes.has("read:org")}
                    onChange={e => {
                      e.target.checked
                        ? scopes.add("read:org")
                        : scopes.delete("read:org");
                      gitHubModule.setScopes(Array.from(scopes));
                    }}
                  />
                  組織のポジトリにアクセス
                </label>
              </div>
            </div>
          </div>

          <CircleButton
            onClick={() => {
              windowModule.setWindowState(WindowState.HIDE);
              gitHubModule.login();
            }}
          >
            OK
          </CircleButton>
          <CircleButton
            onClick={() => windowModule.setWindowState(WindowState.HIDE)}
          >
            Cancel
          </CircleButton>
        </WindowStyle>
      </JSWindow>
    </>
  );
}

以下はログアウト用ウインドウです

LogoutWindow.tsx
import React from "react";
import { useModule } from "@jswf/redux-module";
import { CircleButton } from "../../Parts/CircleButton";
import { GitHubModule } from "../../GitHub/GitHubModule";
import { JSWindow, WindowState } from "@jswf/react";
import { githubConfig } from "../../config";
import { WindowStyle } from "./WindowStyle";
import { WindowModule } from "./WindowModule";

export function LogoutWindow() {
  const gitHubModule = useModule(GitHubModule);
  const windowModule = useModule(WindowModule, "Logout");
  const windowState = windowModule.getWindowState();
  return (
    <>
      <JSWindow
        windowState={windowState}
        title="ログアウト"
        clientStyle={{ backgroundColor: "#aaeeff" }}
        onUpdate={e =>
          windowState !== e.windowState &&
          windowModule.setWindowState(e.windowState)
        }
      >
        <WindowStyle>
          <div id="link">
            <a
              target="_blank"
              href={`https://github.com/settings/connections/applications/${githubConfig.clientId}`}
            >
              GitHubの権限設定
            </a>
          </div>
          <div id="link">
            <a target="_blank" href="https://github.com/logout">
              GitHubのログアウト
            </a>
          </div>
          <div id="message">
            <div>ログアウトしますか?</div>
          </div>

          <CircleButton
            onClick={() => {
              windowModule.setWindowState(WindowState.HIDE);
              gitHubModule.logout();
            }}
          >
            OK
          </CircleButton>
          <CircleButton
            onClick={() => windowModule.setWindowState(WindowState.HIDE)}
          >
            Cancel
          </CircleButton>
        </WindowStyle>
      </JSWindow>
    </>
  );
}

リポジトリ表示部分

データはGitHubModuleから送られてくるので、あとはそれを表示するだけです
表示には@jswf/reactのListView機能を使っています
このListViewはWindowsライクなヘッダサイズ可変機能がついています

RepositorieList.tsx
import {
  ListView,
  ListHeaders,
  ListHeader,
  ListRow,
  ListItem
} from "@jswf/react";
import React, { useEffect, useRef } from "react";
import { GitHubModule, GitRepositories } from "../GitHub/GitHubModule";
import { useModule } from "@jswf/redux-module";
import { LoadingImage } from "../Parts/LodingImage";
import dateFormat from "dateformat";
import styled from "styled-components";

const Root = styled.div`
  flex: 1;
  overflow: hidden;
  position: relative;
  #name {
    > div:nth-child(2) {
      font-size: 60%;
    }
  }
  #org {
    font-size: 80%;
    white-space: normal;
  }
  #message {
    white-space: normal;
  }
`;

export function RepositorieList() {
  const gitHubModule = useModule(GitHubModule);
  const repositories = gitHubModule.getState("repositories");
  const loginName = gitHubModule.getLoginName();
  const listView = useRef<ListView>(null);
  const loading = gitHubModule.isLoading();
  const firstUpdate = useRef(true);
  useEffect(() => {
    //初回は無視
    if (firstUpdate.current) {
      firstUpdate.current = false;
      return;
    }
    //リポジトリデータが無ければ要求
    gitHubModule.getRepositories();
  }, [loginName]);

  return (
    <Root>
      {loading && <LoadingImage />}
      <ListView
        ref={listView}
        onItemDoubleClick={index =>
          listView.current &&
          window.open(
            (listView.current!.getItemValue(index) as GitRepositories[0]).url,
            "_blank"
          )
        }
      >
        <ListHeaders>
          <ListHeader width={250}>Name</ListHeader>
          <ListHeader width={100}>Owner</ListHeader>
          <ListHeader>Private</ListHeader>
          <ListHeader type="number">Branches</ListHeader>
          <ListHeader type="number">Stars</ListHeader>
          <ListHeader type="number">Watchers</ListHeader>
          <ListHeader width={180}>Date</ListHeader>
          <ListHeader width={180}>Last Branch</ListHeader>
          <ListHeader>Commit Message</ListHeader>
        </ListHeaders>
        {repositories &&
          repositories.map(e => (
            <ListRow key={e.id} value={e}>
              <ListItem value={e.name}>
                <div id="name">
                  <div>{e.name}</div>
                  <div>{e.description}</div>
                </div>
              </ListItem>
              <ListItem value={e.owner}>
                <div id="org">{e.owner}</div>
              </ListItem>
              <ListItem>{e.private && "*"}</ListItem>
              <ListItem>{(e.branche && e.branche.count) || 0}</ListItem>
              <ListItem>{e.stars}</ListItem>
              <ListItem>{e.watchers}</ListItem>
              <ListItem value={new Date(e.updatedAt).getTime()}>
                <div>
                  <div>
                    U:{dateFormat(new Date(e.updatedAt), "yyyy/mm/dd HH:MM")}
                  </div>
                  <div>
                    C:{dateFormat(new Date(e.createdAt), "yyyy/mm/dd HH:MM")}
                  </div>
                </div>
              </ListItem>
              <ListItem
                value={
                  e.branche.update ? new Date(e.branche.update).getTime() : 0
                }
              >
                <div>
                  {e.branche.update &&
                    dateFormat(new Date(e.branche.update), "yyyy/mm/dd HH:MM")}
                </div>
                <div>{e.branche.name}</div>
              </ListItem>
              <ListItem>
                <div id="message">{e.branche && e.branche.message}</div>
              </ListItem>
            </ListRow>
          ))}
      </ListView>
    </Root>
  );
}

まとめ

 今回のプログラムを作ってみて、GraphQLのクエリーを初めて触ってみたのですが、慣れていないせいもあって、欲しいデータにたどり着くまでかなり時間がかかりました。それとGitHubのAPIはRestだと取得できるのにGraphQLでは存在しないデータ(Insights系)があって不便でした。この辺りは今後追加されることを期待しています。

 GraphQLへのアクセスは、自作ライブラリで固めてしまったのですが、その中でも@jswf/redux-moduleはかなり扱いやすいです。これを使うことによって必要な非同期のデータの入出力は、専用のモジュールで集中して作ることができます。表示側のコンポーネントは、作成したモジュールを使用することによって、自動的にデータ更新の副作用が起こるようになります。ということで細かいことを気にしなくてもどんどんプログラムが作っていけます。しかもReduxの面倒な手続きは一切書く必要がありません。自分で作っておいてなんですがヤバいです。

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5