20191215のReactに関する記事は16件です。

ApolloClient(React)で、loadingやerror時の表示処理を共通化する。

元々はこちらで紹介されている方法です。
How to handle loading and error state in a generic way?

ローディング中にコンテンツを覆うモーダルウィンドウ。

const LoadingModal: React.FC = () => (
  <Dialog open>
    <ProgressImage />
  </Dialog>
);

エラーメッセージを表示するダイアログ。

import { ApolloError } from 'apollo-client';

interface ErrorModalProps {
  error: ApolloError;
  onClose(): void;
}

const ErrorModal: React.FC<ErrorModalProps> = ({ error, onClose }) => {
  const [open, setOpen] = useState<boolean>(!!error);
  const handleClose = () => {
    setOpen(false);
    onClose();
  };

  return (
    <Dialog open onClose={handleClose}>
      {error.message}
      <Button onClick={handleClose}>
        close
      </Button>
    </Dialog>
  );
};

PropsでuseQueryORuseMutateのステータス値を受け取り、必要に応じてダイアログを表示するコンポーネント。

interface HandleQueryProps {
  loading: boolean;
  error?: ApolloError;
}

const HandleQuery: React.FC<HandleQueryProps> = ({
  loading,
  error,
  children,
}) => {
  const onError = ()=> {
    setOpen(false);
    // 必要に応じてリダイレクトなど
  };

  return (
    <>
      {children}
      {loading && <LoadingModal />}
      {error && <ErrorModal error={error} onClose={onError} />}
    </>
  );
};

HandleQueryコンポーネント利用例。

const SameComponent: React.FC = () => {
  const { data, loading, error } = useQuery(QUERY);

  return (
    <HandleQuery loading={loading} error={error}>
      <ChildComponent data={data} />
    </HandleQuery>
  );
}

参考情報

APOLLO DOCS > Client(React) > Error handling
React > Error Boundary

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Redux入門

Reduxってなに?

ReduxとはReactと相性が良いフレームワークのことです。
Redux単体で利用することも可能ですが、ReactとReduxの組み合わせは鉄板でしょう。
stateを容易に管理することの出来るReduxですが、大規模なアプリケーションになればなるほど効果を発揮してくれそうです。

Reduxアプリを構成する機能ってなに?

Reduxを使ったアプリはAction,Reducer、Storeによって構成されています。

Actionとは

何がおきたのかという情報を持つオブジェクトです。

ActionをStoreへ送信(dispatch)すると、Storeのstateが変更されます。
stateの変更ではActionが必ず必要となります。
stateへ通じるルートを攻略する第一段階、まるで門番のような立ち位置の機能ですね。

const action = {
  type: 'SET_wanko',
  text: 'トイプードル'
};

Actionではどういうタイプのアクションなのかを明示するためtypeプロパティが必要となります、他と区別できないと何がなんだかわからなくなりますもんね。

逆に言えばこのActionはその程度の情報しか持っておらず、どのようにstateを変更するのか知らない存在なのです。

Reducerとは

上記したSET_wankoというタイプのアクション受けて、storeから受け取ったstateを変更して返す純粋関数です。
Reducer内では引数変更したり、API呼び出したり、Math.random()等の純粋関数以外の関数を呼び出してはいけません。
結果が毎回同一になるような操作しか扱えないのです。

stateをどう変更するのかactionでは決めれなかったことを指定しています。

function triming(state = [], action) {
  switch (action.type) {
    case 'SET_wanko':
      return state.concat([{ text: action.text, completed: false }]);

    default:
      return state;
  }
}

Storeとは

Storeとはアプリケーションの全てのstateを保持するオブジェクトです。

dispatchされたActionと保持するstateをreducerに渡してstate変更に一役買う立場の存在で、ボスのような風格ですね。
Storeの複製はダメです、ボスは一人だけなのです。
又、stateの変更は必ずActionを経由してください、バグの特定が困難になるのを防ぐためです。
ボスに会うためにはまず名乗って(Action type)からが礼儀ってもんです。

// Action
const action = {
  type: 'SET_wanko',
  text: 'トイプードル'
};

// Reducer
function triming(state = [], action) {
  switch (action.type) {
    case 'SET_wanko':
      return state.concat([{ text: action.text, completed: false }]);

    default:
      return state;
  }
}

// Store
const store = Redux.createStore(triming);

// Actionをdispatchする
// Reducerであるtodosが実行され、Storeが保持しているstateが変更される。
store.dispatch(action);

// stateを取得する
console.log(store.getState()); 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React上でtailwindcssを使いたい!

この記事について

今話題になってるらしいCSSフレームワークの「Tailwindcss」をReactで使ってみるためのまとめ。

そもそもTailwindって

CSSフレームワークの一つです。
下のGifを見ればわかりやすいと思いますが、要素に対してclassを指定することでスタイルを適用できます。
https://tailwindcss.com/
screen.gif

開発環境

  • VsCode
  • create-react-appした状態
  • npm 6.12.1

インストール手順

パッケージインストール

対象プロジェクトのpackage.jsonがあるフォルダで下記コマンドを実行。

npm install postcss-cli autoprefixer --save-dev
or
yarn add postcss-cli autoprefixer --save-dev

styles.css作成

tailwindcssのいろいろをインポートした.cssファイルを作成します。

styles.css
@tailwind base;

@tailwind components;

@tailwind utilities;

package.jsonにビルド用のスクリプトを作成し実行

package.json
"scripts": {
    "start": "react-scripts start",
    "build:tailwind":"tailwind build src/styles.css -o src/tailwind.css",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
npm run-script build:tailwind

そうすると、tailwind.cssが作成される。

buildされたtailwind.cssを任意のファイルでインポート

index.js
import './tailwind.css';

動作確認

任意のコンポーネントに下記コードを張り付けて、正常にスタイルが適用されてるか確認します。

App.js
import React from "react";

function App() {
  return (
    <div className="App">
      <div class="max-w-sm rounded overflow-hidden shadow-lg">
        <img
          class="w-full"
          src="https://source.unsplash.com/random/1600x900/"
          alt="Sunset in the mountains"
        ></img>
        <div class="px-6 py-4">
          <div class="font-bold text-xl mb-2">The Coldest Sunset</div>
          <p class="text-gray-700 text-base">
            Lorem ipsum dolor sit amet, consectetur adipisicing elit.
            Voluptatibus quia, nulla! Maiores et perferendis eaque,
            exercitationem praesentium nihil.
          </p>
        </div>
        <div class="px-6 py-4">
          <span class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2">
            #photography
          </span>
          <span class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2">
            #travel
          </span>
          <span class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700">
            #winter
          </span>
        </div>
      </div>
    </div>
  );
}

export default App;

こんな感じになってたら成功です。
(画像はrandomでunsplashAPIから取ってきてるので変わってるはずです。)
image.png

最後に

bootstrapはなんも考えなくても適当にクラスつけてれば、いい感じのスタイルにできてたのに比べて、tailwindcssはしっかりCSS理解してクラスつけないとうまくいかんなーって所感です。
tailwindcss自体もうちょい勉強せんとなーって思いました。

また細かいtaiwindcssの使い方は記事にまとめます。

参考記事

https://blog.logrocket.com/create-react-app-and-tailwindcss/
https://qiita.com/zzzzz/items/68461080515ec1012980

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.js で Page Transition を実装する

この記事はNext.js Advent Calendar 2019 16日目の記事です。

Qiitaに書くのは久々の @_Ria0130です。

SPAを作っていても Transition を実装したことない方結構いらっしゃるんじゃないでしょうか?
ページ遷移と Transition の組み合わせって結構難しくって、クロスフェードとかをしようとすると遷移前と遷移後の要素をどちらも描画したままスタイルを適応する必要があります。

React など VirtualDOM を扱うライブラリを採用しているとDOMの反映を自動でしてくれて楽な反面、要素のコントロールが自分でできないので少し複雑です。

React で有名な Transition ライブラリだと React Transition Group があるのですが、今回は UIT INSID Eep.29 で紹介されていて気になっていた react-spring を使って実装する方法を紹介していきます。

_app.jsx

_app.jsx
import React from "react";
import App, { Container } from "next/app";
import { PageTransition } from "../components/PageTransition";
import { GlobalStyle } from "../components/GlobalStyle";

export default class extends App {
  static async getInitialProps({ Component, ctx }) {
    let pageProps = {};

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    return { pageProps };
  }

  render() {
    const { Component: SsrComponent, pageProps: ssrPageProps } = this.props;

    return (
      <Container>
        <GlobalStyle />
        <PageTransition>
          {({ Component, pageProps }) => {
            return Component ? (
              <Component {...pageProps} />
            ) : (
              <SsrComponent {...ssrPageProps} />
            );
          }}
        </PageTransition>
      </Container>
    );
  }
}

_app.jsx では次で説明する <PageTransition> でページのコンポーネントを囲みます。

PageTransition.jsx

PageTransition.jsx
import React, { useContext } from "react";
import styled from "styled-components";
import { useTransition, animated } from "react-spring";

import { withRouter } from "next/router";

const Context = React.createContext();

const Provider = ({ router, children }) => (
  <Context.Provider value={router}>{children}</Context.Provider>
);

const useRouter = () => useContext(Context);
const RouterContextProvider = withRouter(Provider);

const Transition = ({ children, ...props }) => {
  const router = useRouter();
  const transitions = useTransition(router, router => router.pathname, {
    from: { opacity: 0 },
    enter: { opacity: 1 },
    leave: {
      position: "absolute",
      top: 0,
      right: 0,
      bottom: 0,
      left: 0,
      opacity: 0
    }
  });

  return (
    <>
      {transitions.map(({ item, props: style, key }) => {
        const { Component, props } = item.components[item.pathname] || {};

        return (
          <Page key={key} style={style}>
            {children(
              item ? { Component, pageProps: props && props.pageProps } : {}
            )}
          </Page>
        );
      })}
    </>
  );
};

export const PageTransition = ({ children, ...props }) => {
  return (
    <RouterContextProvider>
      <Transition {...props}>{children}</Transition>
    </RouterContextProvider>
  );
};

const Page = styled(animated.main)`
  min-height: 100%;
  height: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
`;

PageTransition.jsx ではコンポーネントが複数あって Context も宣言してしまってるのですが、なるべく PageTransition を使う側で _app.jsx のようにシンプルになるようまとめて宣言しています。

PageTransition コンポーネントでは next/routerwithRouter を保持する Context を宣言し、 Transition コンポーネントを呼び出します。
Transition コンポーネントでは router の値を react-springuseTransition に渡すことで Transition を実装しています。

CodeSandbox に全体のソースがあるので試したい方はそちらからご確認ください。

この実装だとページ全体ではなく、コンテンツだけあるいはサイドバーだけ Transition させることも可能で、その場合は _app.jsx ではなくコンテンツのラッパーコンポーネントやサイドバーで PageTransition を呼び出すことで出来ると思うのでお試しください。

それでは、よいReactライフを。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsでcookieをシンプルに扱うことができるライブラリ nookies を紹介

Next.jsでcookieを扱うのは大変

Next.jsなどのサーバーサイドレンダリング(以下SSR)をしているフレームワークでcookieを扱うのは面倒くさいですよね。
その理由の一つとして、同じコードでもSSRの場合とクライアントでレンダリングしている場合で挙動が違うということがあります。
例をお見せしましょう

クライアントでレンダリングしている場合

console.log(document.cookie); // accessToken=test1234;

SSRの場合

console.log(document.cookie); // ReferenceError: document is not defined

原因

クライアントサイド(ブラウザ)でレンダリングしている時は、ブラウザに保存されているcookieにアクセスできるが,
SSRの時はブラウザに保存されているcookieにアクセスできません。

SSRの時にcookieを扱うには

SSRでcookieの情報はここに入っています

index.tsx
const TestPage: NextPage<Props> = (props) => {
    return <div>test</div>
}

TestPage.getInitialProps(ctx) {
    // ここ
    console.log(ctx.req.headers.cookie) // accessToken=test1234;     

    return {};
}

同じライブラリをクライアントとSSRで共有していたりすると、条件分岐などが大変ですね
そんな時に nookies を使います
https://www.npmjs.com/package/nookies

使い方

以下の例で示すようにクライアントサイドの場合ctxを渡さずに、SSRならctxを渡せば、cookieをオブジェクトに整形して返してくれます。

tool.ts
import { parseCookies } from 'nookies';
import { NextPageContext } from 'next';

export function printCookie(ctx?: NextPageContext) {
    const cookie = parseCookies(ctx);
    console.log(cookie) // { accessToken: 'test1234' }
}

また、cookieの追加もクライアントとSSR分け隔てなく行ってくれます

set_cookie.ts
import { setCookie, destoroyCookie } from 'nookies';
import { NextPageContext } from 'next';

export function setCookie(ctx?: NextPageContext, token: string) {
        setCookie(ctx, 'accessToken', token, {
            maxAge: 30 * 24 * 60 * 60,
        });
}

// ついでにcookie削除(動作確認してません)
export function destoroyCookie(ctx?: NextPageContext) {
    destroyCookie(ctx, 'accesstToken')
}

ライブラリを読んでみた(箇条書きです!)

https://github.com/maticzav/nookies

nookies/src/index.ts
const isBrowser = () => typeof window !== 'undefined' // 今の環境がSSRかクライアントサイドレンダリングか調べてるらしいです

.
.

if (ctx && ctx.req && ctx.req.headers && ctx.req.headers.cookie) { 
    return cookie.parse(ctx.req.headers.cookie as string, options) // SSRだったらctx.req.headers.cookieに入っているcookieをparseして返却
} 

.
.

if (isBrowser()) { 
   return cookie.parse(document.cookie, options) //クライアントだったらdocument.cookieにあるcookieをparseして返却
} 

.
.

ctx.res.setHeader('Set-Cookie', cookiesToSet) // SSRならレスポンスヘッダーにcookieをセットする

.
.

if (isBrowser()) { |
    document.cookie = cookie.serialize(name, value, options) // クライアントならクッキーをセット
}

まとめ

以上です。いかがでしたでしょうか?
参考になりましたら幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

create-react-appで作ったアプリがhttpsだと動かない

問題点

create-react-appで作成したアプリケーションにhttpsでアクセスすると、以下のようにエラーとなりました。
SecurityError: Failed to construct 'WebSocket': An insecure WebSocket connection may not be initiated from a page loaded over HTTPS.
d1fb5456080f4fbb895c367aae298593.jpeg
httpだと問題なく動きます。
create-react-appで作ったアプリを試しにHerokuに上げてみたときに、この問題を踏みました。

原因

以下でIssuesが上がっていました。
https://github.com/facebook/create-react-app/issues/8075
https://github.com/facebook/create-react-app/pull/8079

WebSocketsを利用している箇所で、httpsの場合はwss(WebSockets over SSL)を利用しなくてはいけないところ、wsを利用してしまっているためのようです。

解決法

問題が発生しているときのpackage.jsonの依存関係は以下です。

package.json
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-scripts": "3.3.0"
  }

react-scriptの3.3.0で発生している問題なので、3.2.0にバージョンダウンすると、一旦動作するようになります。

$ npm install react-scripts@3.2.0

react-scriptsのバージョン変更がpackage.jsonにも反映され、httpsでも動作するようになりました。

package.json(更新後)
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.4.0",
    "@testing-library/user-event": "^7.1.2",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-scripts": "3.2.0"
  }

2cd9968afd83f4f2863a9ff10f724a74.jpeg

上記Issuesは、react-scriptの3.3.1で修正予定(2019/12/15時点)のようなので、バージョン下げは暫定対応とし、修正されたら3.3.1に上げるのがよいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.js + AWS Amplify + Graphqlで作るサーバーレスアプリケーション環境構築

概要

こんにちは、先日AWSのre:Inventで発表されたAWS Amplify DataStoreがめちゃくちゃ便利すぎて驚きを隠せない、都内でフロントエンドエンジニアをしていますかめぽんです。
今まで、VueやNuxtでの開発がメインでやってきておりましてReact、Nextでの開発経験が少なくまたTypescriptでなにか出来ないかなと探しておりました。
最近だとモバイル開発のためのAWSのサービスをインテグレートしたAWS版firebaseのようなAmplifyというサービスが出ています。内容をみてみると、何やら爆速でサーバーレスアプリケーションが出来そうだなと感じたので、Next.js + AmplifyでTodoアプリを作ってみました。巷では、Nuxt.js + firebaseの組み合わせの記事がかなり多かったですが、こちらのアーキテクチャでも実装出来たのでその方法を体系的にまとめて見ました。
Next + Amplifyでの開発がなんとなくわかるようになると思うので、ぜひ最後まで読んでいただけると嬉しいです。

Next.jsとは

top_next.png

こちらはもはや説明不要かもしれませんが、ZEIT社が開発したサーバーサイドレンダリング対応のwebアプリケーションを構築できるReact製フレームワークです。

pages配下の自動ルーティングやダイナミックルート、SPA/SSRに始まり静的サイトジェネレートなアプリはもちろん、最近だとゼロコンフィグでTypescriptがそのまま使えたり、AMP対応、apiディレクトリによるapiの実装などかなりDXがよくなってきています。もちろん導入も手軽にできるので開発スピードを格段に高めることが出来ます。

AWS Amplifyとは

top_amplify.png

AWS AmplifyはAWSのサービスを仕様したmBaasの一種で、webアプリケーション作成、設定、開発をかなり簡単にすることが出来、スケーラブルでもあるためサービスの規模に応じてオートスケールさせることも可能です。似たようなサービスではgoogle社のfirebaseがあります。バックエンドの資材を自分で準備しなくてもAmplifyのコマンドで必要なソースコードやモデルなどを準備してくれます。本来であれば、設定やプロビジョニング、分析などを全て自前で行っていかなければいけませんが、選択したもに関してAmplifyはそれらを管理してくれます。認証、オフラインデータ、解析、プッシュ通知、AR/VR、botなどを必要に応じてAWSサービスをアプリケーションに対してインテグレートします。

Next.js + Amplifyは何が嬉しいのか

初期開発スピードの爆速化

昨今では、マーケットの変化が非常に早くユーザーニーズも目まぐるしく変わっています。それに加えて、サービスやプロダクトにおける提供すべきUXも何が適切かわかりにくくなってきている中で、ビジネスサイドと開発サイドで共通認識として持っていなければならないのが仮説検証を高速に回し、フィードバックをUXに還元することです。DX時代とその未来における「ユーザーエクスペリエンス」についての基本を抑えるという記事を書かせていただいたのですが、サービスのローンチだけでなくそこに至る検証もスピード感を持って行うことが重要です。そこで、技術選定やアーキテクチャをどうするのかは悩む部分だと思いますが、いかに早く社会実装するかという観点で見るのそれ自体は早く解決すべき問題です。もちろん軽視すべき問題ではないというのが前提です。

そういった中で、Next.js + Amplifyの組み合わせはそれを解決することができると考えています。両者の環境構築で必要な時間は、独自で設計に応じてAWSサービスの選定をしたり構築することに比べても時間がかからずすぐにlocalhostなどで確認出来ますし、必要に応じてバックエンドサービスを提供してくれます。

適切な型やモデルを定義した上でのオートスケール

Next.jsではゼロコンフィグでTypescriptを動かせますし、AmplifyではAppSyncというサービスを含んでいてGraphqlでのデータのやりとりをします。そこで、必要なデータの型を定義してくれるのでフロントエンドとバックエンドで共通で型を使用することが容易です。
フレームワークが何かよりも最重要ビジネスルールは何かを抑える方が大事で、それを定義した上でオートスケールできるのでデータの保全性を担保しつつ希望に応じて対応することが出来ます。

Next.jsのセットアップ

早速Next.jsのセットアップを始めていきます。

ディレクトリ構成

最終的なディレクトリ構成は以下のようになっています。実際のディレクトリから必要な部分だけ掲載してますので、全ファイルを確認したい場合はGithubにコードを準備してますのでぜひ参考にしてみてください。amplifygraphqlディレクトリに関しては以降のAWS Amplifyのセットアップにて自動生成されます。

├─ amplify
│  ├─ #current-cloud-backend
│  │  ├─ amplify-meta.json
│  │  ├─ api
│  │  │  └ todo
│  │  │   ├─ build
│  │  │   ├─ parameters.json
│  │  │   ├─ resolvers
│  │  │   ├─ schema.graphql
│  │  │   ├─ stacks
│  │  │   └─ transform.conf.json
│  │  └─ backend-config.json
│  ├─ backend
│  │  ├─ amplify-meta.json
│  │  ├─ api
│  │  │  └ todo
│  │  │   ├─ build
│  │  │   ├─ parameters.json
│  │  │   ├─ resolvers
│  │  │   ├─ schema.graphql
│  │  │   ├─ stacks
│  │  │   └─ transform.conf.json
│  │  ├─ awscloudformation
│  │  │  └ nested-cloudformation-stack.yml
│  │  └─ backend-config.json
│  └─ team-provider-info.json
├─graphql/
│ ├─queries.ts
│ ├─mutations.ts
│ ├─subscriptions.ts
│ └─schema.json
├─pages/
│ ├─index.tsx
│ └─todo.tsx
├─components/
│ └─templates/
│   ├─head.tsx
│   └─navigation.tsx
├─store/
├─aws-exports.js
├─package.json
├─.gitignore
├─next.config.js
├─next-env.d.ts
└─tsconfig.json

必要なモジュールのインストールとpackage.jsonの編集

作業ディレクトリが出来たら、以下コマンドで必要なモジュールの準備をしましょう。

npm install --save react react-dom next
npm install --save-dev @types/node @types/react

インストール出来きたら、package.jsonのscriptsを以下のように編集します。

package.json
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  }

各種コンポーネントの準備

ここでは、共通で使うコンポーネントの定義をします。ナビゲーション用とhead部用のコンポーネントを準備します。

navigations.tsx
import * as React from 'react';
import Link from 'next/link'

const Navigation: React.FC = () => {
  return (
    <div>
      <Link href="/">
        <p>Index</p>
      </Link>
      <Link href="/about">
        <p>About</p>
      </Link>
      <Link href="/todo">
        <p>Todo</p>
      </Link>
    </div>
  )
}
export default Navigation

head用のコンポーネントに関してはお好みで設定してみてください。

head.tsx
import * as React from 'react'
import Head from 'next/head'
import info from '../../package.json'

const defaultOGURL = ''
const defaultOGImage = ''

interface Props {
  title: string,
  description?: string,
  url?: string,
  ogImage?: string,
}

const head: React.FC<Props> = props => {
  return (
    <Head>
      <meta charSet="UTF-8" />
      <title>{props.title || ''}</title>
      <meta name="description" content={props.description || info.description} />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <meta property="og:url" content={props.url || defaultOGURL} />
      <meta property="og:title" content={props.title || ''} />
      <meta
        property="og:description"
        content={props.description || info.description}
      />
      <meta name="twitter:site" content={props.url || defaultOGURL} />
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:image" content={props.ogImage || defaultOGImage} />
      <meta property="og:image" content={props.ogImage || defaultOGImage} />
      <meta property="og:image:width" content="1200" />
      <meta property="og:image:height" content="630" />

      <link rel="icon" sizes="192x192" href="/static/touch-icon.png" />
      <link rel="apple-touch-icon" href="/static/touch-icon.png" />
      <link rel="mask-icon" href="/static/favicon-mask.svg" color="#49B882" />
      <link rel="icon" href="/static/favicon.ico" />
  </Head>
  )
};

export default head

pagesの準備

次にpagesコンポーネントの準備です。

pages/index.tsx
import * as React from 'react';
import Head from '../components/templates/head'
import Navigation from '../components/templates/navigation'

const Index: React.FC = () => {
  return (
    <div>
      <Head title="Index page" />
      <Navigation />
      <p>Hello world</p>
      <p>Index</p>
    </div>
  )
}
export default Index

pages/todo.tsx
import * as React from "react";
import Head from '../components/templates/head'
import Navigation from '../components/templates/navigation'

const Todo = () => {

  return (
    <div>
      <Head title="todo" />
      <Navigation />
      <h2>Todo with amplify</h2>
    </div>
  )
};

export default Todo;

この状態で npm run devのコマンドを実行し、http://localhost:3000/にアクセスしてみましょう。以下のような画面にが表示されたら成功です。試しに/todoにもアクセスしてみてtodoページが表示されるかもみてみましょう。

スクリーンショット 2019-12-15 18.17.02.png

Next.jsの準備は一旦以上です。

AWS Amplifyのセットアップ

Amplifyのインストールと初期セットアップ

なにはともあれ、amplify/cliのグローバルインストールをします。

npm install -g @aws-amplify/cli

インストールが完了したら、amplify -vでバージョンを確認しましょう。以下のような表記になっていたらインストール完了です。

Scanning for plugins...
Plugin scan successful
3.17.0

次にAWSアカウントの紐付けを行います。コンソールにて

amplify configure

とコマンドを打つとIAMユーザーを作成するためにブラウザが立ち上がります。アカウントがあればログイン、なければ新規作成を行いましょう。

aws_account.png

スクリーンショット 2019-11-09 3.09.57.png

accessKeyIdsecretAccessKeyIdが順番に出るので、それをIAMユーザーの作成画面に貼ります。

スクリーンショット 2019-12-15 18.37.54.png

画面を進めて行くとIAMユーザー作成画面側にアクセスキーIDが表示されるので、コンソール側に貼り付けてEnterを押します。Profile nameを決めた後、Enterを押しSuccessfully set up the new user.のメッセージが出たら完了です。

Amplifyをプロジェクトで扱えるようにする

ここからは実際にNext.js上でamplifyを使っていく流れを説明していきます。
amplifyバックエンドの様々なサービスを扱えるようにするため、各種リソースをプロジェクトフォルダ内に作成します。
以下コマンドを打ってみましょう。

amplify init

以下のように、使用言語やフレームワーク、ディレクトリ情報などをインタラクティブ形式で進めていきます。

スクリーンショット 2019-11-09 3.47.46.png

Initializing project in the cloud...のメッセージが出るとバックエンドの資材を初期化&準備し始めるので待ちましょう。
Your project has been successfully initialized and connected to the cloud!が出たら準備が整う合図になります。

バックエンドAPI(Graphql)の準備

次にAPIの準備をするために、以下コマンドを打ちます。

amplify add api

そうすると、プロジェクト内で扱うapiの種類や名前、スキーマの設定をインタラクティブに決めていきます。今回はGraphqlを使用していきます。
以下、質問例になります

スクリーンショット 2019-12-15 18.49.16.png

スクリーンショット 2019-12-15 18.52.35.png

GraphQL schema compiled successfullyのメッセージが出たら、Graphqlでのapi実行に必要なファイル等が自動生成されます。そのあと、schemaファイルに変更をかけたいならば自分で編集します。今回は以下の形式のスキーマにします。

type Todo @model {
  id: ID!
  description: String
  isDone: Boolean
}

次に以下コマンドでデプロイをします。

amplify push

ここでも以下のようにインタラクティブに質問を進めていきます。

スクリーンショット 2019-12-15 15.40.57.png

デプロイが完了すると、バックエンドのリソースが自動生成されます。

Next.jsとamplifyの結合

必要なバックエンドリソースが揃ったら、いよいよNext.jsの実装に入っていきます。
まずは必要なnpmモジュールをインストールします。

npm install --save @aws-amplify/api @aws-amplify/core @aws-amplify/pubsub

次にNext.jsのセットアップの時に作ったpages/todo.tsxを編集します。

基本的にamplifyモジュールのインポートとconfigの処理をかけます。amplifyの処理を書きたいときは基本的にpagesで以下の処理をかけておきます。

pages/todo.tsx
import Amplify from '@aws-amplify/core';
import PubSub from '@aws-amplify/pubsub';
import API, { graphqlOperation } from '@aws-amplify/api';

import awsmobile from '../aws-exports';

Amplify.configure(awsmobile);
API.configure(awsmobile);
PubSub.configure(awsmobile);

次に使用したバックエンドapiを使えるようにします。graphqlで作っているので、そこからquery、mutations, subscriptionsをimportします。ここでは全てインポートしてますが、実際には使う分だけで大丈夫です。

pages/todo.tsx
import { createTodo, deleteTodo, updateTodo } from '../graphql/mutations';
import { getTodo, listTodos } from '../graphql/queries';
import { onCreateTodo, onUpdateTodo, onDeleteTodo } from '../graphql/subscriptions';

少しだけ説明すると、QueryとMutaionsは従来のCRUDに対応させると以下のようになります。

昨日 CRUD graphql
作成 CREATE Mutation
取得 READ Query
更新 UPDATE Mutation
削除 DELETE Mutation

加えてgraphqlではSubscription(購読)というものがあります。これは、端的にいうとサーバー側からのPushのようなものです。バックエンド側のデータに変更がかかった場合などに検知をして値を知らせてくれるものです。

Queryの使い方

query.ts
export const getTodo = `query GetTodo($id: ID!) {
  getTodo(id: $id) {
    id
    description
    isDone
  }
}
`;

importしたlistTodosのクエリを以下のようにAPI.graphql(graphqlOperation)を使ってアクセスします。通信が成功するとdataに取得した値が入ってくるのでそれをpropsで渡したり、リストレンダリング用のローカルステートに渡してあげるとReact側でレンダリングすることが出来ます。

Todo.getInitialProps = async (props) => {
  const data = await API.graphql(graphqlOperation(listTodos));
  return {...props, ...data};
};

Mutationsの使い方

mutations.ts
export const createTodo = `mutation CreateTodo($input: CreateTodoInput!) {
  createTodo(input: $input) {
    id
    description
    isDone
  }
}
`;

こちらは登録用のファンクションです。API.graphql(graphqlOperation(createTodo, inputData));にて第一引数にMutatio、で第二引数で登録するデータを入れます。

pages/todo.ts
const submitTodo = async (list: Array<string>, todo: string) => {
  const id = Math.floor(Math.random() * Math.floor(1000))
    const inputData = {
    input: {
      id,
      description: todo,
      isDone: false
    }
  }

    try {
    await API.graphql(graphqlOperation(createTodo, inputData));
  } catch (e) {
    console.log(e);
  }
};

Subscriptionの使い方

subscription.ts
export const onCreateTodo = `subscription OnCreateTodo {
  onCreateTodo {
    id
    description
    isDone
  }
}
`;
pages/todo.tsx
API.graphql(graphqlOperation(onCreateTodo)).subscribe({
    next: e => {
        // 購読する値の取得
        const todo = e.value.data.onCreateTodo

        // 値をセットする処理を書く
        ...
    }
})

Todoアプリの実装

以下に今回の実装例を載せておきます。

pages/todo.tsx
import * as React from "react";
import { useState } from 'react';
import Amplify from '@aws-amplify/core';
import PubSub from '@aws-amplify/pubsub';
import API, { graphqlOperation } from '@aws-amplify/api';

import awsmobile from '../aws-exports';
import {
  createTodo,
  deleteTodo,
  updateTodo
} from '../graphql/mutations';
import { getTodo, listTodos } from '../graphql/queries';
import { onCreateTodo, onUpdateTodo, onDeleteTodo } from '../graphql/subscriptions';

import Head from '../components/templates/head'
import Navigation from '../components/templates/navigation'

Amplify.configure(awsmobile);
API.configure(awsmobile);
PubSub.configure(awsmobile);

interface TodoType {
  id: number,
  description: string
  isDone: boolean
}

interface DataProp {
  data: {
    listTodos?: {
      items: Array<TodoType>
    }
  }
}

const Todo = (props: DataProp) => {
  const { items: todoItems } = props.data.listTodos;

  const [todo, setTodo] = useState('');
  const [list, setList] = useState([]);

  // 新規追加でTodoを追加する
  const submitTodo = async (list: Array<string>, todo: string) => {
    const id = Math.floor(Math.random() * Math.floor(1000))
    const inputData = {
      input: {
        id,
        description: todo,
        isDone: false
      }
    }
    try {
      await API.graphql(graphqlOperation(createTodo, inputData));
    } catch (e) {
      console.log(e);
    }
  };

  // 既存のTodoを削除する
  const deleteItem = async (id) => {

    const deleteData = {
      input: {
        id
      }
    }

    try {
      await API.graphql(graphqlOperation(deleteTodo, deleteData));
    } catch (e) {
      console.log(e);
    }
  };

  return (
    <div>
      <Head title="todo" />
      <Navigation />
      <h2>Todo with amplify</h2>
      <input style={{
        border: 'solid 1px #ddd',
        padding: 10,
        borderRadius: 4,
        fontSize: 18,
        WebkitAppearance: 'none',
        color: '#333'
      }} value={todo} type="text" placeholder="please write todo" onChange={e => setTodo(e.target.value)} />
      <button style={{
        padding: 10,
        background: '#F06292',
        color: '#eee',
        borderRadius: 4,
        fontSize: 18,
        WebkitAppearance: 'none'
      }} onClick={() => submitTodo(list, todo)}>add Todo</button>
      <ul className="ListContainer">{
        todoItems.map( item => (
          <li key={item.id} className="ListItem">
            <span className="title">{item.description}</span>
            <span>{item.isDone}</span>
            <input type="button" value="delete" onClick={() => deleteItem(item.id)} />
          </li>
        ))
      }</ul>
    </div>
  )
};

Todo.getInitialProps = async (props) => {

  const data = await API.graphql(graphqlOperation(listTodos));

  try {
    const client = API.graphql(graphqlOperation(onCreateTodo));
    if ("subscribe" in client) {
      client.subscribe({
        next: e => {
          console.log(e);
        }
      });
    }
  } catch (e) {
    console.error(e);
  }

  return {...props, ...data};
};

export default Todo;

試しにあらかじめ以下のようにデータをセットしておきます。(このデータ自体はGraphqlでポストしておいたデータになります。)
スクリーンショット 2019-12-15 20.52.26.png

その状態で、npm run devでサーバーを起動して/todoにアクセスしてみてTodoが表示されていれば成功です。

スクリーンショット 2019-12-15 20.50.52.png

まとめ&感想

ここまで最後ま目を通していただいてありがとうございます。

巷ではサーバーレスWebアプリケーション開発はおそらく非常に人気で、Nuxt.js + Firebaseの情報が非常に多く見受けられます。非常に便利で僕も好きな技術の一つですが、逆にNext.jsがあまり無くバージョンアップにより魅力的な昨日が増えてきています。また、React製なのでVueに比べると 壊しやすいと感じていまして、それでいうとNext.jsもかなりアリかなと思っております。
AWS Amplify自体も元は一つ一つのAWSサービスから成り立っているため使いやすさだけでなくスケーリングやSLAの面でも非常におすすめかと思います。

しかしながらどの技術選定においても、仮説検証を高速に回し、フィードバックをUXに還元することが大事かなと思っています。その選択肢の一つとして非常に魅力的なので今後少しづつ使っていければと思います。

参考

https://aws-amplify.github.io/docs/js/api
https://qiita.com/G-awa/items/a5b2cc7017b1eceeb002

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React hooksが何故うれしいのか、Reactの今までを含めて解説

はじめに

アルプ株式会社でフロントエンドをしているmura-と申します。
弊社サービスのSaaSを管理するtoBむけプラットフォーム ScalebaseではReactを使ってフロントを構築しており、最近話題になっているhooksも導入しています。
昨年から発表はありましたが、今年の2月に、正式にhooksが利用できるようになったReact v16.8.0がリリースされました。

hooksはReact界にとってかなり良いインパクトを残し、hooksとはなにか、どう便利なのかという記事や話題をたくさん目にするようになったと思います。
hooksを導入することのメリット自体は調べればたくさんでてきますが、この記事ではReactの今まで提供されたコンポーネント変遷を交えて、最終的になぜhooksが導入されもてはやされるようになったかという視点で説明していきます。

TL;DR

忙しい方は 今までの問題点とhooksが解決すること をご覧ください

Reactの変遷

基本的なComponentの定義

hooksを駆使したものを除き、Componentの定義には createClass()React.Component を継承したClassでの定義があります。

React.createClassを使った、React初期のComponentの定義

Class構文に対応している他の言語であれば、Component指向にしようとするとClassで表現するのを思いつきますが、当時Class構文に対応したES2015がまだ正式にリリースされてなかったのもあり、React.createClass() でClass構文を使わずComponentを作成するメソッドが用意されていました。

See the Pen Use React Create Class by Kazuki Murahama (@mura-the-looper) on CodePen.

createClassによる定義でも今と同じようにライフサイクルメソッドやStateの初期値、渡すPropsなどを引数のオブジェクトに定義する必要がありました。

当時はVisual Studio CodeがなくTypeScriptもサポートされてなかったですし、普通のエディタでカジュアルに触ると、長大なオブジェクトの定義でカンマのつけ忘れなどでsyntax errorを起こしてしまうも多くあったことを覚えています。

React.Componentを継承したClassBasedなコンポーネントの定義

Reactは、v0.13から正式にES2015のClass構文に対応しました。

React.ComponentをextendsしてClassを定義すると、そのClassのインスタンスメソッドとしてライフサイクルメソッドを定義できます。creactClassと同じく各インスタンスは render() が必ず実行されます。

See the Pen Use React Class based component by Kazuki Murahama (@mura-the-looper) on CodePen.

Classですので、constractorになにか処理をさせたり、Stateをインスタンス変数として定義できます。

定義において基本的には、React.createClassでやっていたことがClassで書けるようになったことにより、よりComponentらしく書けるようになりました。

※ライフサイクルメソッドはv16.3から大きく変更しています。
参考: React v16.3 changes

パフォーマンスを考慮したコンポーネントの定義

React.Component だけだとシンプルに書きたい場合やパフォーマンスを考慮したときに難点がでてきました。それを解消するためのStateless Functional Componentと React.PureComponent が用意されました。

関数で定義するStateless functinal compnentの定義の仕方

Classを使って書けるのは開発者としては嬉しいですが、Stateすら持たず、ただ render() だけしたいだけのComponentなど、通常のClassによる定義がオーバースペックになる場合がありました。

そこでシンプルでカジュアルに定義できるStateless functional component (SFC)がv0.14で用意されました。

See the Pen StatelessFunctinalComponent by Kazuki Murahama (@mura-the-looper) on CodePen.

JSXをreturnする関数を書けばいいだけなのでだいぶ見通しもいいです。メモリを確保する必要がないことと、React自体がSFCのパフォーマンスを最適化させているのでパフォーマンス的にも向上がはかれます。( 45%速くなった事例もあるようです。)

ただし、個人的には大きなデメリットがあり、SFCを定義したあとでStateをもたせたくなった場合、通常のClassに書き換えざるを得ず大変でした。だったら、最初から通常のComponentを定義しよう、と思っている人もいたのではないかと思います。

React.PureComponentを使ったコンポーネントの定義

Reactでは親のコンポーネントのStateが変わったときなど、その下に含まれる子コンポーネントまで再描画されます。仮に子コンポーネントに渡しているPropsに差分がなくても、再描画してしまうので、子コンポーネントで難しい計算などをしていた場合パフォーマンスに影響がでたりします。shouldComponentUpdate() を使うことでコンポーネントを再描画しないようフラグをたてることができますが、毎回それを書くのは手間ですし忘れます。

v15.3でリリースされた React.PureComponent は、基本的には React.Component と変わらないのですが、自動的に shouldComponentUpdate() を実行して必要以上に再描画しないよう、取り計らってくれます。(ただし、Shallowな比較なのでObjectやArrayのPropsを渡している場合は注意が必要です)

hooksが登場するまでに使われていたComponentと用途のまとめ

諸々経緯があり、いろんなComponentが用意されていますが、hooksが登場するv16.8以前は、通常は下記のような用途で使われていました

Componentの種類 用途 備考
React.createClass()による定義 ES2015などモダンな環境でない場合に使われる。そうでなければ通常使わない。 最近はReactDOMのようにReact本体からも切り離されている
React.Componentを継承したClass Stateをもったり、親Componentとして使われる。通常使われるComponent。
Stateless Functional Component (関数Component) Stateを持たない、難しいことをさせたりしないテンプレートのような用途で使われる。
React.PureComponentを継承したClass Stateをもち、子コンポーネントなどで複雑な計算などをする場合使われる。 v16.6で追加された React.memo() を使えば, Stateさえ持たなければSFCでも役割を担うことができるようになった。もちろん通常のComponentで shouldComponentUpdate() を定義すれば同等なことができるのでプロジェクト次第では使われてないかもしれません。

Componentに共通する振る舞いを与える

ここまでComponent自体について紹介しましたが、定義したComponentに対して、ロジックが一元化された任意の振る舞いやデータを与えたいケースもあります。Componentに任意の振る舞いを与えるために、どうしてきたかということも紹介します。

React.createClass() にのみ存在したMixin

Mixinはかなり便利な機能でした。reactのライフサイクルメソッドとして、componentDidMountcomoponentDidUpdate などがありますが、任意のComponentに好きに振る舞いを注入させることができました。

React.jsのmixinについて

Highr-order Componentを使って振る舞いを与える

ClassBasedなReactではMixinがサポートされなくなりました。Mixinは問題があり ますし、公式でもHigher−order Componentを使えとアナウンスされてました。

Higher-order Component(HoC)自体はReact自体の機能ではなく、あるComponentにPropsや機能などを渡して返すためのイディオムです。Redux Reactの connect() など、ライブラリが作るデータや機能を、Componentに渡すときなどによく使われる手法です。

// react reduxのconnectを使ってReduxのStateやPropsを渡す
connect()(MyComponent)
connect(mapState)(MyComponent)
connect(
  mapState,
  null,
  mergeProps,
  options
)(MyComponent)

See the Pen abzZQrg by Kazuki Murahama (@mura-the-looper) on CodePen.

HoCに関する詳細は下記をごらんください。

高階(Higher-Order)コンポーネント

render propを使って振る舞いを与える

Reactが提供する render prop というものを使って振る舞いを与えることもできます。
render propとHoC、好きな方法を使うことができますが、HoCは名前衝突したり記述が煩雑になったりしますし、型の定義が大変ですのでrender propの方が書きやすいとは思います。

See the Pen RenderPropsComponent by Kazuki Murahama (@mura-the-looper) on CodePen.

hooksを使ったComponentの定義と、今までの問題点とhooksが解決すること

ここまで、長くなってしまいましたが今まで紹介したことを踏まえて、hooksを比較してみます。

hooksで定義したComponent

hooksを使うと、下記のようにComponentが定義できます。

See the Pen React Functional Component by Kazuki Murahama (@mura-the-looper) on CodePen.

まず、大きな点として、React.Componentを使ってClassとして定義しなくて良くなった点が大きいです。

Stateless functinal component(SFC)について先程紹介しましたが、Functional Componentなのに状態を持つことができるようになった、と言うことができます。ここでは仕組みについて言及はしませんが、reactは useState などのAPIを提供したおかげで、関数コンポーネントでStateを扱うことができるようになりました。

また、今まで componentDidMount などで扱っていたであろう、コンポーネントに対する副作用も useEffect などを使って表現できます。

ただそれだけ、といえばそれだけなのですが、今までのReactの問題点を考えるとかなりインパクトがあることがわかります。

今までの問題点とhooksが解決すること

  • 今までのReactの問題点
    • どのコンポーネントを使っていくか都度考えないといけない。
      • SFCを定義したとしてもあとからStateをもたせたくなってClassに書き換える手間がある。
      • PureComponentを使ったとしても結局最適化できない場合があり shouldComponentUpdate() を定義する手間が発生する。など
    • ライフサイクルメソッドの複雑・煩雑さ
      • 一番最初に動作するライフサイクルメソッドはどれか、どのライフサイクルメソッドで副作用がおきるか、関連するロジックであってもライフサイクルごとに定義しないといけないので、いろんなロジックが混ざるとかなり複雑になりバグを生む。
    • Wrapper Hell
      • HoC, render propなどを駆使したとて結局、Componentのネストが深くなってしまい可読性がおちる

※上記は主観もふくまれますので hooksが実装された動機もごらんください

  • hooksが解決すること
    • Componentは基本的にすべてFunctional Componentを使って定義してよい
    • ライフサイクルメソッドから開放され、ロジックはreturnするJSXの前にすべて表現できる そのおかげで下記のメリットがある
      • コードは上から下に実行され、宣言的に書ける
      • ロジック部分を明確にわけることができ、一元的に扱える
      • 状態を直接扱えるため、HoCやrender propを使う必要がなく、Wrapper hellを避けることができる
        • ライブラリもhooksに対応していればそのライブラリが提供する値や関数を直接扱える

先に紹介したReact Reduxも hooksを提供しており connect() を使う必要がなく下記のように扱えます。

import React from 'react'
import { useSelector } from 'react-redux'

export const CounterComponent = () => {
  const counter = useSelector(state => state.counter)
  return <div>{counter}</div>
}

さらなるhooksについての概要はこちらをごらんください
フック早わかり

hooksの問題点

銀の弾丸は存在しませんが、やはりhooksも同様です。下記のような問題点があります

  • Stateが変更されたとき、従来と同じくComponentは再描画される。上から下に実行されるがゆえに、値や関数を必要に応じてメモ化する必要がある。メモ化をうまく扱えないと無限ループを起こしてしまう可能性もある。
  • hooksは制限があり、いつでもどこでも動かせるわけではない。例えばif文のなかで useEffect() は実行できない。
  • メモ化などを含め、新たな考え方が必要になるため、hooksは難しいと感じるケースもある

注意

Context APIなどここでは紹介しきれてないこともたくさんありますがご容赦ください。

おわりに

個人的にも今までComponentの設計やHoC、render prop、Wrapper Hellなど様々なことに悩まされてきましたが、hooksを使うことでかなり見通しのよいComponentを定義できるようになったと実感しています。すべてのライブラリがhooksを提供するわけではありませんが、よく使われている主要なライブラリもhooksを提供しはじめてます。hooksを使うにはReactのバージョンアップしないといけないなどの手間もあると思いますが、それを差し引いてもメリットが大きいのでまだ導入してない方はぜひ使ってみてください。
なにか間違ってる箇所とかありましたらツッコミいただけますと幸いです!※特にサンプルコードは作り込む時間ががが

弊社のサービスScalebaseでも、絶賛hooksに書き換え中です。興味ある方、お手伝いいただける方はぜひご連絡ください!技術に関するお話だけでも大歓迎です。
twitter: https://twitter.com/mura_cx

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初めてReact使ってみました。

めちゃくちゃわかりやすい記事を見つけました→こちら
この記事は上記サイトを読んだ後、復習or辞書的な役割で使っていこうと思います。

知らない単語一覧

React

そもそもですが。jsみたいにデータを表示するもの。詳しく

DOM

「Document Object Model」の略。プログラムからHTMLやXMLを自由に操作するための仕組み。

CDN

「Content Delivery Network」の略。同一のコンテンツを、 多くの配布先、多くのユーザーの端末に効率的に配布するために使われる仕組み。

babel

新しい機能を使って書いた JavaScript のコードを、以前のバージョンの書き方に変換してくれるツール。

jsx

JavaScriptに変換される拡張子

defer

スクリプトの読み込みを非同期で読み込む方法の一つ.
deferにするとscriptが書かれた順に実行される。また、確実にDOM操作ができる。詳しく

コンポーネント

ReactではUI表示部品の事を言う。コンポーネントは最終的にHTML要素として表示される。
コンポーネントはjsのclassや関数で下記のように定義される。

main(冒頭サイトのコピペです).jsx
function App() {
  return <div>Hello React!</div>;
}

const target = document.querySelector('#app');
ReactDOM.render(<App/>, target);

document.querySelector

JavaScriptから任意のHTML要素を検出・取得することができるメソッド。
この例ではid="app"のものを取得している。

ReactDOM.render

1つ目の引数に出力する出力するコンポーネント、2つ目に出力先のelementを指定。

関数コンポーネントについて

Tweet(冒頭サイトのコピペ).jsx
function Tweet(props) {
  return <div>{props.content}</div>;
}

関数コンポーネントは引数をひとつ受け取る。
引数をpropsとして受け取り、その中身を表示する。
htmlの自作タグを作成しているイメージ?
別ファイルでTweetタグを使ってfunction Tweetを呼び出せる。

Hooks

冒頭記事ではいいね機能実装のために説明されていた。
関数に状態を保持させるためにReactに備えられた解決策。
コンポーネントごとに専用の変数ストレージができるような機能。

const [value, setValue] = React.useState('デフォルト値');
setValue('新しい値!');

関数の中にこれをつけるだけらしいです。

コールバック関数

関数に渡される関数
(onClick時の挙動に関しては後日記載します)

下記コードからわからない部分をまとめる

これもパクリ.jsx
function TweetInput(props) {
  // 要素にアクセスするための参照を取得
  // ref.currentのデフォルト値はnullにしておく
  // 参照したい要素にJSXの方でこのrefをセットする
  const textareaRef = React.useRef(null);

  // Tweetボタンクリック時のコールバック関数を作って保存
  // textareaRef.currentかprops.addTweetが更新されたら
  // コールバック関数を作り直し
  const sendTweet = React.useCallback(() => {
    // refのcurrentに入っている値がDOM
    // 初期値はnull(上で指定した)で、
    // 実際にHTML側に描画されると値が入ります
    if(textareaRef.current) {
      props.addTweet({
        id: new Date().getTime(), // IDはユニークな値にする
        icon: '☠️', // このあたりの値は好きにしてください
        displayName: 'ミスター死',
        accountName: 'mrdeath',
        content: textareaRef.current.value
      });
    }
  }, [textareaRef.current, props.addTweet]);

  return (
    <div>
      <div><textarea className="tweet-textarea" ref={textareaRef}></textarea></div>
      <div><button onClick={sendTweet} className="send-tweet">Tweet</button></div>
    </div>
  );
}

React.useRef

書き換え可能な値を .current プロパティ内に保持することができる「箱」のようなもの

React.useCallback

メモ化されたコールバックを返す。メモ化はキャッシュみたいなもの。

冒頭記事の大まかな処理の流れ

それぞれのファイルの役割

index.htmlでjsxファイルを読み込む、読み込んだ順に処理していく。
Tweet.jsxでTweet関数を定義し、与えられた内容を基にTweetを作成。
Timeline.jsxでtweetが格納された配列を取得し、keyを基に一つずつ表示していく
TweetInput.jsxでtweetを取得

流れ

main.jsxで、既存のツイートを表示したのち、addTweet関数を呼び出し新しいツイートを取得。その際にinputフォーマットを表示、入力されたものをTweetInput.jsxで配列に格納。Timeline.jsxでTweet.jsxを呼び出し値を表示していく。

こんな感じだと思いました。

おまけ

おまけと言いつつ自分にとっての本題です。せっかくなので孤独った―の一部を変えたり追加機能つけたりし、理解してるかな?の確認をしていこうと思います。

リツイート機能を付けてみた

いいね機能をそのまま持ってきて作成できました。

Tweet.jsx
function Tweet(props) {
  // Reactからこのコンポーネントの
  // like値と、likeの値をセットするための関数を取り出す
  // デフォルト値はfalseにする
  const [liked, setLike] = React.useState(false);
  const toggleLike = React.useCallback(() => setLike((prev) => !prev), [setLike]);
  // おまけ!!リツイート機能を付けてみた
  const [retweeted, setRetweet] = React.useState(false);
  const toggleRetweet = React.useCallback(() => setRetweet((prev) => !prev), [setRetweet]);


  return (
    <div className="tweet">
      <div className="icon-container">{props.icon}</div>
      <div className="body-container">
        <div className="status-display">
          <span className="display-name">{props.displayName}</span>
          <span className="account-name">@{props.accountName}</span>
        </div>
        <div className="content">{props.content}</div>
        <div className="status-action">
          <span onClick={toggleLike}>{liked ? '❤️' : ''}</span>
        </div>
        <div className="status-action">
          <span onClick={toggleRetweet}>{retweeted ? '●️' : ''}</span>
        </div>
      </div>
    </div>
  );
}

ちょっと簡単すぎましたね。続きはまた今度

追記

Reactってどんなものなの?ってところから入りましたが、どのサイト見ても「いろいろ入れてね」から始まるのが面倒なので、冒頭のサイトをなぞってみました。

よかったこと

・reactは何ができるのか、どう動くのか、という浅い部分はふんわりわかってきたと思います。
・面倒なことが一切なく、すぐに表示されたのでやってて楽しかったです。

よくなかったこと

・記事で書かれていましたが、実践的ではないようですね。実務経験ないから知らんけどって感じでしたが。
・Reactだけで実装しているサイトがほかになかったので、機能追加しようとしたときにこの環境でできることなのかどうか判断が出来ませんでした(フロントエンドょゎょゎな自分のせいですが)。

終わりに

めちゃくちゃ楽しかったです。バックエンドでやっていた処理がフロントでできるのめちゃすごいですね(コナミ)。
なんとなくいじれたので、次は実際の現場で使われるような環境を整えて再チャレンジしたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React-Draggableで<Draggable>内のインプットから勝手にフォーカスが外れる問題

React-Draggableを使ったコンポーネントでテキスト入力ができなくなる問題が発生したので対処法をメモ

環境

Chrome 78.0
React 16.8.6
React-Draggable 4.1.0

起きた問題

タイトルのまんまですが

<Draggable>
  <div>
    <input type="text" />
  </div>
</Draggable>

上記のコンポーネントで、inputに文字を入力しようとしてフォーカスを当てても瞬時にフォーカスが外れて文字が入力できない問題が発生。

解決方法

https://github.com/mzabriskie/react-draggable/issues/314
こちらのissueに解決方法がありました。

Draggableにcancelというプロパティでdraggableの対象外にする要素のセレクタを渡せばいける。

<Draggable cancel="input[type=text]">
  ...
</Draggable>

どうやらドラッグ中に意図せぬテキストの選択が発生しないようにフォーカスをすぐに外す処理が入っているのが原因のようです。
なのでテキスト入力部分だけはdraggableの適用対象外とすることで解決できます。

過去には
enableUserSelectHack={false}というプロパティを渡す必要があったようですが、
現在のバージョン(4.1.0)ではcancelだけでいけるようです。

参考

https://github.com/mzabriskie/react-draggable/issues/314

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npm install -g create-react-appでcheckPermissions が出た時の対処法

状況

macで、udemyの動画でreactを勉強するために
npm install -g create-react-app
で最新版をインストールしようとしたところ、

npm WARN checkPermissions Missing write access to /Users/tagawahirotaka/.npm-global/lib/node_modules/create-react-app

と怒られた。

解決できなかった策

npmでpermission deniedになった時の対処法[mac]
を参考にして、pathの設定をしたりbrewでnodeをインストールしたりしても同じエラーがでた。

解決策

エラー文に

npm ERR! path /Users/tagawahirotaka/.npm-global/lib/node_modules/create-react-app
enoent ENOENT: no such file or directory, access '/Users/tagawahirotaka/.npm-global/lib/node_modules/create-react-app'

とあったので、なんだかよく分からないが、
/Users/tagawahirotaka/.npm-global/lib/node_modules/create-react-app
にあったcreate-react-appを削除した。

その後、npm install -g create-react-appをしたら、
/Users/tagawahirotaka/.npm-global/bin/create-react-app
ができて無事に最新版がインストールできた。

が、しかし

npm startをしたところ、

A template was not provided. This is likely because you're using an outdated version of create-react-app.
Please note that global installs of create-react-app are no longer supported.

というエラーに遭遇。

ひさしぶりにcreate-react-appしたらテンプレートができなかった時の対処法
この記事を参考にし、
npm uninstall -g create-react-app
yarn create react-app my-app
yarn start
でいけた。

が、しかし

There might be a problem with the project dependency tree.
It is likely not a bug in Create React App, but something you need to fix locally.

というエラーに遭遇。
webpackがプロジェクトフォルダーではないところにインストールされてしまっているのが原因らしいので、
/Users/tagawahirotaka/node_modules/webpack を削除。
その後、yarn startでいけた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React 俺のエラーコレクション

れっつごー

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactくんのエラーコレクション

開発環境

Xubuntu
vscode
eslint
prettier

prettierいれてるのにeslintrc未設定なのは豚に真珠

pm install eslint eslint-config-prettier eslint-plugin-prettier -D

これらのツールをいれるとprettierがやったことにeslintは口を出さなくなる(警告しなくなる!)
https://qiita.com/soarflat/items/06377f3b96964964a65d
知らずに悪戦苦闘しながらルール追加していた(約2日間)

eslint: cannot find module 'prettier' require stack.

prettierがローカルに入ってないと怒られます。

npm install --save-dev prettier

[eslint] is not defined

.eslintrc.js
"env": {
  node: true,
  commonjs: true,
  browser: true,
  es6: true
}

[eslint] eslint-disable react/prop-types

警告を消す

.eslintrc.js
rules: {
  "react/prop-types": 0
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

僕のP2HACKS譚

はじまり

 12月7日~15日に開催される「P2HACKS」に参加してきました!当初に予定していたRailsではなく全く触ったことのない「React」で製作しました。というより、していますのほうが正しいんですが。しかし、全く触ったことのないものでアプリ製作を行って生じる苦労が1つや2つで収まるわけがないんです。

 とういうわけで、アプリ製作中に僕が学んだことや苦労したことのいくつかを書いていこうと思います。注意:書いてることの正確さは保証しません。

1index.htmlをいじらない

 まずそこからかよって思う人もいると思いますが、そこからなんです。HTMLとCSSは少しわかるがJavaScriptはあんまりわからない、という状況で僕はまず初めに脳死でindex.htmlをいじり始めました。
 Reactは基本的にindex.htmlをいじりません。App.js以下をいじっていれば何か変なことをしない限り勝手にindex.htmlを書き換えてくれます。少しだけ具体的に説明します。

index.htmlでは「root」という名前のidを読み込むことになっています。下記の画像は実際のindex.htmlのコードの一部です。
accc1131ed4d6eb22e7f23ed40b62025.png

次にindex.jsですが、「ReactDOM.render(, document.getElementById('root'));」の部分でAppの部分を編集すれば'root'という名前のidの場所でhtmlを書き換えてくれます。さらに、上の方にある「import App from './App'」でApp(fromの後)という名前でexport defaultされたモジュールをApp(fromの前)という名前で受け取ってます。
下記の画像はindex.jsのコードです
97efcb8bad6974329bdc827ca4d3ea67.png

最後にApp.jsです。ここで直接HTMLを操作したりするコードを書きます。下記の画像を見てください。これはApp.jsのコードです。一番下の「export default App」でこのファイルのデフォルトとしてAppというものをexportするよって意味です。だからfunction App(){ ~ }の中を書き書きすれば動きます。

挿入ミス

2jQueryとReactを併用するな

 jQueryとReactを併用してはいけない。理由はjQueryは本物のDOMをいじるがReactは仮想のDOMを使ってコンポーネントを描画するから処理がコンフリクトするらしい。...正直、説明聞いてもよくわからなかったので「併用してはいけない」という知見しか僕は得ることが出来なかった。それとjQueryを使うと問答無用で猫先輩に怒られる。下記は併用してはいけないことを知らずに僕がどや顔で書いたコードです。やりたかったことは画像をクリックしたとき、それが何の画像でも、src属性の値を持ってくるということです。それ自体は成功しましたが、当然没になりました。
ea0ee1d5737ae6f91a1721b130819e71.png

3flexboxについて(css)

 実装した機能の中に、HTMLでulとliを使って画像をたくさん表示するものがありました。しかし、何もしないままだと画像が縦一列に並んでしまうのでとても見にくいUIになってしまいます。
 そこで利用したものが「flexbox」というレイアウトモジュールです。flexboxでは要素を縦や横に柔軟に配置するためのものらしいです。今回やったことは画像をたくさん表示するときに横に3つずつ並べて居れていくことです。4つ目は次の行の一番左に…って感じです。要素を左から右に、入りきらなくなったら下に行を追加していく、ということを実装するには親要素に「flex-wrap:wrap;」を加えてあげればできます。その後、配置する要素の大きさを(100% /n)の大きさにしてあげれば、横一列にn個の要素が入ります。下記は今回実際に実装したulに対して加えたクラスのコードです。
18aa8c2cf806150658ef79fb75ca145e.png

今回のオチ

 今回、自分が先輩に教わったり自分で調べたりしたりして解決した苦労の一部を書きました。画面遷移とかも書きたかったんですが、自分のコード見ても何で動いてるのか思い出せなかったので止めました(笑)。しかも、かなりあやふやな知識で書いてるのであんまりあてにしないでください。あぁ、こいつはこんなことしてたんだな、くらいの気持ちで読んでいてくれたら嬉しいです。(本当はP2HACKSと関係ないことを紹介したかったけど出来なかったなんて口が裂けても言えない。)
 これ書いてる時間がP2HACKS最終日の1時なんですが、まだ開発が終わっていない...。ということで僕は開発を再開しなければならないので今回はこの辺りで。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsを変な使い方してみた

この記事はNext.js Advent Calendar 2019 15日目の記事です。

はじめに

YoutubeLiveを録画する自分用のアプリをさくっと作りたかったのでまず技術選定をしました

候補
CLI(Node.js) GUIがない、スマホから操作できない
BFF + React めんどい
Next.js Node.js + GUI をさくっと作れる?

今回はNext.jsを選んでみました

完成品のソースコードは↓にあります
https://github.com/shinyoshiaki/youtube-downloader

Next.jsをサーバサイドElectron?と見立てて使う

Next.jsはgetInitialProps内がNode.jsのコンテキストで動いてElectron的な使い方ができそうだったので、getInitialProps内でytdl-coreというNode.js用のYoutubeの動画を録画するライブラリを使ってみました。

さっそくgetInitialProps内でytdl-coreを使っている箇所を見ていきましょう

pages/dl.tsx

import { NextPage } from "next";
import { Button } from "@material-ui/core";
import Link from "next/link";
import { download, Progress, finishDownload } from "../src/domain/youtube";

type Props = {
  status: Progress;
  id?: string;
};

const DlPage: NextPage<Props> = ({ id, status }) => {
  const renderStatus = () => {
    switch (status) {
      case "completed":
        return (
          <a href={`/static/${id}.mp4`} download>
            Click to download
          </a>
        );
      case "downloading":
        return (
          <div>
            <p>downloading</p>
            <a href={`/dl?id=${id}&finish=true`}>
              <Button>finish</Button>
            </a>
          </div>
        );
      case "fail":
        return <p>fail</p>;
    }
  };

  return (
    <div>
      <Link href={`/`}>
        <Button>Home</Button>
      </Link>
      {renderStatus()}
    </div>
  );
};

DlPage.getInitialProps = async ({ query }) => {
  let { id, finish } = query as any;

  if (finish && id) {
    await finishDownload(id);
    return { status: "completed", id };
  }

  if (id) {
    try {
      const status = await download(id);
      return { status, id };
    } catch (error) {
      return { status: "fail" };
    }
  } else return { status: "fail" };
};

export default DlPage;

getInitialPropsでdownloadという関数を実行しています。この関数の中でytdl-coreを使っています。download関数の中を見てみましょう

src/domain/youtube.ts

import fs from "fs";
const ytdl = require("ytdl-core");

export const download = async (youtubeId: string): Promise<Progress> => {
  const BASE_PATH = `https://www.youtube.com/watch?v=`;

  const url = BASE_PATH + youtubeId;

  const error = await new Promise<object | undefined>(r =>
    ytdl.getInfo(url, (err: object) => {
      if (!err) {
        r(undefined);
      } else r(err);
    })
  );

  if (error) {
    console.warn("error", error);
    return "fail";
  }

  return await new Promise<Progress>(async r => {
    try {
      const lock = `${youtubeId}.dl`;
      const file = `${youtubeId}.mp4`;

      if (!fs.existsSync(lock)) {
        if (fs.existsSync(`static/${file}`)) {
          if (fs.existsSync(file)) fs.unlinkSync(file);
          r("completed");
          return;
        }

        ytdl(url)
          .on("response", () => {
            fs.rename(file, `static/${file}`, () => {
              fs.unlinkSync(lock);
              r("completed");
            });
          })
          .on("data", (data: Buffer) => {
            console.log("ondata", data.length);
            fs.writeFileSync(lock, "");
          })
          .pipe(fs.createWriteStream(file));

        setTimeout(() => {
          r("downloading");
        }, 3000);
      } else {
        r("downloading");
      }
    } catch (error) {
      console.log({ error });
      r("fail");
    }
  });
};

がっつりfsをインポートしているのでこのコードは少なくともブラウザでは動作しませんし、そもそもそのままだとビルドすら通らないので、next.config.jsを少し書き換えます。

next.config.js

module.exports = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.node = {
        fs: "empty"
      };
    }
    return config;
  },
  routes: [{ src: "^/static/(.*)", dest: "/static/$1" }]
};

こうしてやるとビルドできます。

最後にDockerfileにまとめてサーバーにぶちこめば、スマホから操作をして、サーバー側で録画保存できるウェブサービスの完成です!

Dockerfile

FROM node:11.15.0-stretch AS build

RUN apt update &&\
    apt install git curl && \
    curl -o- -L https://yarnpkg.com/install.sh | sh

ENV PATH $HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH

RUN mkdir /next
WORKDIR /next
COPY . .
RUN yarn

EXPOSE 3000

CMD [ "yarn","serve" ]

あとがき

Next.jsも本来は、getInitialPropsで色々するんじゃなくてサーバーを建ててそっち側で色々してBFF風にするのが正しいのですが、一応今回のように自分用のウェブサービスをさくっと作りたい場合はgetInitialPropsで色々するのも選択肢になるんじゃないのかなと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reduxをenjoyableにする

この記事はフラーAdvent Calendar 2019の15日目の記事です。

はじめに

airpc という Redux Toolkit のようなReduxのロジック周りのコード量を減らすためのライブラリを作ってみたので、それについての記事を書いてみました。

素のRedux

素のReduxだとActionCreatorでActionを作って、ActionをReducerにくぐらせて状態を更新しています。
ActionCreatorとReducerの簡単な例を書いてみます

type State = {
  some: string;
};

const initialState: State = { some: "" };

export const actionCreator = ( data: string ) => ({
  type: "action" as const,
  payload: data
});

type Actions = ReturnType<typeof actionCreator>;

export default function reducer(
  state = initialState,
  action: Actions
): State {
  switch (action.type) {
    case "action": {
      return {
        ...state,
        some: action.payload
      };
    }
    default:
      return state;
  }
}

ただ、stateを操作したいだけなのに actionCreatorreducer のswitch文中のロジックの2通りを書かないといけません。面倒ですね。

enjoyableなRedux (by airpc)

そこでReduxのActionCreatorとReducerを一体化させて、もっと楽に書けるようにairpcを使ってReduxのロジックを書いてみましょう

import { withRedux } from "airpc";

type State = {  
  pinned: string;
};

const initialState: State = { some: "" };

const [actions, reducer] = withRedux(
  class Hogehoge {
    constructor(public state: State) {}

    someMethod = (pinned: string) => ({ ...this.state, pinned });
  },
  initialState
);

export default reducer;
export { actions };

ActionCreatorとReducerが消えた代わりに、HogehogeというクラスがwithReduxという関数の引数内で宣言されている奇妙なコードが出てきました。
state操作のロジックはHogehogeクラス内のメソッドとして書かれており、先程の素のReduxに比べるとコード全体がスッキリしたような印象があるんじゃないかと思います。
また、withRedux関数は、actionsとreducerの2つの値を返しています。
actionsはActionCreatorの集合体のようなものです。
reducerは普通のreducerなので、combineReducerやcreateStoreなんかにそのまま食わせられます。

それではwithRedux関数についてもう少し詳しく見ていきましょう。

withRedux

withReduxの第1引数にはstateを操作するクラスを渡します。このクラスのことを便宜上ロジッククラスとでも呼びましょう。ロジッククラスは次のルールに従います。

ロジッククラス

項目 ルール
クラス名 自由
コンストラクタ 引数に public state:State のみを必ず取る
メソッド 任意の長さの引数を取れる。必ず、State型のオブジェクトを返す
プロパティ プロパティを持つことは禁止されている

withReduxの第2引数にははstateの初期状態を渡します。

withRedux関数内部ではロジッククラスを元にロジッククラスと同じメソッドを持ったクラスのインスタンスを生成し、それをactionsとして返しています。ですので、このactionsをactionCreatorとして使う際には以下のようにdispatch関数内でactionsのメソッドを実行すればOKです

import {actions} from '../modules/hogehoge'

dispatch(actions.someMethod("hi"));

簡単ですね!

どうやって実現しているのか?

airpcのソースコードを覗いてみましょう

src/redux.ts

import { ActionCreator, ValidState } from "./typings/action";

class WrapRedux {
  constructor(target: any) {
    const subscribe = (type: string) => {
      const actionType = target.name + "_" + type;
      (this as any)[type] = (...args: any[]) => ({ type: actionType, args });
    };
    Object.keys(new target()).forEach(type => {
      if (type === "state") return;
      subscribe(type);
    });
    Object.getOwnPropertyNames(target.prototype).forEach(type => {
      if (type === "constructor") return;
      subscribe(type);
    });
  }
}

function exposeRedux<T extends any>(instance: T) {
  const update = (state: any, v: { type: string; args: any }): T["state"] => {
    const { type, args } = v;
    const [name, method] = type.split("_");
    if (instance.constructor.name !== name) return state;

    if (instance[method]) {
      instance.state = state;
      return { ...state, ...instance[method](...args) };
    } else {
      return state;
    }
  };
  return update;
}

export function withRedux<A extends any, B>(
  target: { new (state: B): A },
  initialState: B
): [
  Omit<ActionCreator<A, Required<B>>, "state">,
  (
    state: B | undefined,
    action: Omit<A[keyof A], "state">
  ) => ValidState<B, A["state"]>
] {
  const instance = new target(initialState);

  const methods = new WrapRedux(target) as any;
  const update = exposeRedux(instance);

  const reducer = (state = initialState, action: any) => update(state, action);

  return [methods, reducer];
}

airpcのwithRedux関数はWrapReduxとexposeReduxの2つのクラスと関数から成り立っています。

まずWrapReduxの方から見ていきましょう

WrapRedux

class WrapRedux {
  constructor(target: any) {
    const subscribe = (type: string) => {
      const actionType = target.name + "_" + type;
      (this as any)[type] = (...args: any[]) => ({ type: actionType, args });
    };
    Object.keys(new target()).forEach(type => {
      if (type === "state") return;
      subscribe(type);
    });
    Object.getOwnPropertyNames(target.prototype).forEach(type => {
      if (type === "constructor") return;
      subscribe(type);
    });
  }
}

WrapReduxでは要するに、ActionCreatorをクラスの情報から自動生成しています。
コンストラクタで引数にwithReduxの第1引数のクラスを受け取り、
引数のクラスの持つメソッドに対応するActionCreatorをWrapReduxクラスのメソッドとして生成しています。

次にexposeReduxを見ていきましょう

exposeRedux

function exposeRedux<T extends any>(instance: T) {
  const update = (state: any, v: { type: string; args: any }): T["state"] => {
    const { type, args } = v;
    const [name, method] = type.split("_");
    if (instance.constructor.name !== name) return state;

    if (instance[method]) {
      instance.state = state;
      return { ...state, ...instance[method](...args) };
    } else {
      return state;
    }
  };
  return update;
}

exposeReduxは要するに、dispatchされたActionに対応するクラスのメソッドを実行し、新しいstateの作成を行っています。
exposeReduxの引数はwithReduxの第1引数のクラスのインスタンスとなります。
exposeRedux内のupdate関数はreducerとなります。update関数では、dispatchされたactionの値に該当するクラスインスタンスのメソッドにactionの値を渡して実行し、戻り値を新しいstateとしています。

あとはwithRedux関数でWrapReduxとexposeReduxのセットアップをして、actionsとreducerを返しています。
ユーザはreducerを普通にcombineReducerなんかに食わせてactionsをdispatch関数内で実行すればOKです。

あとがき

今回作ったairpcは一応テストは書いているものの、誰のレビューも受けていない信頼性の低いライブラリなので、プロダクションでReduxのactionCreatorとReducerをどうにかしたい場合は普通にRedux Toolkit を使ったほうが良いと思います。

自分でこういったライブラリを作ってみるのもそれはそれで勉強になります。特にライブラリの型安全性を実現するためにConditionalTypesを駆使するあたりは結構勉強になりました!(本記事では触れてないけど...)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む