20191128のReactに関する記事は6件です。

React初心者だけどUbuntu18.04上でcreate-react-appとReact Hooksを使ってサクッとカウンタを作る

初投稿です。私もReactの勉強を最近始めたばかりなので備忘録的に。
色々すっ飛ばしてます。

まずはUbuntuのバージョン確認

Ubuntu上で作ります。

% cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.3 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.3 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic

yarnをインストール

yarn公式に従ってください
https://yarnpkg.com/lang/en/docs/install/#debian-stable

$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -  
$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
$ sudo apt update && sudo apt install yarn

create-react-appをインストール

これもyarn公式に従ってください
https://yarnpkg.com/lang/en/docs/cli/create/

$ yarn global add create-react-app

こんな感じのメッセージが出ればOK(多分)。

success Installed "create-react-app@3.2.0" with binaries:
      - create-react-app
Done in 9.51s.

$ yarn --version
1.17.3

create-react-appコマンドを叩く。

下のコマンドでカレントディレクトリにmy-react-appというディレクトリが作られる。
my-react-appの直下にReactでアプリを作るのに必要な諸々のファイルが自動的に作られる。

$ create-react-app my-react-app

成功するとこんなメッセージが出る。

Success! Created my-react-app at /home/koralle/learn/my-react-app
Inside that directory, you can run several commands:

  yarn start
    Starts the development server.

  yarn build
    Bundles the app into static files for production.

  yarn test
    Starts the test runner.

  yarn eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd my-react-app
  yarn start

Happy hacking!

$ create-react-app --version
3.1.2

試しにアプリを起動してみる。

$ cd my-react-app
$ yarn start

ちょっと待つとこんなメッセージになる。

Compiled successfully!

You can now view my-react-app in the browser.

  Local:            http://localhost:3000/
  On Your Network:  http://10.0.2.15:3000/

Note that the development build is not optimized.
To create a production build, use yarn build.

言われたとおりにブラウザで http://localhost:3000/ にアクセスします。
というか人によっては自動的にブラウザでページが立ち上がります。

こんな感じ。
無題.png

次からが本題。

カウンタを作るための準備

まずはmy-react-app直下のディレクトリ構成を確認
今回触るのは./public/index.html./src/App.jsだけです。

$ cd my-react-app
$ ls
README.md  node_modules  package.json  public  src  yarn.lock

$ tree ./public
./public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt

0 directories, 6 files

$ tree ./src
./src
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
└── serviceWorker.js

0 directories, 7 files

先にindex.htmlを開く。今回はできる限り余計なものを全部消します。邪魔なので。
私はこれだけ残しました。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

次は./src/App.jsを編集。
function App()全体がいわゆるAppコンポーネントっていうやつですが、
一回こいつを全部消します。
そしてAppコンポーネントをアロー関数式で書き直す。アロー関数式、おしゃれな感じが好きです。

import React from 'react';
import logo from './logo.svg';
import './App.css';

const App = () => {

} 

export default App;

一回途中経過を見たいので先にカウンタのボタンを作ります。
Appコンポーネント以外はいったん省略します。

const App = ()=> {

  return(
    <div>
      <button> + </button>
      <button> - </button>
    </div>
  );
}

こんな感じ。小さいけど確かに+ボタンと-ボタンが表示されてます。
無題1.png

次にカウンタを作ります。そのために必要なのはあと最低限以下の二つです。

  • カウンタの値を保持する変数 (*1)
  • カウンタの値を増減させる関数 (*2)

これらを実装するために、今回はReact HooksというReact公式APIの中のuseState()という関数を利用します。
詳しくは公式ドキュメントを見てください。

フック早わかり – React
https://ja.reactjs.org/docs/hooks-overview.html

書式は今回の場合こんな感じ。
countが上記の(*1)で、setCountは(*2)とイコールではないですが、(*2)の実装に必要になります。
useState(0)の0はcountの初期値です。

const [count, setCount] = useState(0);

これを使って./src/App.jsにカウンタの動作を書きますが、1行目もちょっと編集します。

import React, {useState} from 'react';
import logo from './logo.svg';
import './App.css';

const App = () => {
  // (*1), (*2)の実装に必要
  const [count, setCount] = useState(0);

  const increment = () => setCount(count+1); // カウンタの値を増やす
  const decrement = () => setCount(count-1); // カウンタの値を減らす

  // index.htmlの<div id="root"></div>の中に表示
  return(
    <div>
      <p> {count} </p>
      <button onClick={increment}> + </button>
      <button onClick={decrement}> - </button>
    </div>
  );
} 

export default App;

これで完成

カウンタの値を増やす
増やす.png

カウンタの値を減らす
減らす.png

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

僕が開発しやすいReact環境を構築する。

環境

  • React
  • ESlint
  • prettier
  • TypeScript
  • styled-components

手順

TypeScript用のReactプロジェクトを作成する

yarn create react-app my-app --typescript

src内にあるファイルをすべて削除する。

cd my-app
cd src

# If you're using a Mac or Linux:
rm -f *

# Or, if you're on Windows:
del *

# Then, switch back to the project folder
cd ..

ESlintをインストールする

yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

prettierをインストールする

yarn add -D prettier eslint-config-prettier eslint-plugin-prettier

基本設定

.eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking",

    "plugin:prettier/recommended",
    "prettier/@typescript-eslint"
  ],
  "plugins": [
    "@typescript-eslint"
  ],
  "parser": "@typescript-eslint/parser",
  "env": { "browser": true, "node": true, "es6": true },
  "parserOptions": {
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "rules": {
    // 適当なルール
  }
}

VS CODE

setting.js
{
  // "editor.formatOnSave": false,
  "eslint.autoFixOnSave": true,
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    {"language": "typescript", "autoFix": true },
    {"language": "typescriptreact", "autoFix": true }
  ]
}
  • editor.formatOnSave: trueは自動整形が衝突するので指定しないでください。
  • eslint.validateでtypescriptを指定してください。
  • reactを使用するのでtypescriptreactも指定します。

styled-componentsをインストールする

公式

yarn add styled-components
yarn add @types/styled-components

Material-UIをインストールする

公式

yarn add @material-ui/core

フォルダを構成する

Styled-componentsの作者の提案する構成にする。
この構成はページやコンポーネント固有のものと複数のコンポーネントから使用される共通コンポーネントに大きくディレクトリを分けるアプローチです。

src
├── App
│   ├── Header
│   │   ├── Logo.js    
│   │   ├── Title.js   
│   │   ├── Subtitle.js
│   │   └── index.js
│   └── Footer
│       ├── List.js
│       ├── ListItem.js
│       ├── Wrapper.js
│       └── index.js
├── shared
│   ├── Button.js
│   ├── Card.js
│   ├── InfiniteList.js
│   ├── EmojiPicker
│   └── Icons
└── index.js

参考

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

Reactでhtmlの文字列をレンダリングする

概要

Reactでhtmlの文字列をレンダリングする。

実装

dangerouslySetInnerHTMLを使うと可能。

render(){
    let hoge = "<div style='color: red;'>auauauaua</div>";
    return <div dangerouslySetInnerHTML={{ __html: hoge }}></div>;
} 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React + Firebaseで認証機能を実装してみよう

Ateam Lifestyle Advent Calendar 2019の2日目は
株式会社エイチームライフスタイル 名古屋開発部のタツミが担当します。
フロントエンドの開発をメインでやっています。
駆け出しのエンジニアですが、よろしくお願いします。

はじめに

チームでWebサービスを開発するときにフロントエンドとバックエンドの開発担当に分かれるのはよくある光景ですが、開発のスピードやチームのリソースが求められた時にバックエンドまで開発するのは大変です。

バックエンドのサービス開発に時間を取られずに、フロントエンドの開発に専念したい。
その解決策の1つとして、Googleが提供しているFirebaseというツールの機能の1つを使ってバックエンドの開発をせずに認証機能を作っていきたいと思います。

FirebaseはiOS/AndroidアプリからWebサービスまでバックエンドを必要とするサービスで活用することができます。

認証機能の概要

Firebaseではデータベース、ストレージサービス、ホスティングサービス、分析ツール、プッシュ通知などWeb/アプリ開発で活用できる様々なサービスを提供しています。
今回はFirebaseの認証機能の1つであるFirebase Authenticationを使って、React認証機能を実装していきたいと思います。

説明の進め方

本記事は題材ごとに全部で以下、4パートに分けて説明します。
Reactの基礎は理解している程で話を進めます。

  • Part1: フォームを作成しよう
  • Part2: Firebaseの認証機能を有効にしよう
  • Part3: ログイン機能を実装しよう
  • Part4: ログアウト機能を実装しよう

Part1: フォームを作成しよう

https://firebase.google.com/
↑にアクセスして、Firebaseのアカウントを作成してください。

create-react-appで新しいReactプロジェクトを作成してください。

認証用のコンポーネントを作成します。
新たにsrc/components/Auth/index.jsを作成してください。
簡単にメールアドレスとパスワードの基本的なフォームを作成します。

※ このファイルに認証機能を全て実装します。実装は例ですので適宜、適切なファイルに実装してください。

src/components/Auth/index.js
import React, { Component } from 'react'

class Auth extends Component {
  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
  }

  handleChange(event) {
    // テキストボックスの値を変更する度に、変数に値が格納される
    this.setState({ [event.target.name]: event.target.value })
  }

  handleSubmit(event) {
    // 後半のパートでemailとpasswordをfirebaseに登録する処理を追加
    event.preventDefault()
  }

  render() {
    return (
      <div>
        <form method="post" onSubmit={this.handleSubmit}>
          <div>
            <input type="text" name="email" onChange={this.handleChange} /><br/>
            <input type="password" name="password" onChange={this.handleChange} />
          </div>
          <button>SUBMIT</button>
        </form>
      </div>
    )
  }
}

export default Auth

メールアドレスとパスワードのonChangeイベントに設定したhandleChangeアクションでテキストボックスの値が変わる度にstateに更新された値が格納されます。

後半のパートのなかで、handleSubmitアクションで格納されたstateをFirebase Authenticationに登録していく処理を追加する方法を説明します。

作成した認証コンポーネントを描画するために、App.jsを変更します。

src/App.js
import React from 'react'
import './App.css'
import Auth from './components/Auth'

function App() {
  return (
    <div className="App">
      <Auth />
    </div>
  )
}

export default App

Part2: Firebaseの認証機能を有効にしよう

ここからログイン機能を実装していきます。

Part1で作ったFirebaseのアカウントでログインして、コンソールへ移動を選択します。
firebase_1.png

新しいプロジェクトを作成する必要があるので、プロジェクトを追加を選択してください。

firebase_2.png

プロジェクト名は何でも良いのですが、今回は認証テストとしてauth-testと命名して続行を選択します。

firebase_3.png

google analyticsと連携させたい場合は「このプロジェクトでGoogle アナリティクスを有効にする」にチェックを入れますが、今回は連携させないのでチェックを外します。プロジェクトを作成を選択してください。

firebase_4.png

無事にauth-testプロジェクトが作成されました。
左のメニューからAuthenticationを選択して、ログイン方法を設定を選択してください。

firebase_5.png

様々な認証方法を選ぶことができますが、今回はメールアドレスとパスワードで認証機能を作っていきたいのでメール/パスワードの右の鉛筆マークを選択してください。

firebase_6.png

メールアドレスとパスワードの認証を有効にします。
今回はパスワードの設定を必須にしたいので、パスワードなしでログインは無効にしておきます。
保存を選択します。

firebase_7.png

以上で、メール/パスワードの設定が有効になりました。

firebase_8.png

Part3: ログイン機能を実装しよう

Firebase AuthenticationのAPIを使って、フロントエンドにログイン機能を実装します。
認証が完了するとトークンが発行されますが、トークンをstateに保存するとリロードした時にトークン情報が消えてしまうので、localStorageに保存する方法を実装していきます。
Firebase APIと通信する必要があるので、axiosを使っていきます。

SignUpとSignIn機能を実装するボタン<button>を設置します。
Part2で作ったフォームの続きから実装していきます。
ボタンをクリックする度に、switchAuthModeHandlerが発火してisSignUpフラグのon, offの切替を行います。

src/components/Auth/index.js
import React, { Component } from 'react'

class Auth extends Component {
  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
    this.state = {
   // signUpとsignInフラグ
      isSignUp: true
    }
  }

  handleChange(event) {
    this.setState({ [event.target.name]: event.target.value })
  }

  handleSubmit(event) {
    event.preventDefault()
  }

 // クリックの毎にsignUpとsignInの切替を行う
 switchAuthModeHandler = () => {
    this.setState({ isSignUp: !this.state.isSignUp })
  }

  render() {
    return (
      <div>
        <form method="post" onSubmit={this.handleSubmit}>
          <div>
            <input type="text" name="email" onChange={this.handleChange} /><br/>
            <input type="password" name="password" onChange={this.handleChange} />
          </div>
          <button>SUBMIT</button>
        </form>
    {/* signUp、signInボタン */}
        <button onClick={this.switchAuthModeHandler}>
          SWITCH TO {this.state.isSignUp ? 'SignIn' : 'SignUp'}
        </button>
      </div>
    )
  }
}

export default Auth

続いて、認証メソッドを作成します。
Firebaseの認証機能APIのエンドポイントを紹介します。
APIを使うにはAPIキーを入力する必要があるので、キーを先に確認します。

firebase_9.png

firebase_10.png

SignUp用のエンドポイントとSignIn用のエンドポイントを確認します。
https://firebase.google.com/docs/reference/rest/auth#section-create-email-password
↑ URLにアクセスします

SignUp用エンドポイント
API_1.png

SignInのエンドポイント
API_2.png

各エンドポイントの?key=[API_KEY]の部分に先ほど確認したAPIキーを入力して使います。

認証メソッドの続きを実装します。

src/components/Auth/index.js
import React, { Component } from 'react'
import axios from 'axios'

class Auth extends Component {
  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
    this.state = {
      isSignUp: true,
      token: null,
      error: ''
    }
  }

  handleChange(event) {
    this.setState({ [event.target.name]: event.target.value })
  }

  handleSubmit(event) {
    this.auth()
    event.preventDefault()
  }

  switchAuthModeHandler = () => {
    this.setState({ isSignUp: !this.state.isSignUp })
  }

  auth = () => {
    // 認証データ
    const authDate = {
      email: this.state.email,
      password: this.state.password,
      returnSecureToken: true
    }
    // signIn用のAPIキー
    let url = 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=APIキーを入力'
    // signUp用のAPIキー
    if (this.state.isSignUp) {
      url = 'https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=APIキーを入力'
    }
    axios.post(url, authDate)
      .then(response => {
        // 返ってきたトークンをローカルストレージに格納する
        localStorage.setItem('token', response.data.idToken)
      })
      .catch(error => {
        // Firebase側で用意されているエラーメッセージが格納される
        this.setState({ error: error.response.data.error.message })
      })
  }

  render() {
    return (
      <div>
        <form method="post" onSubmit={this.handleSubmit}>
          <div>
          <input type="text" name="email" onChange={this.handleChange} /><br/>
            <input type="password" name="password" onChange={this.handleChange} />
          </div>
          <div>
            {this.state.error}
          </div>
          <button>SUBMIT</button>
        </form>
        <button onClick={this.switchAuthModeHandler}>
          SWITCH TO {this.state.isSignUp ? 'SignIn' : 'SignUp'}
        </button>
      </div>
    )
  }
}

export default Auth

これでログイン機能の実装は完了です。
サインアップすると新しくユーザが作られているのを確認することができます。
パスワードは6文字以上でないと登録できないので注意してください。

firebase_11.png

サインアップまたは、サインインするとlocalStrageにtokenが保存されているのを確認することができます。

token.png

(localStorage.getItem('token'))でtokenが保存されている(ログインしている)場合に任意の処理を実行できます。

if (localStorage.getItem('token')) {
  // ログインしている場合に任意のメソッドを実行
}

Part4: ログアウト機能を実装しよう

ログアウトは簡単です。
ログインの時にlocalStrageに保存したtokenを削除するだけです。

src/components/Auth/index.js
import React, { Component } from 'react'
import axios from 'axios'

class Auth extends Component {
  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
    this.state = {
      isSignUp: true,
      token: null,
      error: ''
    }
  }

  handleChange(event) {
    this.setState({ [event.target.name]: event.target.value })
  }

  handleSubmit(event) {
    this.auth()
    event.preventDefault()
  }

  switchAuthModeHandler = () => {
    this.setState({ isSignUp: !this.state.isSignUp })
  }

  auth = () => {
    const authDate = {
      email: this.state.email,
      password: this.state.password,
      returnSecureToken: true
    }
    let url = 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=APIキーを入力'
    if (this.state.isSignUp) {
      url = 'https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=APIキーを入力'
    }
    axios.post(url, authDate)
      .then(response => {
        localStorage.setItem('token', response.data.idToken)
      })
      .catch(error => {
        this.setState({ error: error.response.data.error.message })
      })
  }

  logout = () => {
    // トークンの削除
    localStorage.removeItem('token')
  }

  render() {
    return (
      <div>
        <form method="post" onSubmit={this.handleSubmit}>
          <div>
          <input type="text" name="email" onChange={this.handleChange} /><br/>
            <input type="password" name="password" onChange={this.handleChange} />
          </div>
          <div>
            {this.state.error}
          </div>
          <button>SUBMIT</button>
        </form>
        <button onClick={this.switchAuthModeHandler}>
          SWITCH TO {this.state.isSignUp ? 'SignIn' : 'SignUp'}
        </button><br/>
        {/* ログインしている場合だけログアウトボタンが表示される */}
        {localStorage.getItem('token') &&
          <button onClick={this.logout}>LOGOUT</button>}
      </div>
    )
  }
}

export default Auth

実装後の画面イメージ
・ サインイン、サインアップするとlocalStrageにトークンが保存される
・ リロードしてもトークンは保持し続ける
・ ログアウトを選択するとトークンが削除される

operation2.gif

まとめ

長くなりましたが、いかがでしたでしょうか?
バックエンドサービスを何らかのフレームワークを使って実装していれば、標準の認証機能を導入できるとは思いますが、自前でデータベースを用意したり、個人情報をどう管理するかを考える必要があります。
Firebase Authenticationを使うことでこれらの問題を意識することなく、認証機能を実装することができました。
Firabaseでは、導入部分でも紹介したように様々な機能が用意されているので、ぜひ活用してみてください。

Ateam Lifestyle Advent Calendar 2019の3日目は、@chardenがお送りします!!どんなネタを用意してくるのか楽しみです!!
"挑戦"を大事にするエイチームグループでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトを御覧ください。
https://www.a-tm.co.jp/recruit/

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

Gatsby + ContentfulをTypeScript化する

Gatsby + Contentfulという構成にすることで、CMSのフロントエンドとバックエンドを分離することが可能になります。
ただ、この構成にTypeScriptを導入する場合、一手間必要になります。その手順を書いていきます。
また、GatsbyのTypeScript化については、下記の記事が大変参考になりました。内容が被る箇所が多々あるので、その部分についてはこの記事では割愛します。
Gatsby.js を完全TypeScript化する

Gatsbyアプリ作成

まず、gatsby-cliをインストールします。

yarn global add gatsby-cli

その後、下記コマンドを実行することでgatsbyの雛形がダウンロードされます。

gatsby new <任意のディレクトリ名>

続けて以下のコマンドを実行し、localhost:3000でサイトが表示されればOK。

cd <任意のディレクトリ名>
yarn develop

Contenfulのほうもアカウント登録して、適当なSpaceを作成しておきます。

GatsbyのTypeScript化

GatsbyのTypeScript化については、冒頭で紹介したこちらの記事を参考にしてください。自分が書くよりも確実にわかりやすいはず...!

Contentfulの導入

GatsbyとContentfulを繋げるには、プラグインを2つインストールする必要があるので、以下のコマンドを実行します。

yarn add gatsby-source-contentful gatsby-transformer-remark

gatsby-source-contentfulは、ContentfulからGraphQLでデータを取得できるようにするプラグイン。これを導入することでデータ取得用のQueryが利用できるようになります。

gatsby-transformer-remarkは、Markdownで記述されたテキストをHTMLに変換してくれるプラグインです。これを導入することで、Contentful側でMarkdownで書いた内容を、Gatsby側でそのままHTMLとして表示できます。

また、インストールしたプラグインはgatsby-config.jsに記述しないと有効にならないので、忘れないように注意です。

gatsby-config.js
plugins: [
  ...,
  `gatsby-transformer-remark`,
   {
     resolve: `gatsby-source-contentful`,
     options: {
       spaceId: process.env.CONTENTFUL_SPACE_ID,
       accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
     },
   },
]

spaceIdとaccessTokenはContentfulから取得します。envの参照方法はお好みで。自分はdirenvを使いました。

ここまでできたら、Contentful側で適当なコンテンツを作成しておきます。また、GraphiQLで、Contentfulからデータが取得できることを確認しておきましょう。

gatsby-node.jsをTypeScript対応させる

gatsby-node.jsは、主に動的なサイトページを作る際に変更するファイルです(/posts/:idのような)

また、動的なページテンプレートは、src/templates配下に配置します。gatsby-node.jsでContentfulからデータを取得し、そのデータをテンプレートファイルに流し込み、ページを作るイメージです。

今回やりたいのは、gatsby-node.jsで取得したデータ構造を型(Type)としてexportし、テンプレートファイルでそれをimportして紐づけることです。これにより、テンプレート側で型推論を利用したコーディングが可能になります。

参考にした記事に倣って、gatsby-node.jsに以下を記述します。(ts-nodeをインストールしておく必要があります)

gatsby-node.js
'use strict'
require('ts-node').register({
  compilerOptions: {
    module: 'commonjs',
    target: 'esnext',
  },
})
exports.createPages = require('./gatsby-node/index').createPages

これにより、./gatsby-node/index.tsが実質的なエントリーポイントになりました。TypeScriptを用いてgatsby-nodeファイルを記述していくことができます。

ContentfulからPost(投稿)を取得して、動的にページを割り当てるための記述は以下のようになります。

index.ts
const path = require("path")
import { GatsbyNode } from "gatsby"
import {
  ContentfulPostConnection,
  ContentfulPost,
} from "../types/graphql-types"

// GraphQLにより取得されるデータの型
type Result = {
  allContentfulPost: ContentfulPostConnection
}

// テンプレートファイルに渡すデータの型
export type PostContext = {
  post: ContentfulPost
}

// 実行するGraphQLのQuery
const query = `
{
  allContentfulPost {
    edges {
      node {
        content {
          childMarkdownRemark {
            html
          }
          content
        }
        publishedAt
        slug
        title
      }
    }
  }
}
`

// 動的にページを生成する関数
export const createPages: GatsbyNode["createPages"] = async ({
  graphql,
  actions: { createPage },
}) => {
  // ジェネリクスでGraphQLの返却データ型を指定
  const result = await graphql<Result>(query)
  const { edges } = result.data.allContentfulPost

  // 利用するテンプレートファイルを指定
  const postTemplate = path.resolve("./src/templates/post.tsx")

  edges.forEach(edge => {
    // ジェネリクスでcontextプロパティ(テンプレートに渡すデータ)の型を指定
    createPage<PostContext>({
      path: `/posts/${edge.node.slug}`,
      component: postTemplate,
      context: { post: edge.node },
    })
  })
}

ContentfulPostConnectionやContentfulPostといった型は、gatsby-plugin-graphql-codegenというプラグインを導入後、buildすることで自動的に生成されるものです。

GraphQLでContentfulからデータを取得する際のQueryに対応する型が全て自動的に生成されるので、これを用いて型を指定していきます。大量にあるので目当ての型を探すのも一苦労ですが、頑張って探します。

await graphql<Result>(query)で、GraphQLで取得できるデータの型を指定しています。上記のquery(allContentfulPost {...})ではこういったデータが返却されてきます。(GraphiQLでの実行結果です)

{
  "data": {
    "allContentfulPost": {
      "edges": [
        {
          "node": {
            "publishedAt": "2019-11-27T00:00+09:00",
            "slug": "sample",
            "title": "sample",
            "content": {
              "childMarkdownRemark": {
                "html": "<p>sample content</p>"
              }
            }
          }
        }
      ]
    }
  }
}

これに対応する、ContentfulPostConnectionの型はこんな感じ。

export type ContentfulPostConnection = {
  totalCount: Scalars['Int'],
  edges: Array<ContentfulPostEdge>,
  nodes: Array<ContentfulPost>,
  pageInfo: PageInfo,
  distinct: Array<Scalars['String']>,
  group: Array<ContentfulPostGroupConnection>,
};

テンプレートファイルとの型による紐付けは、createPage<PostContext>で行なっています。こう書くことで、以下のcontextの部分をPostContextで縛ることができます。

createPage<PostContext>({
  path: `/posts/${edge.node.slug}`,
  component: postTemplate,
  context: { post: edge.node },
})

contextはテンプレートに渡す値を指定している箇所になるので、ここをPostContextで縛ったことにより、テンプレート側に渡される変数も当然PostContext型ということになります。

post.ts
import React from "react"
import Layout from "../components/layout"
import { PostContext } from "../../gatsby-node"

type Props = {
  pathContext: PostContext
}

// pathContextがgatsby-node.tsから渡される変数で、PostContext型となる
const Post: React.FC<Props> = ({ pathContext }) => {
  const contentHtml = pathContext.post.content.childMarkdownRemark.html

  return (
    <Layout>
      <div dangerouslySetInnerHTML={{ __html: contentHtml }} />
    </Layout>
  )
}

export default Post

これにより、テンプレートファイルでも型推論を利用したコーディングが可能になりますね。

まとめ

自動生成される型定義が多すぎて、目当ての型を探すのに一番苦労しました。ここまでやってしまえば、あとはTypeScriptの恩恵を受けながら開発していけるのかなと思います。

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

[React]説明がいらないReact+TypeScript開発環境設定(CRA無し)

Install

yarn init --yes
yarn add next react react-dom
yarn add @zeit/next-typescript @types/next @types/react @zeit/next-typescript @types/next @types/react

ファイル追加

// next.config.js
const withTypescript = require('@zeit/next-typescript')
module.exports = withTypescript()
//.babelrc
{
  "presets": ["next/babel", "@zeit/next-typescript/babel"]
}
//tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "jsx": "preserve",
    "lib": ["dom", "es2017"],
    "module": "esnext",
    "moduleResolution": "node",
    "noEmit": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "preserveConstEnums": true,
    "removeComments": false,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "target": "esnext"
  }
}

eslint setting

npm i eslint -D
npm i @typescript-eslint/parser -D
npm i @typescript-eslint/eslint-plugin -D
npx eslint --init
//.eslintrc.js
module.exports = {
    "env": {
        "browser": true,
        "es6": true,
        "node": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended"
    ],
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    "parserOptions": {
        project: './tsconfig.json',   //추가
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": 2018,
        "sourceType": "module"
    },
    "plugins": [
        "react"
    ],
    "rules": {}
};

package.json

  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  }

pages/index.tsx 追加

import React from "react";

export default () => {
  return <div>hello</div>;
};
yarn add --dev typescript @types/node
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む