20191203のReactに関する記事は19件です。

関数コンポネントのライフサイクル使うhook

関数コンポーネントのライフサイクル

Reactの関数コンポーネントにも、mount / unmount の概念が存在します。

useState など (公式の) hooksの戻り値は、react内部の配列のような構造で管理・配分された値です。もし本当にunmountされた後で使うと、Reactのdebug buildは↓のように警告してくれます。production版は試していないが、もし問題になると、このようなミスを探すのが難しいと予想します。

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

async/await とリソース解放

無駄な計算とメモリリークを防止するために、コンポーネントによって配分された各種リソース (開始した非同期タスクなど) は、unmount以降早く解放すべき。

effectの中で割り当てられて、dispose() などAPIを設けてくれたリソースならいいが、async/awaitの処理フローはそうではなく、Promiseオブジェクトが生成された時点からcancelもpauseもできない。

さらに、1箇所にawait使うと、伝染のように以降の処理のタイミング (とリソース配分) もpromiseに依存するようになる。

async/awaitを使わないで、非同期処理のフローをuseEffect()に合わせることはできなくはないが、awaitの回数によってロジック上連続する処理を複数のuseEffectに分けるようになってしまうため、私はそう書きたくない。

あるコード例

こんなコンポーネント作ることとします:

  • あるボタンをクリックすると、signIn() API でログインして (このAPIはPromiseを返す)、返されたユーザ名をUIに表示する
  • ↑ signIn() Promiseがfulfillした時点からの秒数をUIに表示する

version 1

const SignInTimerV1: React.FC = () => {
  const [username, setUsername] = useState(null);
  const [timeSinceLogin, setTimeSinceLogin] = useState(NaN);

  const onClick = async () => {
    const user = await signIn();

    setUsername(user.name);
    const timer = setInterval(() => {
      setTimeSinceLogin(t => t+1000);
    }, 1000);
  };

  return (
    <div>
      <button onClick={onClick} > signIn() </button>
      <p>current user: { username } </p>
      <p>time since sign in: {timeSinceLogin} </p>
    </div>
  );
}

もしsignIn() がfulfillした時点で すでにunmountされてると、setUsernameを呼ぶべきではない。そして、unmountされるときtimerを解放しないとメモリリークになる。

こんなhooks書いた

export function useLifeCycle() {
  const l = useMemo(() => {
    const mounted /* boolean */ = false;
    const unmountCallback /* Function[] */ = [];
    const mountCallback /* Function[] */ = [];
    const unmounted /* Promise<void> */ = new Promise(fulfill => unmountCallback.push(fulfill));
    return {
      mounted,
      unmountCallback,
      mountCallback,
      unmounted,
    };
  }, []);

  useEffect(() => {
    l.mounted = true;
    for (const f of l.mountCallback) f();
    return () => {
      for (const f of l.unmountCallback) f();
      l.mounted = false;
    };
  }, []);

  return l;
}
  • 呼び出し元のコンポーネントのライフサイクル状態 (mounted) を返す
  • classコンポーネントのように、mount時点 (componentDidMount)、unmount時点 (componentWillUnmount) にコールバックを登録できる

version 2

const SignInTimerV2: React.FC = () => {
  const lifecycle = useLifeCycle();                               // changed
  const [username, setUsername] = useState(null);
  const [timeSinceLogin, setTimeSinceLogin] = useState(NaN);

  const onClick = async () => {
    const user = await signIn();

    if (!lifecycle.mounted) return;                               // changed
    setUsername(user.name);
    const timer = setInterval(() => {
      if (lifecycle.mounted) setTimeSinceLogin(t => t+1000);      // changed
    }, 1000);

    lifecycle.unmounted.then(() => clearInterval(timer));         // changed
  };

  return (
    <div>
      <button onClick={onClick} > signIn() </button>
      <p>current user: { username } </p>
      <p>time since sign in: {timeSinceLogin} </p>
    </div>
  );
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gatsbyの超高速テンプレート(PWA, MDX, TypeScript対応)を作ったヨ!

Gatsby.jsって?

この記事はGatsby.js Advent Calendar 2019 3日目の記事です。

実は1日目も記事を書いてます(Reactの最強フレームワークGatsby.jsの良さを伝えたい!!)。中1日です。高野連に怒られるやつです。カレンダーが空いていたので記事を書きます。

よければGatsbyの良さを叫んだ上の記事も合わせてご覧ください!

改めてとなりますが、Gatsby.jsReactで作られた静的サイトジェネレーターです。内部的にGraphQLを用いてデータを取得し、markdownからHTMLを生成、などの処理を簡単に行うことができます。

Gatsbyはとても凄いので、プラグインなどをブチ込むと簡単にPWA対応した超高速サイトが作れたりします。もはや静的サイトジェネレータとしてだけでなく、create-react-appに代わるフレームワークとして使えるなと思っている次第であります。

そんなこといいつつも、プラグインの選定をしたり、TypeScript対応をしたりと、新しくプロジェクトを始めるときは面倒ごとが多いものです。Gatsbyが初めての人にこそ、こういうめんどくささを省いたわかりやすいテンプレートが必要です。

というわけで急造ではありますが、簡単なテンプレートを作成しました。テンプレートというよりスケルトンと言った方が正しそうなほど薄いものですが、以下の項目をバッチリ実現しています。

  • PWA(Progressive Web Application)対応
  • SEO対策(各種metaタグ,twitter ogpなど)
  • TypeScript(v3.7.2)対応
  • MDX(JSX in Markdown)対応
  • EsLint, Prettier対応
  • Netlifyへのデプロイ用ファイルをあらかじめ用意、Netlifyでのhttp/2対応
  • emotion(CSS-in-JSライブラリ、styled-componentsより色々出来る)対応
  • ResetCSSやsiteのconfig用のコンポーネントをあらかじめ用意

スケルトンなのでサイトスピードは早くて当然といえば当然ですが、lighthouse(chromeの拡張機能、サイトのパフォーマンス測定が出来る)のスコアは以下のようになっています。ぶっちぎりの最高得点。

スクリーンショット 2019-12-03 23.41.41.png

lighthouseってオールグリーンだとめでたい感じにエフェクト出るんですね...(知らなかった)

このスケルトンのGitHubリポジトリはこちらです。

GitHub: https://github.com/hppRC/gatsby-starter-hpp

また、実際にNetlifyにデプロイしてあるので、そちらでも動作確認が出来ます。

実際のサイト: https://gatsby-starter-hpp.netlify.com/

見た目がきらびやかな訳ではないですが、シンプルな分結構いじりやすいと思うので、ぜひこのテンプレートをもとに超高速で楽しいサイトを作ってみてください!!

まとめ

本記事は以下の記事、リポジトリを多分に参考にさせていただきました。ありがとうございます!!
まだ出来ていませんが、のちほどGitHubのリポジトリにも明記しておきたいと思います。

Gatsby.js を完全TypeScript化する
https://www.lekoarts.de/
LekoArts/gatsby-themes

最後に...

Gatsbyはいいぞ!!!!!!!!!!!!

明日の記事担当者は未定です!でも多分僕が書くと思います。進捗が産めたら書きます!!

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

react-chaosでカオスエンジニアリングをやってみよう

Ubiregi Advent Calendar 2019 3日目です。

2日目に引き続きフロントエンドエンジニアのコジャが担当します。

今回は簡単にReactでカオスエンジニアリングが体感できるreact-chaosをやってみます。

カオスエンジニアリングって何?

何でしょう。私も今日に至るまで言葉は知っていましたが、実際に何をするのか、何でするのかはわかりませんでした。

ざっくり調べると「擬似的に障害を起こし、実際の障害にも耐えられるようにする」ことだそうです。

詳細は以下のQiita記事がわかりやすかったです。

カオスエンジニアリングと聞いてカオスになった人必見

react-chaosって?

react-chaosはReactで作られたアプリケーションに対して、意図的にエラーを発生させるHigh Order Componentです。

ただ、READMEを見てみると、以下の一文があります。

? Pre-Installation Notes

  • This is currently WIP and a proof-of-concept.
  • There is nothing in place to help ensure good performance practices. Use at your own risk.

実運用はまだのようですね。

なにはともあれ

やっていきます。

まずは、react-router-domを使用して、簡単なルーティングするようなアプリを作ってみます。

const App = () => {
    return (
        <BrowserRouter>
            <Layout>
                <Switch>
                    <Route exact path='/'><Home /></Route>
                    <Route path='/about'><About /></Route>
                    <Route path='/dashboard'><Dashboard /></Route>
                </Switch>
            </Layout>
        </BrowserRouter>
    )
}

動きはこんな感じです。

見ての通り、/がHome、/aboutがAbout、/dashboardがDashboardと表示されます。

before.gif

では早速、<About />にreact-chaosを適用してみましょう。
第一引数は適用対象のコンポーネント、第二引数はエラーの発生頻度、第三引数はエラーメッセージになります。

今回エラー発生頻度を10にしたので、/aboutにアクセスした時、必ずエラーが発生することになります。

import React from 'react';
import withChaos from 'react-chaos/src/index.tsx';


const About = () => {
    return (
        <div>
            <h2>About</h2>
        </div>
    );
}

export default withChaos(About, 10, 'カスタムエラーだよ');

yarn start します。

./node_modules/react-chaos/dist/index.js
Module not found: Can't resolve './chaos-react.cjs.development.js' in '/Users/koja/workspace/react-chaos-example/node_modules/react-chaos/dist'

失敗します。なんでや。

動かないんだ...

まぁまだ慌てる段階ではない。node_modules/react-chaos/dist配下を見てみると、出力エラーの通り、index.jsしか存在しませんでした。へぇ~

リポジトリのissueを漁るとこんなのが。

the newest npm package wrong? #10

This is still an issue.

なるほどね。

npmのレジストリでなく、直接gitのリポジトリを指定してもダメでした。というかbuildがコケてた。

再挑戦

ただ、demoは動いているので以前のバージョンを指定すれば、ビルドできるはずです。

react-chaos v1.0.6

現vは0.1.0でその前のvは1.0.6ってことは破壊的変更があったんでしょうか?まぁあまり気にしないことにします。

yarn add -D react-chaos@1.0.6

yarn start して /aboutにアクセスしてみると、画面が真っ白になります。コンソールを開くと舐めたエラーメッセージが出力されています。成功です。

スクリーンショット 2019-12-03 21.12.35.png

どうやって対処する?

Reactでは予期せぬエラーが発生した時、代わりの画面を表示するError Boundaryなる方法があります。

https://ja.reactjs.org/docs/error-boundaries.html

実装はこんな感じ。

import React, { Component } from 'react'

export default class ErrorBoundary extends Component {
    constructor(props) {
        super(props)
        this.state = { hasError: false }
    }

    static getDerivedStateFromError(error) {
        return { hasError: true }
    }
    render() {
        return this.state.hasError ? <h1>一時的なエラーが発生しています</h1> : this.props.children;
    }
}

エラーが発生しうるコンポーネントを子要素に持つようにします。今回は<About />が対象です。

const App = () => {
    return (
        <BrowserRouter>
            <Layout>
                <Switch>
                    <Route exact path='/'><Home /></Route>
                    <Route path='/about'>
                        <ErrorBoundary>
                            <About />
                        </ErrorBoundary>
                    </Route>
                    <Route path='/dashboard'><Dashboard /></Route>
                </Switch>
            </Layout>
        </BrowserRouter>
    )
}

こうすればエラーが発生した時、画面がホワイトアウトするのを防いで、用意したエラーメッセージを表示してくれます。下記のgifではエラーの発生頻度を3まで落としたので、なかなか発生しませんでした。

error.gif

最後に

実装まで紹介できるかなと思ってたんですが、Errorを発生させる条件が割と複雑だったのでやめました。数学ができない。

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

その技術選択は正しいか? - React、SPA、Flux、Redux

はじめに

近年Web技術は様々な選択肢が存在しています。その中でユースケースに応じて適切に技術選択することがプロダクトのためにはとても大切です。

エンジニアの興味関心だけで不要な技術選択をして、開発速度やユーザ体験の低下は起きていないでしょうか?

また技術選択をするときに正しい言語化をできているでしょうか?

  • コンポーネント開発をしたいから、Reduxを採用する
  • SPAにしたいから、Reduxを採用する
  • 単一フローを採用したいから、Reduxを採用する
  • React Hooksの登場でReduxは必要なくなった

これらはすべて適切な説明ではありません。

今回は、React、SPA、Flux、Reduxをそれぞれ採用している方が、改めてその技術選択をしていることを言語化できているかの確認になればと思います。

Reactを採用するべきか?

Reactの価値は宣言的UI、コンポーネント開発にあります。
宣言的UIではないことで発生する問題を実際に感じたことがあるのでしょうか?
もしこれらの価値を自身で説明することができなければ、Reactに限らずVue.jsやAngularなどの宣言的UIフレームワークが不要かもしれません。

また単方向バインディングもReactの特徴です。
もしこれの価値を自身で説明することができなければ、ReactとVue.jsの選択を適切にできていないかもしれません。

SPAを採用するべきか?

宣言的UIでかつコンポーネント開発が必要だからといって、SPAにしなければいけないというわけではありません。
ここには明確に技術的な境界が存在します。

名前の通り「単一ページのアプリケーション」である必要があるのでしょうか?
通常のWebページのように画面遷移してはいけないのは何故でしょうか?

あなたはSPAがないことで発生する問題を実際に感じたことがあるのでしょうか?
もしこれらを自身で説明することができなければ、SPAは不要かもしれません。

Fluxを採用するべきか?

ReactやSPAを採用したからといってFluxにする必要はありません。
ここには明確に技術的な境界が存在します。

Fluxの価値は単方向フローです。
単方向バインディングと区別せずに議論されていることがあります。
単方向バインディングと単方向フローの違いを説明できるでしょうか?

またFluxはどのように単方向フローを実現しているのでしょうか?
また単方向フローはどのような課題を解決しているのでしょうか?
単方向フローにしないとどのような実装になり、どのような問題が起きるのでしょうか?

あなたはFluxがないことで発生する問題を実際に感じたことがあるのでしょうか?
もしこれらを自身で説明することができなければ、Fluxは不要かもしれません。

Reduxを採用するべきか?

Fluxを採用したからといってReduxにする必要はありません。
ここには明確に技術的な境界が存在します。

Fluxで実現できておらずReduxで実現していることはなんでしょうか?
Reduxの三原則は、Single source of truth、State is read-only、Changes are made with pure functionsにあります。
これらの三原則がもたらすメリットはなんでしょうか?

またReduxはFluxで解決できていないProp Drillingの問題をConnect(Selector)で解決しています。

あなたはReduxがないことで発生する問題を実際に感じたことがあるのでしょうか?
もしこれらの価値を自身で説明することができなければ、Reduxは不要かもしれません。

React Hooksの登場でReduxは不要になるのか?

React Hooksの登場により、Reduxの三原則の価値はなくなるのでしょうか?
それともReact HooksのuseReducer、useState、useContextなどを利用して、Redux相当のものを自作できるということでしょうか?

もしこれらを自身で説明することができなければ、Reduxは必要かもしれません。

まとめ

Fluxを提案したReact Coreチームに所属しているDan Abramovは以下のように主張しています。

もしFluxが解決する問題をあなた自身が解決しようとしたことがない場合、Fluxが何を解決するかを理解することは難しいです。
私は人々がトレードオフを理解しないがために、Fluxの最悪な特徴を獲得して、最高の特徴を見逃すのを見てきました。
https://medium.com/swlh/the-case-for-flux-379b7d1982c6

またDan AbramovはRedux作者でもあり、別の記事では以下のように主張しています。

人々はReduxを必要とする前からReduxを使おうとします。
しかしながら、あなたが今まさにReactを学習中なのであれば、Reduxを最初の選択にしてはいけません。
その代わりReactで考えることを学びましょう。本当に必要になったら、または何か新しいことに挑戦したくなったらReduxに戻って来てください。
https://mae.chab.in/archives/2937

課題を解決するためにフレームワーク使うことが大切です。フレームワークの必要性を言語化できていない状態で採用してもフレームワークが解決している目的を達成できない可能性があります。そうなるとただ冗長なコードが増えただけという結果になりかねません。

そうならないためにも一度はフレームワークを使わずに挑戦して、フレームワークが解決しようとした課題を自分で体験することがとても大切です。

また新しく登場して流行しているフレームワークは素晴らしく見えます。
そこで新しいフレームワークを導入する際には、一度立ち止まりこのフレームワークの価値を表面的にではなく何回も深堀りして考えてみてください。

それを踏まえてプロダクトと組織のことを考え、長期的に負債を生まないように適切な解決手段を提案しなければなりません。

これらのバランスを考え技術選定できることがアーキテクトの実力の見せ所であり、本当のスキルではないでしょうか。

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

Firebase Meetup #15 Cloud Functions Day 資料まとめ

イベント内容

2019/12/03 に開催された Firebase Meetup #15 Cloud Functions Day の資料まとめです。

Connpass: https://firebase-community.connpass.com/event/154400/
YouTube: https://youtu.be/BaNrP26EzU0

Firebase & Google Cloud によるサーバーレス帳票管理

React x Firebase を用いた Web アプリケーション開発時のハマりどころと解決策

もしもFDLでiOSアプリが開かなかったら

PoC using Firebase Cloud Firestore

Firestoreのスキーマ定義をチームで共有したい

firebaseオンリー + React Nativeでアプリを作ると果たして簡単になるのか?

Functionsでhtmlベタ書きレンダリングした上でリダイレクトするのをやめるんだ

CQRSを学ぶついでにCloud FunctionsとFirestoreを連動させる時の小技も学ぶ

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

ReactくんさぁHooksとか言ってるけど、で?どうやってAPIから値持ってくんの?

はじめに

いなたつアドカレの三日目の記事です。

今回はReactでRestAPIなどから値を取ってくるのに、関数型コンポーネントでどーやってAPIから値取ってくんの?

componentDidMountなんてないよ?

componentDidUpdateなんてないよ?

ってなりますよね、僕はなります。

結論から言ってクレメンス

useEffectを使うンゴ

React Hooks にある、useEffectを使用します。

useEffectってなんぞや

関数コンポーネントで副作用を実行するためのフックで、classコンポーネント時のcomponentDidMount,componentDidUpdate,componentWillUpdateの3つにあたります。

じっそー

useEffect(() => {
// ここでAPIをたたく
},[])

useEffectの第一引数が、実行する関数で、第二引数が何が変化したときにuseEffectを実行するかを決めるものとなっています。
ここでは空配列を指定しています、空配列を指定することでコンポーネントが配置される前(componentDidMount)でのみuseEffectを実行するといったことができます。

useEffect(() => {
// ここでAPIをたたく
},[hoge])

これでhogeが変更された時に反応してuseEffectが実行され、APIを再度叩くことができます。

このようにしてきっちりバックエンドとフロントエンドを分離した際に再描画のタイミングを指定してAPIを再び指定することができます。

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

React x Firebase を用いた Web アプリケーション開発時のハマりどころと解決策

概要

これから React x Firebase の構成で Web アプリケーションを開発する方が同じところでハマらないようにハマりどころと解決策をまとめました。

この資料は Firebase Meetup #15 Cloud Functions Day の LT 資料です。

スライドは以下です。
React x Firebase を用いた Web アプリケーション開発時のハマりどころと解決策 - Speaker Deck

1. Hosting と同じドメインで Cloud Functions の関数を呼び出す

Cloud Functions のデフォルトで生成される URL は以下ですが、

https://[region]-[project id].cloudfunctions.net/[function name]

以下の URL で呼び出したいことがあります。

  • https://[project id].web.app/[function name]
  • https://[project id].firebaseapp.com/[function name]
  • https://[独自ドメイン]/[function name]

Hosting の設定ファイルである firebase.json の rewrites の設定をすれば OK です。以下の設定で /notifications にアクセスされた時に Cloud Functions の notifications 関数を実行し、その他のパスにアクセスされた場合は index.html が参照されます。

firebase.json
  "hosting": {
    "rewrites": [
      {
        "source": "/notifications",
        "function": "notifications"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]

Functions は以下のように、通常通り実装すれば良いです。

functions/index.js
exports.notifications = functions
  .https
  .onRequest((req, res) => {
    // ...
  };

2. ローカル環境の React から Emulator の Functions を呼び出す

やりたかったことは以下です。

  • Functions をいちいちデプロイして検証したくない
  • React のホットリロードはそのまま使いたい

そのため、 React は yarn start して Functions は emulator を使って接続します。 Functions をローカルで動作させるために Emulator Suite を使います。

Command
$ firebase emulators:start --only functions
src/index.js
const app = firebase.initializeApp(config)

if (process.env.NODE_ENV === 'development') {
  app.functions().useFunctionsEmulator('http://localhost:5001')
}

3. React と Functions で Firestore の参照先を合わせる

2 の設定を行った場合、 Firestore を利用していると以下のような状態になります。

  • React: Cloud の Firestore を参照
  • Functions: Emulator の Firestore 参照

そのため、Cloud か Emulator か参照先を統一する必要があります。

3-1. Cloud の Firestore を参照する

以下のような構成です。

  • React: yarn start
  • Functions: Emulator
  • Firestore: Cloud

この場合のメリデメは以下のようになります。

  • メリット: 開発時に Firebase console からデータを確認できる
  • デメリット: Cloud のデータが更新されるため、複数人開発には不向き。リリース後は Firebase を複数プロジェクト構成 にするなどの工夫が必要

設定は以下のように行います。Functions のディレクトリで @google-cloud/firestore パッケージをインストールして、 functions/index.js からそのパッケージを利用します。 引数には Firebase の project id を指定します。

Command
$ npm install --save @google-cloud/firestore
functions/index.js
const Firestore = require('@google-cloud/firestore')
const admin = require('firebase-admin')

const db = (() => {
  if (process.env.FUNCTIONS_EMULATOR) {
    return new Firestore({ projectId: 'your project id' })
  } else {
    return admin.firestore()
  }
})()

公式ドキュメントの 認証の開始 ページからサービスアカウントキーを作成してローカルに配置し、 .bashrc や .zshrc などに追記しておきます。

Command
$ export GOOGLE_APPLICATION_CREDENTIALS="[PATH]"

3-2. ローカルの Firestore を参照する

以下のような構成です。

  • React: yarn start
  • Functions: Emulator
  • Firestore: Emulator

この場合のメリデメは以下のようになります。

  • メリット: ローカル環境に閉じてデータを変更できるので複数人開発やリリース後の運用にも向いている
  • デメリット: データの確認が難しい

デメリットとしてデータ確認が難しいとしましたが、これは Firestore CLI などがない(?)ためで、もし良いアクセス方法ご存じの方いたらコメントください。

設定は以下のように行います。公式ドキュメント でもこの方法が紹介されています。

Command
$ firebase emulators:start --only functions,firestore
src/index.js
const app = firebase.initializeApp(config)

if (process.env.NODE_ENV === 'development') {
  app.functions().useFunctionsEmulator('http://localhost:5001')
  db.settings({
    host: "localhost:8081",
    ssl: false
  });
}

4. Hosting から asia-northeast1 region の Functions を呼び出せない

以下のように region 指定をすると Hosting から呼び出せなくなる

functions/index.js
exports.getEvents = functions
  .region(asia-northeast1)
  .https()
  .onCall((data) => {
    // ...
  });

スクリーンショット 2019-12-02 14.49.50.png

公式ドキュメント を見ると

Firebase Hosting は、us-central1 でのみ Cloud Functions をサポートします。

との記述が。。というわけで、Hosting とつなぐ Function は、現状 region 指定しないで運用するしかなさそうです。

以上!

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

身軽な静的サイトジェネレーター Charge の紹介&採用例

この記事は Speee Advent Calendar 2019 4日目の記事です。
GatsbyJS の重装備を外して、Charge で身軽になるまでの経緯をご紹介します。

前段

あんた誰?

(昨年から引き続き)Markdown によるスライド作成ツール Marp の次世代版を開発している人です。

プロジェクト公式サイト

今年の 6 月に、プロジェクトの公式サイトを開設しました。

このサムネイルは開設当初のもので、GatsbyJS を使って立ち上げたものの、プロジェクトのランディングページとしては思ったより重装備だったことが災いし、ほぼ蛻の殻のままの状態が数ヶ月続いてしまい、コンテンツを拡充できませんでした。

この記事は、そんな状況を打破するために移行先として選んだ、新進気鋭の静的サイトジェネレーター Charge の紹介です。

Charge

Charge"an opinionated, zero-config static site generator" と銘打っている通り、設定より規約 を重視した JavaScript / React による静的サイトジェネレーターです。

  • ゼロ設定
  • JSX と MDX によるテンプレート
  • サーバーサイド React
  • Babel / PostCSS のビルトインサポート
  • ライブリロード対応の開発サーバー
  • ファイルのリビルドが最小限
  • 充実したドキュメント :sparkles:

Charge は GatsbyJS や Next.js と比べても、現時点ではユーザーも少ないマイノリティなツール(Marp は一応その中では一番の大口顧客)ですが、シンプルかつ必要十分な機能が揃っており、設定なしですぐにサイトを構築し始めることができます。

作者によれば、 『本当は新しい静的サイトジェネレーターなんて作りたくなかったけど、シンプルで、ドキュメントが充実していて、機能が揃っていて、メンテナンスされていて、望むように動くようデザインされているものがなかった』 とのことで作り始めたとのことです。

Yeah, I know, another static site generator. Let me be clear, I really did not want to make a static site generator. It’s really the very last thing I wanted to do.

I went on StaticGen and looked at every JavaScript-based one. I could not find a single one that I thought was simple, well-documented, had the features I needed, was actively maintained, and was designed and worked the way I wanted. So here I am, making a static site generator.

https://github.com/brandonweiss/charge#why

GatsbyJS との比較

リポジトリおよびドキュメントで、GatsbyJS との違いについて 述べられています。大きな違いは以下の2点です。

GatsbyJS は複雑な Web アプリケーションを作れるが、設定のために複雑な知識を必要とする

https://qiita.com/uehaj/items/1b7f0a86596353587466#20181228%E8%BF%BD%E8%A8%98 でも説明されていますが、GatsbyJS は React を使った Web アプリケーションに応用することができ、サイトの要件に合わせて柔軟な設定が可能です。

一方、設定に際しては、関連する技術 (Webpack, GraphQL, ルーターなど) の習得が必要になるため、要求される知識量は必然的に多くなります。

Charge は、サーバーサイドでの静的サイト生成にフォーカスを絞っているため、設定可能な箇所はほとんどなく、動的なアプリケーションや、複雑なサイトを作成するのには不向きです。

逆に言えば、LP やドキュメント、ポートフォリオなど、シンプルなサイトではこのような機能は必要無いはずです。目的がマッチしていれば、最小限の知識でサイトを作成することができます。

GatsbyJS はクライアント側に提供された React を使ってページを表示するが、Charge は 真の静的ページ を作る

Charge は GatsbyJS と同じく React を採用しているものの、大きく異なる点として 『サーバーサイドレンダリングのため "だけ" に React を使う』 という点が挙げられます。

GatsbyJS が出力するサイトには、クライアント側の HTML 向けの React / JavaScript も少なからず注入されます。事実、GatsbyJS 製のサイトで PWA や高速な画面遷移を実現しているのはそのおかげでもあるのですが、オプトアウトする手立てはありません。

Charge には、そのような暗黙的な注入はなく、HTML のみで動く "真の静的ページ" を作成することができます。クライアント側の React はそもそも注入されていないので、Charge の React は SSR を気にするだけで OK です。

実例: GatsbyJS に感じたモヤモヤ

実際のサイト構築で GatsbyJS を使ってみて、モヤモヤを感じたのが以下でした。

  • 単なる「LPを作る」という目的に対し、手段にオーバーテクノロジー感があり、着手から公開まで半年ほどかかってしまった
  • @reach/router のコンテナ <div> が CSS やスクロールなどの各種挙動を邪魔してしまう

時間がかかったのは、単に私の知識とリソースの不足に依るところが大いにありますが、それを抜きにしても、今回の目的に対して要求される技術の釣り合いが合わない印象があり、オーバーテクノロジーだなぁと感じた面は否めませんでした。

また、GatsbyJS で採用されているルーター @reach/router のコンテナに注入される <div> 1 によって、CSS(グローバルスタイル)が壊れたり、ルーター側のフォーカス管理による スクロール関連のトラブル が散見されるなど、地味にハマりどころが多かったのも尾を引きました。

結果的に、サイトを公開しても、メンテナンスを継続することが難しくなってしまったため、よりシンプルな Charge に白羽の矢が立ったというわけです。

使い方

インストール

npm i --save-dev @static/charge

CLI コマンド

Charge の CLI コマンド は迷いようが無いほど本当にシンプルで、開発用の serve とビルド用の build の2種類しかありません。

# 開発用サーバー起動
npx charge serve <source-dir>

# ビルド
npx charge build <source-dir> <dist-dir>

コンセプト

Charge の基本は、 source-dir に配置された静的ファイルは、ビルド時に dist-dir にコピーされる』 という設計です。これにより、ソースとサーバーのディレクトリ構成はほぼ一致します。

JSX や CSS など、一部の拡張子を持つファイルのみ、『ビルド時にそれぞれのファイルタイプに合わせて変換される』という設計になっています。

JSX

前述の仕様のため、そのまま HTML を置くことも勿論できますが、通常は、JSX でページを書き進めていくことになることでしょう。

命名規則として、index.html.jsx というファイルを置くと JSX がパースされ、ビルド結果が index.html というファイルに出力されます。

index.html.jsx
import Layout from './layout.html.jsx'

export default () => (
  <Layout title="My first Charge">
    <h1>Hello, world!</h1>
    <p>こんにちは!</p>
  </Layout>
)

中身は React なので、以下のようにレイアウトを分けたり、コンポーネントを分割したりできます。

layout.html.jsx
export default ({ children, title }) => (
  <html>
    <head>
      <title>{title}</title>
    </head>
    <body>
      {children}
    </body>
  </html>
)

MDX

Charge は MDX をサポートしているため、Markdown + JSX でページを書くこともできます。

index.html.mdx
import Layout from './layout.html.jsx'

export const layout = ({ children }) => (
  <Layout title="My first Charge">
    {children}
  </Layout>
)

# Hello, world!

こんにちは!

ドキュメントや、ブログの記事のようなページに最適です。

JavaScript

クライアント側で JavaScript が必要な場合、JS ファイルを配置できます。変換時には Babel が適用されます。

index.js
import message from './message.js'

console.log(message)

import による依存関係がある場合は、呼び出し元のファイルにマージされます。

message.js
// このファイルはビルド時に出力されない
export default 'Hello, world!'

拡張子を省略した import (import message from './message') だと、ビルド時に依存関係がうまく解決されないことがあるので、拡張子まで指定しておくのが無難です。

生成された JavaScript をブラウザで使用するには、<script> タグを明示します。Webpack などに慣れていると、つい import を使って呼び出したくなりますが、グッと堪えましょう。

layout.html.jsx
export default ({ children, title }) => (
  <html>
    <head>
      <title>{title}</title>
      <script src="/index.js" async></script>
    </head>
    <body>
      {children}
    </body>
  </html>
)

CSS

スタイルシートは PostCSS が自動的に通され、いくつかの最新の CSS 機能(Stage 2 以上 + α)を使用できます。また、JavaScript と同様に @import ルールに基づいて依存関係が解釈・マージされます。

CSS-in-JS

React に慣れている方であれば CSS-in-JS を使いたくなると思いますが、 "Charge の React は SSR 専門" なので、スタイルシートの注入に CSR が必要なライブラリ (styled-components など) は使用できないようです。

ドキュメント では利用可能な CSS-in-JS ライブラリの1つとして emotion が挙げられています。

その他の機能

全体を通して見ても、そこまで凝った機能はなく、『必要最低限のことを、必要最小限の労力で実現できる』という感じのツールになっています。

デプロイ

デプロイに関するドキュメント では、同作者による Amazon S3 へのデプロイのためのコンパニオンツール Discharge が紹介されています(このツール自体は Charge 以外でも使用可能)。

Next.js を開発する ZEIT が提供する静的サイトホスティングサービス ZEIT Now による Charge サイトのデプロイガイド もあります。

採用してみて

先月 11 月に、ユーザー導線の改善を目的に、Charge + emotion を使用して Marp の公式サイトを再構築 しました。2

ページ自体はほぼ HTML + CSS で構成されたページですが、主要な要素(ボタンなど)は React コンポーネントとして分割してあり、GatsbyJS の時に作ったコンポーネントなどは Charge でも引き続き使えるので、使用感にはそれほど違和感はありませんでした。

Charge 特有の要素は無い訳ではありませんが、大したものではなく、React についても「SSR しか考えなくて良い」と割り切れるため、実装はかなりスピーディーに進めることができました。暗黙的なコンテンツの注入に邪魔されず、コンテンツの内容を完全にコントロールできるのも安心できるポイントです。

中央に見える Marp スライドのサンプルは、画像ではなく、実際に Marp Core を使った変換結果を Charge 向けに作成した React コンポーネント を使ってレンダリングしています。

難点

Charge は『1ファイルにつき1ページ』の原則があるため、例えばページネーションを扱うために 1.html 2.html ... のような連番ページを作成する、なんてことはできません。もしこのような仕組みを実現するのであれば、自分でクライアント向けの JS を書いて実装する必要があります。

Marp プロジェクトのブログページ は、現状ページネーションが必要なほど記事が爆発的に増えることは想定していませんが、今後プロジェクトが拡大して記事が増えていくと、この点は考える必要が出てくるかもしれません。

Charge における動的ページ (Dynamic pages) の対応については "Coming soon" となっているため、今後に期待しましょう。

おわりに

静的サイトジェネレーター Charge の採用例のご紹介でした。

Charge の機能は本当にシンプルで、用途によってハッキリとした向き不向きこそありますが、GatsbyJS の重装備についていけなかった人(私)にとってはピッタリフィットする身軽なツールになると思いますので、同じ悩みを抱えている人には是非お勧めします。

Charge — an opinionated, zero-config static site generator
https://charge.js.org/


  1. このコンテナは無効にすることができず、該当 Issue でもハマった人達による多くの報告がなされています。 

  2. ソースコードは https://github.com/marp-team/marp/tree/master/packages/website で閲覧できます。コンテンツの拡充にも関わらず、GatsbyJS 採用時と比べると、コード量は およそ半減 しました。 

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

Reactで特定の条件を満たす行の背景色を変えたい

やりたいこと

値により行の背景を変えたい。ここではgenderがfemaleならピンクにする。
下記のような感じ。

スクリーンショット 2019-12-03 13.15.05.png

基本方針?

基本的には条件に応じて三項演算子等を使ってstyleやclassNameを切り替えるのが基本。

条件 ? A : B;

条件がtrueならA、falseならBを実行。

CSSを切り替える

まず、CSSを切り替えてみる。
ループにて表を描画しながら、genderがfemaleなら背景を変えるような操作。

<tbody>
    {
        this.state.listData.map(member => (
            <tr key={member.docId} style={member.gender === "female" ? { background: "#ffe4e1" } : {}}>
                <td>{member.docId}d</td>
                <td>{member.email}</td>
                <td>{member.gender}</td>
                <td><img src={member.avatarUrl} height="30" width="30" alt="" /></td>
                <td><Link to={`/show/${member.docId}`}><Button size="sm" color="primary">詳細</Button></Link></td>
                <td><Link to={`/edit/${member.docId}`}><Button size="sm" color="success">編集</Button></Link></td>
            </tr>
        ))
    }
</tbody>

Classを切り替える

classの場合も基本は同じ。

あらかじめ、

.female {
  background: #ffe4e1;
}

のようなcssが定義されているものとして、下記のようにclassを切り替える。

<tbody>
    {
        this.state.listData.map(member => (
            <tr key={member.docId} className={member.gender === "female" ? "female" : null}>
                <td>{member.docId}d</td>
                <td>{member.email}</td>
                <td>{member.gender}</td>
                <td><img src={member.avatarUrl} height="30" width="30" alt="" /></td>
                <td><Link to={`/show/${member.docId}`}><Button size="sm" color="primary">詳細</Button></Link></td>
                <td><Link to={`/edit/${member.docId}`}><Button size="sm" color="success">編集</Button></Link></td>
            </tr>
        ))
    }
</tbody>

なお、もっとエレガントにclass名を制御したい場合は、こちらの記事が参考になります。
以上。

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

Next.jsをつかってSPAをSSRしてみた感想

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


ReactのSPAをNext.jsでSSRしてみて気づいたことをまとめてみました。

まだNext.jsやNuxt.jsでSSRやったことない人や、これからやろうか検討している人への情報共有的位置づけです。

目次

  • SPAをSSRするとは?
  • どのような構成になったか?
  • どのような実装をおこなったか?
  • SPAをSSRしてどうなったか?
  • 気づいたこと
  • 所感

SPAをSSRするとは?

SPAの弱点である初回アクセス時の読み込み時間を短縮するために、サーバサイドでレンダリングを行いブラウザ上に素早く画面を表示させる手法。
初回表示後の画面遷移やアクションは通常のSPAと同じように機能する。

クライアントにおける初回表示のフローは、通常のSPAとSSRした場合とで以下のようになる

通常のSPA

  1. サーバからバンドルされたJSファイルを取得
  2. Reactコンポーネントの初期化
  3. ローディング表示
  4. APIリクエスト&レスポンス
  5. 表示

SSRした場合

  1. サーバからデータが挿入されたDOMとバンドルされたJSファイルを取得
  2. 表示 (裏側で初期化がはしる)

通常のSPAではバンドルファイル取得後に順を追って初期化していくが、SSRすると取得したものをそのまま表示できるので、SPAなのにパッと画面が表示される。

どのような構成になったか?

通常のSPAではバンドルJSファイルをS3やFirebaseHostingなんかに置いてサーバレス構成にもできたが、SSRを行う場合はNode.jsのWebサーバをたてることになる。自分の場合は以下のような感じ。

  • Next.jsで実装したアプリをExpressと組み合わせてWebサーバを構築
  • GAEとかGKEのコンテナ上でサーバを立ち上げる
  • サーバのルーティングもクライアントのルーティングもNext.jsが内部でやってくれる

どのような実装を行ったか?

基本的にNext.jsの作法に乗っかっていく感じ。公式リポジトリのexamplesがかなり充実しているので、主なケースは参考にしつつ実装できる。

ReactアプリをSSRする場合は具体的に以下のような改修を行っていく。

  • componentDidMountやuseEffect(マウント時)で宣言していた処理をgetInitialPropsという関数で宣言する
  • クライアントとサーバで違う処理を行う必要があるときはpropsのisServerを参照して処理を分ける
  • Next.jsのルーティングシステムに合わせた構造にする(pagesディレクトリ配下にコンポーネント整備)
  • サーバサイドからのAPIリクエストに使用する認証token(Cookie)をreqから取得する
  • サーバにはwindowやdocumentがないので、ライブラリの初期化等で必要な場合は動的インポートを使用してSSRをエスケープする
  • などなど……

SPAをSSRしてどうなったか?

初回表示がかなり速くなり、思わず「おおおぉ〜」と声がでた。

  • ロード完了まで5秒ほどかかっていた画面が、1秒かからなくなった
  • APIアクセスもサーバ同士なのでなんか速そう
  • Next.jsのコード分割 & 動的インポートの機能をさらに活用していけばもっと速くなりそう

気づいたこと

通常のSPAでは(サーバレス構成にすれば特に)Webサーバについて気にする必要がなかったので、フロントエンドパワーがあればそれだけで完結する話が多かったように思う。

SSR構成にすればサーバ内の処理やリソースについても思いを巡らせる必要があるので、難易度も必要な工数も増すが、ある程度SPA開発に慣れていてサーバサイドに対しても明るいエンジニアであれば楽しんでいける領域だと思う。

Next.jsはSSRだけでなく静的ジェネレートやAMP、PWAについてもサポートしているので、Webアプリの理想郷を追求していける面白いフレームワークだと思う。

まとめ

  • SSRはやっぱり速かった
  • フレームワークの恩恵により、実装は意外と簡単だった
  • サーバサイドをいかに攻略していけるかがポイント
  • 技術力さえあれば最高のWebアプリケーションが作れると思う!

以上です。ここまで読んでいただき、ありがとうございました。


2019年のアドベントカレンダーは3つ参加してます。よかったら他の記事も見てください。

P.S. @tkdnさん: カレンダーへご招待いただきありがとうございました。

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

続:React + FirestoreでCRUD

以前も同じ記事を書いたのですが、人の記事をなぞっただけなので自分で書いてみました。
さらに、実案件を想定して、ファイルアップロードやカレンダーも含めたCRUDを試してみました。

仕様・完成イメージ

CRUDといいつつ、Editページに詳細表示機能を兼ねさせたり削除機能を追加することにより、以下の3ページを作成することでことは足ります。

  • Create(新規登録)
  • Index(一覧)
  • Edit(編集+詳細画面を兼ねる+削除機能追加)
  • Show(いらないけど参考実装)

スクリーンショット 2019-12-03 11.04.50.png

完成版のgithubのリポジトリはこちら

実装の手順

  1. データがないと始まらないのでまずCreateを実装します。
  2. 次に一覧を実装します。
  3. 最後にEditを実装します。

準備

作業場所作成とモジュールのインストール

作業場所を作成し、必要なモジュールをインストールします。

create-react-app crud
cd crud

yarn add bootstrap firebase formik moment react-datepicker react-firebase-file-uploader react-router-dom reactstrap yup

必要なファイルの作成

必要なファイルを生成しておきます。ファイルはsrc以下に作成します。

mkdir src/screens
touch src/screens/Create.js
touch src/screens/Index.js
touch src/screens/Edit.js
touch src/screens/Show.js

touch src/Firebase.js

bootstrapのcssを提供する

index.js
import React from 'react';
import ReactDOM from 'react-dom';
+import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

実装

まずはApp.jsでルーティングの設定を行います。
各コンポーネントが無いとエラーがでるので、作成の順番に注意してください。

App.js

App.js
import React from 'react';
import './App.css';

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

//screens
import Index from './screens/Index';
import Create from './screens/Create';
import Show from './screens/Show';
import Edit from './screens/Edit';

class App extends React.Component {
    render() {
        return (
            <Router>
                <Switch>
                    <Route exact path="/" component={Index} />
                    <Route exact path="/create" component={Create} />
                    <Route exact path="/show/:uid" component={Show} />
                    <Route exact path="/edit/:uid" component={Edit} />
                    <Route render={() => <div>Page not found.</div>} />
                </Switch>
            </Router>
        );
    }
}

export default App;

Create.js

ちょっと複雑にはなりますが、ここでは実践で利用する各種Form要素を利用してみたいと思います。
主な要素は

  • 普通のInput
  • Select
  • Radioボタン
  • カレンダー
  • ファイルアップロード
  • チェックボックス

あたりです。

Create.js
import React from 'react';
import { Form, FormGroup, FormFeedback, Label, Input, Button, Spinner } from 'reactstrap';
import { Formik } from 'formik';
import * as Yup from 'yup';
import { Link } from 'react-router-dom';

import firebase, { db } from '../Firebase';

import FileUploader from "react-firebase-file-uploader";

import moment from 'moment';

//react-datepicker
import DatePicker, { registerLocale } from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
//for locale ja
import ja from 'date-fns/locale/ja';
registerLocale('ja', ja);

class Create extends React.Component {

    state = {
        avatarUrl: '',
        avator: '',
        isUploading: false,
        progress: 0,
        spinnerHidden: true,
    }

    handleOnSubmit = async (values) => {
        // alert(JSON.stringify(values));
        this.setState({ spinnerHidden: false });

        //dbへ書き込み
        const docId = db.collection("members").doc().id;
        await db.collection("members").doc(docId).set({
            docId: docId,
            email: values.email,
            area: values.area,
            gender: values.gender,
            birthday: firebase.firestore.Timestamp.fromDate(new Date(values.birthday)),
            avatarUrl: values.avatarUrl,
            agree: values.agree,
            createdAt: firebase.firestore.FieldValue.serverTimestamp(),
        });

        this.setState({ spinnerHidden: true });
        alert("登録しました。");
    }

    //upload
    handleUploadStart = () => this.setState({ isUploading: true, progress: 0 });
    handleUploadError = error => {
        this.setState({ isUploading: false });
        console.log(error);
    }
    handleUploadSuccess = async filename => {
        await this.setState({ avator: filename, isUploading: false });
        const url = await firebase.storage().ref("images").child(filename).getDownloadURL();
        await this.setState({ avatarUrl: url });
        return url;
    }
    handleProgress = progress => this.setState({ progress: progress });

    render() {
        return (
            <div className="container">
                <h3 className="text-center my-5">新規登録</h3>
                <div className="text-right my-3 mr-5"><Link to="/">一覧へ戻る</Link></div>
                <Formik
                    initialValues={{ email: '', area: '', gender: '', birthday: moment(new Date()).format('YYYY/MM/DD'), avatarUrl: '', agree: '' }}
                    onSubmit={this.handleOnSubmit}
                    validationSchema={Yup.object().shape({
                        email: Yup.string().email().required(),
                        area: Yup.string().oneOf(['関東', '関西']).required(),
                        gender: Yup.string().oneOf(['male', 'female']).required(),
                        avatarUrl: Yup.string().required(),
                        agree: Yup.boolean().oneOf([true]).required(),
                    })}
                >
                    {
                        ({ handleSubmit, handleChange, handleBlur, values, errors, touched, setFieldValue }) => (
                            <Form className="col-8 mx-auto" onSubmit={handleSubmit}>
                                <FormGroup>
                                    <Label for="email">Email</Label>
                                    <Input
                                        type="email"
                                        name="email"
                                        id="email"
                                        value={values.email}
                                        onChange={handleChange}
                                        onBlur={handleBlur}
                                        invalid={Boolean(touched.email && errors.email)}
                                    />
                                    <FormFeedback>
                                        {errors.email}
                                    </FormFeedback>
                                </FormGroup>
                                <FormGroup>
                                    <Label>■お住いの地域</Label>
                                    <Input
                                        type="select"
                                        name="area"
                                        id="area"
                                        value={values.area}
                                        onChange={handleChange}
                                        onBlur={handleBlur}
                                        invalid={Boolean(touched.area && errors.area)}
                                    >
                                        <option value="選択して下さい">選択して下さい</option>
                                        <option value="関東">関東</option>
                                        <option value="関西">関西</option>
                                    </Input>
                                    <FormFeedback>
                                        {errors.area}
                                    </FormFeedback>
                                </FormGroup>
                                <FormGroup className="mb-4">
                                    <legend className="col-form-label">■性別</legend>
                                    <FormGroup inline check>
                                        <Label check>
                                            男性:<Input
                                                type="radio"
                                                name="gender"
                                                id="male"
                                                value="male"
                                                onChange={handleChange}
                                            />
                                        </Label>
                                    </FormGroup>
                                    <FormGroup inline check>
                                        <Label check>
                                            女性:<Input
                                                type="radio"
                                                name="gender"
                                                id="female"
                                                value="female"
                                                onChange={handleChange}
                                            />
                                        </Label>
                                    </FormGroup>
                                    <span className="text-danger small">{touched.gender && errors.gender ? errors.gender : null}</span>
                                </FormGroup>
                                <FormGroup>
                                    <legend className="col-form-label">■直近の誕生日</legend>
                                    <DatePicker
                                        locale="ja"
                                        name="birthday"
                                        id="birthday"
                                        value={values.birthday}
                                        dateFormat="yyyy/MM/dd"
                                        customInput={<Input invalid={Boolean(errors.birthday)} />}
                                        onChange={date => setFieldValue("birthday", moment(date).format('YYYY/MM/DD'))}
                                    />
                                </FormGroup>
                                <FormGroup>
                                    <legend className="col-form-label">■プロファイル画像</legend>
                                    <FileUploader
                                        accept="image/*"
                                        name="avatarUrl"
                                        randomizeFilename
                                        storageRef={firebase.storage().ref("images")}
                                        onUploadStart={this.handleUploadStart}
                                        onUploadError={this.handleUploadError}
                                        onUploadSuccess={async (filename) => {
                                            const path = await this.handleUploadSuccess(filename); //ここもawaitにしないとurl取得できない
                                            setFieldValue("avatarUrl", path); //値のセットとエラーの削除
                                        }}
                                        onProgress={this.handleProgress}
                                    />
                                    <span className="text-danger small">{touched.avatarUrl && errors.avatarUrl ? errors.avatarUrl : null}</span>
                                    {this.state.isUploading ? <p>Uploading... {this.state.progress}%</p> : null}
                                    {this.state.avatarUrl ? <img src={this.state.avatarUrl} width="120" alt="" className="my-2"/> : null}
                                </FormGroup>
                                <FormGroup className="my-4">
                                    <legend className="col-form-label">■規約に同意して下さい。</legend>
                                    <FormGroup inline check>
                                        <Input
                                            type="checkbox"
                                            name="agree"
                                            id="agree"
                                            value={values.agree}
                                            onChange={handleChange}
                                        />
                                        <Label for="agree" check>同意する。</Label>
                                        <span className="text-danger small">{touched.agree && errors.agree ? errors.agree : null}</span>
                                    </FormGroup>

                                </FormGroup>
                                <div>
                                    <Button type="submit" color="primary">
                                        <Spinner color="light" size="sm" className="mr-1" hidden={this.state.spinnerHidden} />
                                        登録する
                                        </Button>
                                </div>
                            </Form>
                        )
                    }
                </Formik>
            </div>
        );
    }
}

export default Create;

Index.js

ひとまず一覧表示してEditやShowにリンクするだけです。

Index.js
import React from 'react';
import { Button } from 'reactstrap';
import { Link } from 'react-router-dom';

import firebase, { db } from '../Firebase';

class Index extends React.Component {

    state = {
        listData: [],
    }

    //db変化時のコールバック
    onCollectionUpdate = (querySnapshot) => {
        const docs = querySnapshot.docs.map(doc => doc.data());
        this.setState({ listData: docs });
    }

    componentDidMount = () => {
        //dbの変化を監視(変化が無くても初回は実行される)
        this.unsubscribe = db.collection("members")
            .orderBy('createdAt','desc')
            .onSnapshot(this.onCollectionUpdate);
    }

    componentWillUnmount = () => {
        //subscribe停止
        this.unsubscribe();
    }

    render() {
        return (
            <div className="container">
                <h3 className="text-center my-5">メンバー一覧</h3>
                <Link to="/create"><Button color="primary" className="m-2">新規登録</Button></Link>
                <table className="table">
                    <thead>
                        <tr>
                            <th>UID</th>
                            <th>Email</th>
                            <th>Avatar</th>
                            <th>詳細</th>
                            <th>編集</th>
                        </tr>
                    </thead>
                    <tbody>
                        {
                            this.state.listData.map(member => (
                                <tr key={member.docId}>
                                    <td>{member.docId}d</td>
                                    <td>{member.email}</td>
                                    <td><img src={member.avatarUrl} height="30" width="30" alt="" /></td>
                                    <td><Link to={`/show/${member.docId}`}><Button size="sm" color="primary">詳細</Button></Link></td>
                                    <td><Link to={`/edit/${member.docId}`}><Button size="sm" color="success">編集</Button></Link></td>
                                </tr>
                            ))
                        }
                    </tbody>
                </table>
            </div>
        );
    }
}

export default Index;

Edit.js

Create.jsの画面をベースに指定したメンバー(ドキュメント)の情報をデフォルト値としてセットし、更新があれば更新することになります。削除ボタンもEditに実装します。

Edit.js
import React from 'react';
import { Form, FormGroup, FormFeedback, Label, Input, Button, Spinner } from 'reactstrap';
import { Formik } from 'formik';
import * as Yup from 'yup';
import { Link } from 'react-router-dom';

import firebase, { db } from '../Firebase';

import FileUploader from "react-firebase-file-uploader";

import moment from 'moment';

//react-datepicker
import DatePicker, { registerLocale } from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
//for locale ja
import ja from 'date-fns/locale/ja';
registerLocale('ja', ja);

class Edit extends React.Component {

    state = {
        avatarUrl: '',
        avator: '',
        isUploading: false,
        progress: 0,
        spinnerHidden: true,
        member: { email: '', area: '', gender: '', birthday: '', avatarUrl: '', agree: false }, //初期値ないとWarning出る
    }

    handleOnSubmit = async (values) => {
        // alert(JSON.stringify(values));
        this.setState({ spinnerHidden: false });

        //dbへ書き込み
        const docId = this.props.match.params.uid;
        await db.collection("members").doc(docId).update({
            email: values.email,
            area: values.area,
            gender: values.gender,
            birthday: firebase.firestore.Timestamp.fromDate(new Date(values.birthday)),
            avatarUrl: values.avatarUrl,
            agree: values.agree,
        });

        this.setState({ spinnerHidden: true });
        alert("更新しました。");
    }

    //upload
    handleUploadStart = () => this.setState({ isUploading: true, progress: 0 });
    handleUploadError = error => {
        this.setState({ isUploading: false });
        console.log(error);
    }
    handleUploadSuccess = async filename => {
        await this.setState({ avator: filename, isUploading: false });
        const url = await firebase.storage().ref("images").child(filename).getDownloadURL();
        await this.setState({ avatarUrl: url });
        return url;
    }
    handleProgress = progress => this.setState({ progress: progress });

    //for Edit
    getMember = async (uid) => {
        const docRef = db.collection("members").doc(uid);
        const doc = await docRef.get();
        if (doc.exists) {
            this.setState({
                member: doc.data(),
            });
        } else {
            alert("メンバーが見つかりませんでした。");
        }
    }

    componentDidMount = () => {
        this.getMember(this.props.match.params.uid);
    }

    deleteMember = async (uid) => {
        if (window.confirm('本当に削除しますか?')) {
            await db.collection("members").doc(uid).delete();
            this.props.history.push("/");
        } else {
            return;
        }
    }

    render() {
        return (
            <div className="container">
                <h3 className="text-center my-5">情報編集</h3>
                <div className="text-right my-3 mr-5"><Link to="/">一覧へ戻る</Link></div>
                <Formik
                    enableReinitialize //これがポイント
                    initialValues={{
                        email: this.state.member.email, //各初期値にdbからの値(このためにenableReinitializeが必要)
                        area: this.state.member.area,
                        gender: this.state.member.gender,
                        birthday: moment(this.state.member.birthday.seconds * 1000).format('YYYY/MM/DD'),
                        avatarUrl: this.state.member.avatarUrl,
                        agree: this.state.member.agree,
                    }}
                    onSubmit={this.handleOnSubmit}
                    validationSchema={Yup.object().shape({
                        email: Yup.string().email().required(),
                        area: Yup.string().oneOf(['関東', '関西']).required(),
                        gender: Yup.string().oneOf(['male', 'female']).required(),
                        avatarUrl: Yup.string().required(),
                        agree: Yup.boolean().oneOf([true]).required(),
                    })}
                >
                    {
                        ({ handleSubmit, handleChange, handleBlur, values, errors, touched, setFieldValue }) => (
                            <Form className="col-8 mx-auto" onSubmit={handleSubmit}>
                                <FormGroup>
                                    <Label for="email">Email</Label>
                                    <Input
                                        type="email"
                                        name="email"
                                        id="email"
                                        value={values.email}
                                        onChange={handleChange}
                                        onBlur={handleBlur}
                                        invalid={Boolean(touched.email && errors.email)}
                                    />
                                    <FormFeedback>
                                        {errors.email}
                                    </FormFeedback>
                                </FormGroup>
                                <FormGroup>
                                    <Label>■お住いの地域</Label>
                                    <Input
                                        type="select"
                                        name="area"
                                        id="area"
                                        value={values.area}
                                        onChange={handleChange}
                                        onBlur={handleBlur}
                                        invalid={Boolean(touched.area && errors.area)}
                                    >
                                        <option value="選択して下さい">選択して下さい</option>
                                        <option value="関東">関東</option>
                                        <option value="関西">関西</option>
                                    </Input>
                                    <FormFeedback>
                                        {errors.area}
                                    </FormFeedback>
                                </FormGroup>
                                <FormGroup className="mb-4">
                                    <legend className="col-form-label">■性別</legend>
                                    <FormGroup inline check>
                                        <Label check>
                                            男性:<Input
                                                type="radio"
                                                name="gender"
                                                id="male"
                                                value="male"
                                                onChange={handleChange}
                                                checked={values.gender === "male"} //追加
                                            />
                                        </Label>
                                    </FormGroup>
                                    <FormGroup inline check>
                                        <Label check>
                                            女性:<Input
                                                type="radio"
                                                name="gender"
                                                id="female"
                                                value="female"
                                                onChange={handleChange}
                                                checked={values.gender === "female"} //追加
                                            />
                                        </Label>
                                    </FormGroup>
                                    <span className="text-danger small">{touched.gender && errors.gender ? errors.gender : null}</span>
                                </FormGroup>
                                <FormGroup>
                                    <legend className="col-form-label">■直近の誕生日</legend>
                                    <DatePicker
                                        locale="ja"
                                        name="birthday"
                                        id="birthday"
                                        value={values.birthday}
                                        dateFormat="yyyy/MM/dd"
                                        customInput={<Input invalid={Boolean(errors.birthday)} />}
                                        onChange={date => setFieldValue("birthday", moment(date).format('YYYY/MM/DD'))}
                                    />
                                </FormGroup>
                                <FormGroup>
                                    <legend className="col-form-label">■プロファイル画像</legend>
                                    <FileUploader
                                        accept="image/*"
                                        name="avatarUrl"
                                        randomizeFilename
                                        storageRef={firebase.storage().ref("images")}
                                        onUploadStart={this.handleUploadStart}
                                        onUploadError={this.handleUploadError}
                                        onUploadSuccess={async (filename) => {
                                            const path = await this.handleUploadSuccess(filename); //ここもawaitにしないとurl取得できない
                                            setFieldValue("avatarUrl", path); //値のセットとエラーの削除
                                        }}
                                        onProgress={this.handleProgress}
                                    />
                                    <span className="text-danger small">{touched.avatarUrl && errors.avatarUrl ? errors.avatarUrl : null}</span>
                                    {this.state.isUploading ? <p>Uploading... {this.state.progress}%</p> : null}
                                    {/* sateからvaluesへ参照を切り替え */}
                                    {values.avatarUrl ? <img src={values.avatarUrl} width="120" alt="" className="my-2" /> : null}
                                </FormGroup>
                                <FormGroup className="my-4">
                                    <legend className="col-form-label">■規約に同意して下さい。</legend>
                                    <FormGroup inline check>
                                        <Input
                                            type="checkbox"
                                            name="agree"
                                            id="agree"
                                            value={values.agree}
                                            onChange={handleChange}
                                            checked={values.agree === true}
                                        />
                                        <Label for="agree" check>同意する。</Label>
                                        <span className="text-danger small">{touched.agree && errors.agree ? errors.agree : null}</span>
                                    </FormGroup>

                                </FormGroup>
                                <div>
                                    <Button type="submit" color="success">
                                        <Spinner color="light" size="sm" className="mr-1" hidden={this.state.spinnerHidden} />
                                        更新する
                                        </Button>
                                </div>
                            </Form>
                        )
                    }
                </Formik>
                <div className="col-8 mx-auto my-3">
                    <Button color="danger" onClick={() => this.deleteMember(this.props.match.params.uid)}>データを削除</Button>
                </div>
            </div>
        );
    }
}

export default Edit;

Show.js

Editと表示内容はほぼ同じなので、いらないのですが参考まで。
テキストベースで表示するため型変換等が必要になります。

Show.js
import React from 'react';
import firebase, { db } from '../Firebase';
import { Link } from 'react-router-dom';
import moment from 'moment';


class Show extends React.Component {

    state = {
        member: {}
    }

    getMember = async (uid) => {
        const docRef = db.collection("members").doc(uid);
        const doc = await docRef.get();
        if (doc.exists) {
            this.setState({
                member: doc.data(),
            });
        } else {
            alert("メンバーが見つかりませんでした。");
        }
    }

    componentDidMount = async () => {
        await this.getMember(this.props.match.params.uid);
    }

    render() {

        if(this.state.member.createdAt === undefined){
            return <p>Loading...</p>
        }

        return (
            <div className="container">
                <h3 className="text-center my-5">メンバー詳細</h3>
                <div className="text-right my-3 mr-5"><Link to="/">一覧へ戻る</Link></div>
                <table className="table">
                    <tbody>
                        <tr>
                            <th>UID</th>
                            <td>{this.state.member.docId}</td>
                        </tr>
                        <tr>
                            <th>Email</th>
                            <td>{this.state.member.email}</td>
                        </tr>
                        <tr>
                            <th>居住地域</th>
                            <td>{this.state.member.area}</td>
                        </tr>
                        <tr>
                            <th>Avatar</th>
                            <td><img src={this.state.member.avatarUrl} width="200" alt="" /></td>
                        </tr>
                        <tr>
                            <th>性別</th>
                            <td>{this.state.member.gender}</td>
                        </tr>
                        <tr>
                            <th>生年月日?</th>
                            <td>{moment(this.state.member.birthday.seconds * 1000).format('YYYY/MM/DD')}</td>
                        </tr>
                        <tr>
                            <th>同意</th>
                            <td>{String(this.state.member.agree)}</td>
                        </tr>
                        <tr>
                            <th>登録日時</th>
                            <td>{ moment(this.state.member.createdAt.seconds * 1000).format('YYYY-MM-DD HH:mm:dd:ss')}</td>
                        </tr>
                    </tbody>
                </table>
            </div>
        );
    }
}

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

HUGO で React環境構築 らくらくSPA??

:calendar_spiral: i-plug Advent Calendar 2019 の【3日目】の記事です:santa::tada:

らくらくSPAを目指してみた

HUGOにReactを積んでらくらくSAP構築みたいなことを目指します。

HUGOとは

Go言語で作成された静的サイトジェネレーターで、世界でもっとも速くビルドできるらしい。
デフォルトでは管理画面がなく、直接マークダウンファイル編集をするため、プログラマ向けの仕様っぽいよ。

そんな爆速を売りにしているHUGOにわざわざReact.jsを積まなくても...。

まずはHUGOセットアップ

:one: インストール

$ brew install hugo



:two: テンプレ生成

$  hugo new site my−hugo-app

vanillaなHUGOの雛形が生成されます。


:three: themeを設定
公式themeページから自分の好きなThemeを引っ張ってくる。おすすめはminimalistからチョイスすること。カスタマイズしやすいです :nerd: :nerd:
config.tomlに使うthemeを明示します。

$ cd my-hugo-app
$ git init
$ git submodule add https://gitlab.com/meibenny/elephants.git themes/elephants
$ echo 'theme = "elephants"' >> config.toml


:four: 起動

$ hugo server

これでローカルホストにトップページが出てきます。簡単にサイトが作れますね :nerd: :nerd:

HUGO に Reactを積む

当たり前なんですけどWebpackつかいます。

:one: npmでいろいろ準備
まずはnpmを準備しましょう。ディレクトは以下のようにしましょう。

.
├── archetypes
│   └── default.md
├── config.toml
├── content
├── data
├── layouts
│   ├── 404.html
│   ├── _default
│   │   ├── list.html
│   │   └── single.html
│   ├── index.html
│   └── partials
│       ├── footer.html
│       └── header.html
├── resources
│   └── _gen
│       ├── assets
│       │   └── js
│       │       └── package.json
│       └── images
├── static
│   └── js
└── themes
    └── elephants
        ├── LICENSE.md
        ├── README.md
        ├── archetypes
        │   └── default.md
        ├── exampleSite
        │   ├── LICENSE
        │   ├── config.toml
        │   └── content
        │       └── posts
        │           ├── happy-new-year.md
        │           ├── my-example-post.md
        │           └── my-first-post.md
        ├── images
        │   ├── screenshot.png
        │   └── tn.png
        ├── layouts
        │   ├── 404.html
        │   ├── _default
        │   │   ├── list.html
        │   │   └── single.html
        │   ├── index.html
        │   └── partials
        │       ├── footer.html
        │       └── header.html
        ├── static
        │   └── css
        │       └── stylesheet.css
        └── theme.toml

あとでhtmlも若干いじるため、themesのlayoutを自前のもうにコピーしておきます。
resource/_gen/assetsのディレクトリにもjsディレクトリを追加しておきます。

$ cd resource/_gen/assets/js
$ npm init
$ npm install --save react react-dom
$ npm install --save-dev webpack webpack-cli
$ npm install --save-dev babel-loader@7 babel-core babel-preset-es2015 babel-preset-react

必要なものはだいたい入れれたはず :nerd: :nerd:

:two: 設定ファイルを記述する

touch .babelrctouch webpack.config.jsで設定ファイルを同じディレクトリ階層につくっておきます。

作った .babelrc にes2015にコンパイルするように記述します。

{"presets":
    [
        "react", "es2015"
    ]
}

次は webpack.config.js :nerd:

module.exports = {
    entry: './app.js',
    output: {
      path: `${__dirname}/../../../../static/js`,
      filename: 'bundle.js'
    },
    module: {
      rules: [{
        exclude: /node_modules/,
        loader: 'babel-loader'
      }]
    },
}

:three: React.jsを書く

お好みの処理を :nerd:

import React from 'react'
import ReactDOM from 'react-dom'

import Body from './components/Body'

const App = () => {
  return (
    <div>
      <div>Hello World!</div>
      <Body />
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById("root-react"))

:four: コンパイル :robot:
package.json に追記します :nerd:


  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode development --open"
  },

いざコンパイル!

$ npm run build

コンパイルできていると
static/jsの階層にbandle.jsとしてjsファイルが吐き出されています。

あとはhtmlファイルに読み込ませるだけです :nerd: :nerd:

こんな感じになるかと思います。

スクリーンショット 2019-11-16 23.32.04.png
白い :snowflake: いいね!

デプロイする

JavaScriptなので静的ホスティングサービスで動きます。
APIを用意すればデータのやりとりもできるのでサーバーレスでアプリケーションを作り上げることもできます。
今回は公開されているAPIを使って国名を入力すればその情報を取得するアプリにしてみました。

そして仕様するホスティングサービスは使うのはNetlify :nerd:

いざデプロイ!
スクリーンショット 2019-11-17 11.22.51.png

成功!そしてアクセスだ!!
スクリーンショット 2019-11-17 17.44.47.png

動いたー!! :robot: やったーできたー!!




ちょっとまって :boy_tone1: それ意味あるんですか?




...ブーーーン:police_car: :police_car:
Gatsby.jsでできるよそれ:cop: :cop:
てかGatsby.jsのほうがそれ得意だよ:cop: :cop:

うわぁぁぁあぁぁあ... :nerd: :nerd: :nerd: :nerd: 目の前が真っ暗になった。

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

jwtについて

YoutubeのfreeCodeCamp.orgでJWTについて動画があったのでまとめます。自分用に(笑。

jsonwebtokenの使い方について https://www.npmjs.com/package/jsonwebtoken。

この動画の中でserver側とfront側で分けていたのでそれに習います。

server側では
cors と cookie-parserを使ってログイン情報を受け取ります。
https://www.npmjs.com/package/corsから

Simple Usage (Enable All CORS Requests)
var express = require('express')
var cors = require('cors')
var app = express()

app.use(cors())

app.get('/products/:id', function (req, res, next) {
  res.json({msg: 'This is CORS-enabled for all origins!'})
})

app.listen(80, function () {
  console.log('CORS-enabled web server listening on port 80')
})

cookie-parser(https://www.npmjs.com/package/cookie-parser)

Example
var express = require('express')
var cookieParser = require('cookie-parser')

var app = express()
app.use(cookieParser())

app.get('/', function (req, res) {
  // Cookies that have not been signed
  console.log('Cookies: ', req.cookies)

  // Cookies that have been signed
  console.log('Signed Cookies: ', req.signedCookies)
})

app.listen(8080)

githubにアップされていたコードのコメントがわかりやすいのでそのまま上げます。

https://github.com/weibenfalk/jwtToken-react-express/blob/master/server/src/index.js

require('dotenv/config');
const express = require('express');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const { verify } = require('jsonwebtoken');
const { hash, compare } = require('bcryptjs');
const {
  createAccessToken,
  createRefreshToken,
  sendRefreshToken,
  sendAccessToken,
} = require('./tokens.js');
const { fakeDB } = require('./fakeDB.js');
const { isAuth } = require('./isAuth.js');

// 1. Register a user
// 2. Login a user
// 3. Logout a user
// 4. Setup a protected route
// 5. Get a new accesstoken with a refresh token

const server = express();

// Use express middleware for easier cookie handling
server.use(cookieParser());

server.use(
  cors({
    origin: 'http://localhost:3000',
    credentials: true,
  }),
);

// Needed to be able to read body data
server.use(express.json()); // to support JSON-encoded bodies
server.use(express.urlencoded({ extended: true })); // to support URL-encoded bodies

// 1. Register a user
server.post('/register', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Check if the user exist
    const user = fakeDB.find(user => user.email === email);
    if (user) throw new Error('User already exist');
    // 2. If not user exist already, hash the password
    const hashedPassword = await hash(password, 10);
    // 3. Insert the user in "database"
    fakeDB.push({
      id: fakeDB.length,
      email,
      password: hashedPassword,
    });
    res.send({ message: 'User Created' });
    console.log(fakeDB);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

// 2. Login a user
server.post('/login', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Find user in array. If not exist send error
    const user = fakeDB.find(user => user.email === email);
    if (!user) throw new Error('User does not exist');
    // 2. Compare crypted password and see if it checks out. Send error if not
    const valid = await compare(password, user.password);
    if (!valid) throw new Error('Password not correct');
    // 3. Create Refresh- and Accesstoken
    const accesstoken = createAccessToken(user.id);
    const refreshtoken = createRefreshToken(user.id);
    // 4. Store Refreshtoken with user in "db"
    // Could also use different version numbers instead.
    // Then just increase the version number on the revoke endpoint
    user.refreshtoken = refreshtoken;
    // 5. Send token. Refreshtoken as a cookie and accesstoken as a regular response
    sendRefreshToken(res, refreshtoken);
    sendAccessToken(res, req, accesstoken);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

// 3. Logout a user
server.post('/logout', (_req, res) => {
  res.clearCookie('refreshtoken', { path: '/refresh_token' });
  // Logic here for also remove refreshtoken from db
  return res.send({
    message: 'Logged out',
  });
});

// 4. Protected route
server.post('/protected', async (req, res) => {
  try {
    const userId = isAuth(req);
    if (userId !== null) {
      res.send({
        data: 'This is protected data.',
      });
    }
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

// 5. Get a new access token with a refresh token
server.post('/refresh_token', (req, res) => {
  const token = req.cookies.refreshtoken;
  // If we don't have a token in our request
  if (!token) return res.send({ accesstoken: '' });
  // We have a token, let's verify it!
  let payload = null;
  try {
    payload = verify(token, process.env.REFRESH_TOKEN_SECRET);
  } catch (err) {
    return res.send({ accesstoken: '' });
  }
  // token is valid, check if user exist
  const user = fakeDB.find(user => user.id === payload.userId);
  if (!user) return res.send({ accesstoken: '' });
  // user exist, check if refreshtoken exist on user
  if (user.refreshtoken !== token)
    return res.send({ accesstoken: '' });
  // token exist, create new Refresh- and accesstoken
  const accesstoken = createAccessToken(user.id);
  const refreshtoken = createRefreshToken(user.id);
  // update refreshtoken on user in db
  // Could have different versions instead!
  user.refreshtoken = refreshtoken;
  // All good to go, send new refreshtoken and accesstoken
  sendRefreshToken(res, refreshtoken);
  return res.send({ accesstoken });
});

server.listen(process.env.PORT, () =>
  console.log(`Server listening on port ${process.env.PORT}!`),
);

会員登録のメソッドについて

server.post('/register', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Check if the user exist
    const user = fakeDB.find(user => user.email === email);
    if (user) throw new Error('User already exist');
    // 2. If not user exist already, hash the password
    const hashedPassword = await hash(password, 10);
    // 3. Insert the user in "database"
    fakeDB.push({
      id: fakeDB.length,
      email,
      password: hashedPassword,
    });
    res.send({ message: 'User Created' });
    console.log(fakeDB);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

DBの中に会員データが見つかったらエラーを返し、見つからなかった場合DBにデータ挿入と板感じですね。

会員ログインのメソッドについて

// 2. Login a user
server.post('/login', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Find user in array. If not exist send error
    const user = fakeDB.find(user => user.email === email);
    if (!user) throw new Error('User does not exist');
    // 2. Compare crypted password and see if it checks out. Send error if not
    const valid = await compare(password, user.password);
    if (!valid) throw new Error('Password not correct');
    // 3. Create Refresh- and Accesstoken
    const accesstoken = createAccessToken(user.id);
    const refreshtoken = createRefreshToken(user.id);
    // 4. Store Refreshtoken with user in "db"
    // Could also use different version numbers instead.
    // Then just increase the version number on the revoke endpoint
    user.refreshtoken = refreshtoken;
    // 5. Send token. Refreshtoken as a cookie and accesstoken as a regular response
    sendRefreshToken(res, refreshtoken);
    sendAccessToken(res, req, accesstoken);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

説明しなくても解ると思います。(笑
念の為このコードで使われているAccesstoken、Refreshtokenについては

https://auth0.com/blog/jp-refresh-tokens-what-are-they-and-when-to-use-them/ 
 に書かれている通り

Access Token はリソースに直接アクセスするために必要な情報を保持しています。つまり、クライアントがリソースを管理するサーバーにAccess Tokenをパスするとき、そのサーバーはそのトークンに含まれている情報を使用してクライアントが認可したものかを判断します。Access Tokenには通常、有効期限があり、存続期間は短いです。
Refresh Token は新しいAccess Tokenを取得するために必要な情報を保持しています。つまり、特定リソースにアクセスする際に、Access Tokenが必要な場合には、クライアントはAuthorization Serverが発行する新しいAccess Tokenを取得するためにRefresh Tokenを使用します。一般的な使用方法は、Access Tokenの期限が切れた後に新しいものを取得したり、初めて新しいリソースにアクセスするときなどです。Refresh Tokenにも有効期限がありますが、存続期間はAccess Tokenよりも長くなっています。Refresh Tokenは通常、漏洩しないように厳しいストレージ要件が課せられます。Authorization Serverによってブラックリストに載ることもあります。

src/isAuth.jsについて

const { verify } = require('jsonwebtoken');

const isAuth = req => {
  const authorization = req.headers['authorization'];
  if (!authorization) throw new Error('You need to login.');
  // Based on 'Bearer ksfljrewori384328289398432'
  const token = authorization.split(' ')[1];
  const { userId } = verify(token, process.env.ACCESS_TOKEN_SECRET);
  return userId;
};

module.exports = {
  isAuth,
};

jsのsplit文についてはhttps://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/split

var str = 'The quick brown fox jumps over the lazy dog.';

var words = str.split(' ');
console.log(words[3]);
// expected output: "fox"

var chars = str.split('');
console.log(chars[8]);
// expected output: "k"

var strCopy = str.split();
console.log(strCopy);
// expected output: Array ["The quick brown fox jumps over the lazy dog."]

front側については
src/App.js

import React, { useState, useEffect } from 'react';
import { Router, navigate } from '@reach/router';

import Navigation from './components/Navigation';
import Login from './components/Login';
import Register from './components/Register';
import Protected from './components/Protected';
import Content from './components/Content';

export const UserContext = React.createContext([]);

function App() {
  const [user, setUser] = useState({});
  const [loading, setLoading] = useState(true);

  const logOutCallback = async () => {
    await fetch('http://localhost:4000/logout', {
      method: 'POST',
      credentials: 'include', // Needed to include the cookie
    });
    // Clear user from context
    setUser({});
    // Navigate back to startpage
    navigate('/');
  }

  // First thing, check if a refreshtoken exist
  useEffect(() => {
    async function checkRefreshToken() {
      const result = await (await fetch('http://localhost:4000/refresh_token', {
        method: 'POST',
        credentials: 'include', // Needed to include the cookie
        headers: {
          'Content-Type': 'application/json',
        }
      })).json();
        setUser({
          accesstoken: result.accesstoken,
        });
        setLoading(false);
    }
    checkRefreshToken();
  }, []);

  if (loading) return <div>Loading ...</div>

  return (
    <UserContext.Provider value={[user, setUser]}>
      <div className="app">
        <Navigation logOutCallback={logOutCallback} />
        <Router id="router">
          <Login path="login" />
          <Register path="register" />
          <Protected path="protected" />
          <Content path="/" />
        </Router>
      </div>
    </UserContext.Provider>
  );
}

export default App;

src/components/Register.jsについては

import React, { useState } from 'react';
import { navigate } from '@reach/router';

const Register = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = async e => {
    e.preventDefault();
    const result = await (await fetch('http://localhost:4000/register', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: email,
        password: password,
      }),
    })).json();
    if (!result.error) {
      console.log(result.message);
      navigate('/');
    } else {
      console.log(result.error);
    }
  };

  const handleChange = e => {
    if (e.currentTarget.name === 'email') {
      setEmail(e.currentTarget.value);
    } else {
      setPassword(e.currentTarget.value);
    }
  };

  return (
    <div className="login-wrapper">
      <form onSubmit={handleSubmit}>
        <div>Register</div>
        <div className="login-input">
          <input
            value={email}
            onChange={handleChange}
            type="text"
            name="email"
            placeholder="Email"
            autoComplete="email"
          />
          <input
            value={password}
            onChange={handleChange}
            type="password"
            name="password"
            autoComplete="current-password"
            placeholder="Password"
          />
          <button type="submit">Register</button>
        </div>
      </form>
    </div>
  );
};

export default Register;

front側についてはそのままありのままなので詳しくは説明できません。
見てくれた方には心苦しいですが、赦してほしいです。
では、また。

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

JWTについて

YoutubeのfreeCodeCamp.orgでJWTについて動画があったのでまとめます。自分用に(笑。

jsonwebtokenの使い方について https://www.npmjs.com/package/jsonwebtoken

この動画の中でserver側とfront側で分けていたのでそれに習います。

server側では
cors と cookie-parserを使ってログイン情報を受け取ります。
https://www.npmjs.com/package/corsから

Simple Usage (Enable All CORS Requests)
var express = require('express')
var cors = require('cors')
var app = express()

app.use(cors())

app.get('/products/:id', function (req, res, next) {
  res.json({msg: 'This is CORS-enabled for all origins!'})
})

app.listen(80, function () {
  console.log('CORS-enabled web server listening on port 80')
})

cookie-parser(https://www.npmjs.com/package/cookie-parser)

Example
var express = require('express')
var cookieParser = require('cookie-parser')

var app = express()
app.use(cookieParser())

app.get('/', function (req, res) {
  // Cookies that have not been signed
  console.log('Cookies: ', req.cookies)

  // Cookies that have been signed
  console.log('Signed Cookies: ', req.signedCookies)
})

app.listen(8080)

githubにアップされていたコードのコメントがわかりやすいのでそのまま上げます。

https://github.com/weibenfalk/jwtToken-react-express/blob/master/server/src/index.js

require('dotenv/config');
const express = require('express');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const { verify } = require('jsonwebtoken');
const { hash, compare } = require('bcryptjs');
const {
  createAccessToken,
  createRefreshToken,
  sendRefreshToken,
  sendAccessToken,
} = require('./tokens.js');
const { fakeDB } = require('./fakeDB.js');
const { isAuth } = require('./isAuth.js');

// 1. Register a user
// 2. Login a user
// 3. Logout a user
// 4. Setup a protected route
// 5. Get a new accesstoken with a refresh token

const server = express();

// Use express middleware for easier cookie handling
server.use(cookieParser());

server.use(
  cors({
    origin: 'http://localhost:3000',
    credentials: true,
  }),
);

// Needed to be able to read body data
server.use(express.json()); // to support JSON-encoded bodies
server.use(express.urlencoded({ extended: true })); // to support URL-encoded bodies

// 1. Register a user
server.post('/register', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Check if the user exist
    const user = fakeDB.find(user => user.email === email);
    if (user) throw new Error('User already exist');
    // 2. If not user exist already, hash the password
    const hashedPassword = await hash(password, 10);
    // 3. Insert the user in "database"
    fakeDB.push({
      id: fakeDB.length,
      email,
      password: hashedPassword,
    });
    res.send({ message: 'User Created' });
    console.log(fakeDB);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

// 2. Login a user
server.post('/login', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Find user in array. If not exist send error
    const user = fakeDB.find(user => user.email === email);
    if (!user) throw new Error('User does not exist');
    // 2. Compare crypted password and see if it checks out. Send error if not
    const valid = await compare(password, user.password);
    if (!valid) throw new Error('Password not correct');
    // 3. Create Refresh- and Accesstoken
    const accesstoken = createAccessToken(user.id);
    const refreshtoken = createRefreshToken(user.id);
    // 4. Store Refreshtoken with user in "db"
    // Could also use different version numbers instead.
    // Then just increase the version number on the revoke endpoint
    user.refreshtoken = refreshtoken;
    // 5. Send token. Refreshtoken as a cookie and accesstoken as a regular response
    sendRefreshToken(res, refreshtoken);
    sendAccessToken(res, req, accesstoken);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

// 3. Logout a user
server.post('/logout', (_req, res) => {
  res.clearCookie('refreshtoken', { path: '/refresh_token' });
  // Logic here for also remove refreshtoken from db
  return res.send({
    message: 'Logged out',
  });
});

// 4. Protected route
server.post('/protected', async (req, res) => {
  try {
    const userId = isAuth(req);
    if (userId !== null) {
      res.send({
        data: 'This is protected data.',
      });
    }
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

// 5. Get a new access token with a refresh token
server.post('/refresh_token', (req, res) => {
  const token = req.cookies.refreshtoken;
  // If we don't have a token in our request
  if (!token) return res.send({ accesstoken: '' });
  // We have a token, let's verify it!
  let payload = null;
  try {
    payload = verify(token, process.env.REFRESH_TOKEN_SECRET);
  } catch (err) {
    return res.send({ accesstoken: '' });
  }
  // token is valid, check if user exist
  const user = fakeDB.find(user => user.id === payload.userId);
  if (!user) return res.send({ accesstoken: '' });
  // user exist, check if refreshtoken exist on user
  if (user.refreshtoken !== token)
    return res.send({ accesstoken: '' });
  // token exist, create new Refresh- and accesstoken
  const accesstoken = createAccessToken(user.id);
  const refreshtoken = createRefreshToken(user.id);
  // update refreshtoken on user in db
  // Could have different versions instead!
  user.refreshtoken = refreshtoken;
  // All good to go, send new refreshtoken and accesstoken
  sendRefreshToken(res, refreshtoken);
  return res.send({ accesstoken });
});

server.listen(process.env.PORT, () =>
  console.log(`Server listening on port ${process.env.PORT}!`),
);

会員登録のメソッドについて

server.post('/register', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Check if the user exist
    const user = fakeDB.find(user => user.email === email);
    if (user) throw new Error('User already exist');
    // 2. If not user exist already, hash the password
    const hashedPassword = await hash(password, 10);
    // 3. Insert the user in "database"
    fakeDB.push({
      id: fakeDB.length,
      email,
      password: hashedPassword,
    });
    res.send({ message: 'User Created' });
    console.log(fakeDB);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

DBの中に会員データが見つかったらエラーを返し、見つからなかった場合DBにデータ挿入と板感じですね。

会員ログインのメソッドについて

// 2. Login a user
server.post('/login', async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Find user in array. If not exist send error
    const user = fakeDB.find(user => user.email === email);
    if (!user) throw new Error('User does not exist');
    // 2. Compare crypted password and see if it checks out. Send error if not
    const valid = await compare(password, user.password);
    if (!valid) throw new Error('Password not correct');
    // 3. Create Refresh- and Accesstoken
    const accesstoken = createAccessToken(user.id);
    const refreshtoken = createRefreshToken(user.id);
    // 4. Store Refreshtoken with user in "db"
    // Could also use different version numbers instead.
    // Then just increase the version number on the revoke endpoint
    user.refreshtoken = refreshtoken;
    // 5. Send token. Refreshtoken as a cookie and accesstoken as a regular response
    sendRefreshToken(res, refreshtoken);
    sendAccessToken(res, req, accesstoken);
  } catch (err) {
    res.send({
      error: `${err.message}`,
    });
  }
});

説明しなくても解ると思います。(笑
念の為このコードで使われているAccesstoken、Refreshtokenについては
https://auth0.com/blog/jp-refresh-tokens-what-are-they-and-when-to-use-them/
に書かれている通り

Access Token はリソースに直接アクセスするために必要な情報を保持しています。つまり、クライアントがリソースを管理するサーバーにAccess Tokenをパスするとき、そのサーバーはそのトークンに含まれている情報を使用してクライアントが認可したものかを判断します。Access Tokenには通常、有効期限があり、存続期間は短いです。
Access Token

Refresh Token は新しいAccess Tokenを取得するために必要な情報を保持しています。つまり、特定リソースにアクセスする際に、Access Tokenが必要な場合には、クライアントはAuthorization Serverが発行する新しいAccess Tokenを取得するためにRefresh Tokenを使用します。一般的な使用方法は、Access Tokenの期限が切れた後に新しいものを取得したり、初めて新しいリソースにアクセスするときなどです。Refresh Tokenにも有効期限がありますが、存続期間はAccess Tokenよりも長くなっています。Refresh Tokenは通常、漏洩しないように厳しいストレージ要件が課せられます。Authorization Serverによってブラックリストに載ることもあります。

src/isAuth.jsについて

const { verify } = require('jsonwebtoken');

const isAuth = req => {
  const authorization = req.headers['authorization'];
  if (!authorization) throw new Error('You need to login.');
  // Based on 'Bearer ksfljrewori384328289398432'
  const token = authorization.split(' ')[1];
  const { userId } = verify(token, process.env.ACCESS_TOKEN_SECRET);
  return userId;
};

module.exports = {
  isAuth,
};

jsのsplit文についてはhttps://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/split

var str = 'The quick brown fox jumps over the lazy dog.';

var words = str.split(' ');
console.log(words[3]);
// expected output: "fox"

var chars = str.split('');
console.log(chars[8]);
// expected output: "k"

var strCopy = str.split();
console.log(strCopy);
// expected output: Array ["The quick brown fox jumps over the lazy dog."]

front側については
src/App.js

import React, { useState, useEffect } from 'react';
import { Router, navigate } from '@reach/router';

import Navigation from './components/Navigation';
import Login from './components/Login';
import Register from './components/Register';
import Protected from './components/Protected';
import Content from './components/Content';

export const UserContext = React.createContext([]);

function App() {
  const [user, setUser] = useState({});
  const [loading, setLoading] = useState(true);

  const logOutCallback = async () => {
    await fetch('http://localhost:4000/logout', {
      method: 'POST',
      credentials: 'include', // Needed to include the cookie
    });
    // Clear user from context
    setUser({});
    // Navigate back to startpage
    navigate('/');
  }

  // First thing, check if a refreshtoken exist
  useEffect(() => {
    async function checkRefreshToken() {
      const result = await (await fetch('http://localhost:4000/refresh_token', {
        method: 'POST',
        credentials: 'include', // Needed to include the cookie
        headers: {
          'Content-Type': 'application/json',
        }
      })).json();
        setUser({
          accesstoken: result.accesstoken,
        });
        setLoading(false);
    }
    checkRefreshToken();
  }, []);

  if (loading) return <div>Loading ...</div>

  return (
    <UserContext.Provider value={[user, setUser]}>
      <div className="app">
        <Navigation logOutCallback={logOutCallback} />
        <Router id="router">
          <Login path="login" />
          <Register path="register" />
          <Protected path="protected" />
          <Content path="/" />
        </Router>
      </div>
    </UserContext.Provider>
  );
}

export default App;

src/components/Register.jsについては

import React, { useState } from 'react';
import { navigate } from '@reach/router';

const Register = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = async e => {
    e.preventDefault();
    const result = await (await fetch('http://localhost:4000/register', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: email,
        password: password,
      }),
    })).json();
    if (!result.error) {
      console.log(result.message);
      navigate('/');
    } else {
      console.log(result.error);
    }
  };

  const handleChange = e => {
    if (e.currentTarget.name === 'email') {
      setEmail(e.currentTarget.value);
    } else {
      setPassword(e.currentTarget.value);
    }
  };

  return (
    <div className="login-wrapper">
      <form onSubmit={handleSubmit}>
        <div>Register</div>
        <div className="login-input">
          <input
            value={email}
            onChange={handleChange}
            type="text"
            name="email"
            placeholder="Email"
            autoComplete="email"
          />
          <input
            value={password}
            onChange={handleChange}
            type="password"
            name="password"
            autoComplete="current-password"
            placeholder="Password"
          />
          <button type="submit">Register</button>
        </div>
      </form>
    </div>
  );
};

export default Register;

front側についてはそのままありのままなので詳しくは説明できません。
見てくれた方には心苦しいですが、赦してほしいです。
では、また。

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

react-hooks-asyncの紹介

はじめに

本稿では、React Hooksで非同期処理をするためのライブラリを紹介します。React HooksのuseEffectを使うと、非同期処理の記述は比較的簡単にできます。一方で、useEffectを使う際はcleanup処理が重要ですが、しばしば忘れられることがあります。custom hooksを使うとその辺りを明確にすることができます。

react-hooks-asyncとは

本ライブラリのリポジトリはこちらです。

https://github.com/dai-shi/react-hooks-async

useEffectのcleanupでは非同期処理を中断することがポイントになります。JavaScriptのPromiseには中断の仕組みは用意されていません。この問題について、redux-thunkやredux-observableはそれぞれのインタフェースを提供して解決しています。

react-hooks-asyncはAbortControllerを使ったインタフェースを提供します。AbortControllerはPromiseではキャンセル処理をできないことに対して、主にFetch API向けに用意されたDOMのAPIです。通常のブラウザでは既に使えるようになっています。polyfillも容易です。react-hooks-asyncで使う非同期関数はAbortControllerのインスタンスを第一引数で受け取る規約とします。つまり、

const asyncFunction = async (abortContoller) => {
  // abortControllerのキャンセル処理を実現するよう処理を書く
  return ...; // 結果を返す
};

のような形になります。

コアAPI

react-hooks-asyncが提供するコアのAPIはuseAsyncTaskuseAsyncRunの二つです。なぜ二つに分かれているかは後ほど説明します。これらのAPIは例えば次のように使います。

const asyncFunction = ...; // 規約に沿った非同期関数

const Component = () => {
  const task = useAsyncTask(asyncFunction);
  useAsyncRun(task);
  return (
    <div>
      <div>Pending: {JSON.stringify(task.pending)}</div>
      <div>Error: {JSON.stringify(task.error)}<div>
      <div>Result: {JSON.stringify(task.result)}<div>
    </div>
  );
};

または、custom hookにすることもできます。ついでに引数を受け付けるようにもしてみましょう。

const useAsyncFunction = (arg1, arg2) => {
  const func = useCallback(async (abortContoller) => {
    // abortContollerの処理
    return ...; // arg1, arg2を使った計算結果など
  }, [arg1, arg2]);
  const task = useAsyncTask(func);
  useAsyncRun(task);
  return task;
};

ヘルパーAPI

useAsyncTaskを使うためは規約に沿ったasyncFunctionが必要ですが、よく使う関数はcustom hooksとしてヘルパーAPIとして用意されています。ヘルパーAPIは現時点では5つあります。

  • useAsyncTaskTimeout
  • useAsyncTaskDelay
  • useAsyncTaskFetch
  • useAsyncTaskAxios
  • useAsyncTaskWasm

今回は、useAsyncTaskFetchについて紹介します。このAPIはFetch APIの引数を基本的にそのままhookの形にしたものです。第一引数はinputで、第二引数はinitで省略可能です。さらに第三引数にreadBody関数を指定できますが、JSON APIの場合は指定不要です。

使用方法は次のようになります。

const Component = ({ userId }) => {
  const url = `https://reqres.in/api/users/${id}?delay=1`;
  const task = useAsyncTaskFetch(url);
  useAsyncRun(task);
  const { pending, error, result } = task;
  return (
    <div>
      <div>Pending: {JSON.stringify(pending)}</div>
      <div>Error: {JSON.stringify(error)}<div>
      <div>Result: {JSON.stringify(result)}<div>
    </div>
  );
};

実は、useAsyncTaskFetchとuseAsyncRunを組み合わせたuseFetchというAPIもあります。これを使うと、

const { pending, error, result } = useFetch(url);

のように簡潔に書くことができ、今回の例ではこれで十分です。

結合API

react-hooks-asyncの特徴として、taskの結合があります。結合のためのAPIは3つあります。

  • useAsyncCombineAll
  • useAsyncCombineReace
  • useAsyncCombineSeq

これらのAPIもAbortControllerを利用する規約に沿っていますので、cleanup処理が正しく実行されます。react-hooks-asyncのREADMEにあるコードを抜粋すると、

const GitHubSearch = ({ query }) => {
  const url = `https://api.github.com/search/repositories?q=${query}`;
  const delayTask = useAsyncTaskDelay(500);
  const fetchTask = useAsyncTaskFetch(url);
  const combinedTask = useAsyncCombineSeq(delayTask, fetchTask);
  useAsyncRun(combinedTask);
  if (delayTask.pending) return <div>Waiting...</div>;
  if (fetchTask.error) return <Err error={fetchTask.error} />;
  if (fetchTask.pending) return <Loading abort={fetchTask.abort} />;
  return ...;
};

のように使うことができます。これはTypeaheadと呼ばれる機能を実現した例で、タイプ中は検索をせず、しばらく経つと検索処理を開始するものです。もし、検索処理中にタイプを再開すると処理を中断します。具体的な動作例は下記になります。

screencast.gif

リポジトリに用意されている例があるのでそちらからも試すことができます。

類似のライブラリ

非同期処理をするライブラリは複数ありますが、その中で特に類似するライブラリを紹介します。

react-async

https://github.com/async-library/react-async

React Hooksが登場する前からあるライブラリで、render propsによるインタフェースも提供されています。また、ドキュメントも充実しています。

react-async-hook

https://github.com/slorber/react-async-hook

こちらは、React Hooksのインタフェースのみを提供するライブラリです。特徴はdepsの扱いで、useCallbackに依存しなくてもいいような書き方を提案しています。

おわりに

最初にも述べましたが、useEffectを使った非同期処理はcleanup処理さえできるようになれば簡単で、ライブラリを使わなくてもおよそのことは手軽に実現できます。react-hooks-asyncは少し複雑なユースケースで役に立つかもしれませんし、cleanup処理が内包されているので通常利用でも便利かと思います。

ところで、Suspense for Data Fetchingでは全く新しい方法が提案されています。Data Fetchingは一般的な非同期処理よりは狭い話に聞こえますが、Reactにおける非同期処理はデータ取得であることが多いですし、また、あらゆる非同期処理は一種のデータ取得でもあるとみなすこともできるかもしれません。新しい方法はRender-as-You-Fetchと呼ばれ、useEffectを使わない方法です。まだ発展途上ですが、Concurrent Mode/useTransitionによるUXの向上や、通常言われているSuspenseコンポーネントによるDXの向上だけでなく、useEffectで苦労していたdepsやuseCallbackなどの問題が無くなる可能性もあり大変期待しています。Render-as-You-Fetch指向のライブラリも試作していますので、興味ある方はご覧ください。

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

【React】JSXをcssで修飾するとき babel-plugin-react-html-attrs を使ってclassName属性を用いないようにする

前置き

JSXを利用している場合、class属性を利用することはできない。
これはclassキーワードがJavaScriptの予約語になっており、JavaScript のソースコード内利用できないため。

なので通常はclassNameとして次のように書く。

import React from "react";

export default (props) => <button className="button" {...props} />;

babel-plugin-react-html-attrsを入れてclassName属性を不使用とする

どうしてもclassName属性を使いたくない人向けにbabel-plugin-react-html-attrsを導入する。

npm install --save-dev babel-plugin-react-html-attrs

webpack.config.jsのoptionsの位置に追記する

webpack.config.js
 loader: 'babel-loader',
         options: {
+          plugins: ['react-html-attrs'],
           presets: ['@babel/preset-react', '@babel/preset-env']
         }

とすることで、コンポーネントも次のようにclass属性をつかってHTMLライクに書ける

<Link to="archives"><button class="btn btn-danger">archives</button></Link>
<Link to="settings"><button class="btn btn-success">settings</button></Link>

おわり

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

React-Redux 初歩 boolean

Redux初心者のメモ。
ログイン状態を判定したい時にboolean型のstateをReduxで管理してみる。(ここではログイン処理やフォームの実装はしません。)

Action

ストアの state を変更するためのメッセージを定義。

src/actions/index.js
export const SIGN_IN = 'SIGN_IN'
export const SIGN_OUT = 'SIGN_OUT'

  export const sign_in = () => {
    return {
      type: SIGN_IN
    }
  }

  export const sign_out = () => {
    return {
      type: SIGN_OUT
    }
  }

Store

Reducerをもとに、storeを作成

src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'

import reducer from './reducers'
import App from "./components/App";

const store = createStore(reducer)


ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);


Reducer

actionを受けてstateを変更する.

src/reducers/islogged.js
import { SIGN_IN ,SIGN_OUT} from '../actions'

//初期値にfalse
const initialState = { value: false }

export default (state = initialState, action) => {
  switch (action.type) {
    case SIGN_IN:
      return { value: true  }
    case SIGN_OUT:
      return { value: false }
    default:
      return state
  }
}

Reducer群を管理する。Reducerが増えた時に必要。

qiita.rb
import { combineReducers } from 'redux'
import islogged from './islogged'

export default combineReducers(({islogged }))

コンポーネント

storeで呼び出したAppコンポーネント
Stringメソッドでbooleanを文字を変更して表示している。
buttonを押すとstateが切り替わる。

src/components/App.js
import React, { Component } from 'react'
import { connect } from 'react-redux'

import { sign_in,sign_out } from '../actions'

class App extends Component {
  render() {
    const props = this.props
    return (
      <React.Fragment>
        <div>count:{String(props.value)}</div>
        <button onClick={props.sign_in}>+</button>
        <button onClick={props.sign_out}>-</button>
      </React.Fragment>
    );
  }
}


const mapStateToProps = state => ({ value: state.islogged.value  })


const mapDispatchToProps = dispatch => ({
  sign_in: () => dispatch(sign_in()),
  sign_out: () => dispatch(sign_out())

})


export default connect(mapStateToProps, mapDispatchToProps)(App)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react-routerとreact-router-domの違い

Ubiregi Advent Calendar 2019 2日目です。1日目は日曜でお休みだったのでした。

フロントエンドエンジニアのコジャが担当します。まだ入社して5ヶ月程度でフロントエンドエンジニアとしても5ヶ月程度です。 

何番煎じかわかりませんが、React-Routerの記事を投稿します。生暖かく見守ってください。いじめないで。

react-routerとreact-router-domの違い

reactのRoutingライブラリはreact-routerとreact-router-domがあります。

「react-routerがv4から改称してreact-router-domになったんだ!」みたいな印象がありますが(me too)。厳密にいうと違います。

react-router でググって一番上にくる記事がいきなりnpm install --save react-router-dom とかしてるのでややこしいのですが。

なにはともあれ

READMEを見てみましょう。今はv5です。

react-router-domはDOMバインディングなreact-routerだよ〜って書いてあります。

https://github.com/ReactTraining/react-router/tree/master/packages/react-router

react-router
Declarative routing for React.

https://github.com/ReactTraining/react-router/tree/master/packages/react-router-dom

react-router-dom
DOM bindings for React Router.

ちなみにreact-router-nativeってのもあります。

使い方を比較

Home,About,Dashboardはあらかじめ用意したコンポーネントです。

react-router

import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, Switch } from 'react-router';
import { createBrowserHistory } from 'history';
import { Home, About, Dashboard } from './component';

const App = () => {
    return (
        <Router history={createBrowserHistory()}>
            <div>{document.title}</div>
            <Switch>
                <Route exact path='/'><Home /></Route>
                <Route path='/about'><About /></Route>
                <Route path='/dashboard'><Dashboard /></Route>
            </Switch>
            <a href='/'>Back To Home</a>
        </Router>
    )
}

react-router-dom

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch, Link } from 'react-router-dom';
import { Home, About, Dashboard } from './component';

const App = () => {
    return (
        <BrowserRouter>
            <div>{document.title}</div>
            <Switch>
                <Route exact path='/'><Home /></Route>
                <Route path='/about'><About /></Route>
                <Route path='/dashboard'><Dashboard /></Route>
            </Switch>
            <Link to='/'>Back To Home</Link>
        </BrowserRouter>
    )
}

パッと見、違いとしては以下の感じです。
1. react-routerは<Router />historyを渡している
2. react-routerはaタグだが、react-router-domは <Link/> を使用している。

2に関してはのAPIをみた方が早い。
1について調べていきます。

RouterとBrowserRouterの違い

結論から書きますと、この二つにあまり違いはありませんでした。

実装を見てみます。

react-router/Router
react-router-dom/BrowserRouter

見た通りですが、BrowserRouterは内部でRouterを使用しており、historyをpropsに渡しています。単なるWrapperですね。

class BrowserRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

PropsTypesではいくつかoptionalなパラメータが定義されていますが、これらのpropsは全てhistoryへ渡されるので、RouterでもcreateBrowserHistoryにパラメータを渡すことで同じようなことが実現できます。

BrowserRouter.propTypes = {
  basename: PropTypes.string,
  children: PropTypes.node,
  forceRefresh: PropTypes.bool,
  getUserConfirmation: PropTypes.func,
  keyLength: PropTypes.number
};

以下はbasenameとforceRefreshを指定した例

const App = () => {
    return (
        <Router history={createBrowserHistory({ basename: '/', forceRefresh: true })}>
            <div>{document.title}</div>
            <Switch>
                <Route exact path='/'><Home /></Route>
                <Route path='/about'><About /></Route>
                <Route path='/dashboard'><Dashboard /></Route>
            </Switch>
            <a href='/'>Back To Home</a>
        </Router>
    )
}

あんまり違わないけど...

react-routerもreact-router-domも「Routingをする」だけならば同じように使えます。useHistory, userLocation, useParams, useRouteMatch といったhooksもreact-routerに実装があります。

ただ、当然のことをいうと、react-routerだけを使用してもあまり嬉しくありません、<Link /><NavLink />といったAPIが使用できませんしね。

<Router />の活用...?

最後に<Router />の活用を考えてみましょう。前述したようにBrowserRouterはRouterのWrapperなので、<Link /><NavLink />といったAPIも同じように使えます。
historyを別でinstallして置かなければなりませんが、まぁいいでしょう。

RouterとBrowserRouterの違いはただ一つ、historyを外部から渡すことができる。 これだけです。
なので、ReactGAと組み合わせてトラッキングが...とか思いましたが、調べたらすでにやってる方がいらっしゃいました。更に言えばuseEffect使う形に落ち着いてた。

https://github.com/react-ga/react-ga/issues/122

更に更に言えばuseLocationを使った例が公式にありました。

https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/hooks.md#uselocation

結論

迷わずreact-router-domを使おう。

蛇足

ぼんやりとreact-routerのissueを眺めてたらこんなdiscussionがされていました。

react-router-dom -> react-router/dom #6755

react-routerに依存している別のパッケージがあった時に、バージョンが不一致だと競合するからreact-router-domをreact-router配下のパッケージにしようよーみたいな感じですね。
react-router-nativeに全くメリットがないのと、そもそもreact-routerは直接依存するものではない?ようなので、積極的ではないのかなと思いますが。

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

メモ 読んでも意味ない【React】react-router@v4でルーティングを書くときのちょっとした注意事項

ルーティングは次のファイルに書いているものとする。
/, /archives, /settings のパスにアクセスした時に表示されるコンポーネントをそれぞれ、Featured, Archives, Settings になるように設定している。

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";
import Layout from "./pages/Layout";
import Featured from "./pages/Featured";
import Archives from "./pages/Archives";
import Settings from "./pages/Settings";

const app = document.getElementById('app');


ReactDOM.render(
    <Router>
        <Layout>
            <Route exact path="/" component={Featured}></Route>
            <Route exact path="/archives" component={Archives}></Route>
            <Route path="/archives/:article" component={Archives}></Route>
        </Layout>
    </Router>,
app);

<Route exact path="/" component={Featured}></Route>のみexactがついている。
ここで、exactを設定しない限り、pathが入力したURLに前方一致していれば描画される(つまりlocalhost:3000/以下のすべてのURLに対してが描画されてしまう)

このexact というキーワードが無いと/foo にも/bar にも/archives にもFeatured コンポーネントが表示されることになるので注意すること。

要するにユーザが/archives というパスにアクセスするとFeatured コンポーネントもArchives コンポーネントも表示されるという事態が発生する。

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