20201204のReactに関する記事は9件です。

StripeでApple PayとGoogle Payをウェブアプリに対応させる方法【Stripe, Next.js, React.js, Typescript, Javascript】

PostCoffee01.jpg

PostCoffeeでウェブエンジニアをしています、@imryoheiです!

概要

普段は〜Pay(Apple Pay, Google Pay, Line Payなど)を利用するだけでしたがつい最近StripeとNext.jsを使ってApple PayとGoogle Payをシステムに実装しました。弊サービスのECサイトではチェックアウト画面で顧客情報の入力を求めていましたが、Apple PayとGoogle Payの導入によりログイン情報だけを求める決済UIUXを実現することができました。ユーザーの入力時間を省くことでスマートな決済体験を生み出すことが可能になりました。

本記事では、実際に開発中につまづいたポイントなども含めながら、StripeのPayment Request Buttonの実装方法を解説していきます。※Stripeの決済処理などは割愛しています。

それではApple Pay & Google Payの実装を始めましょう。

実装方法

  1. StripeのPayment Request Buttonを実装
  2. Stripeがブラウザに合わせて自動的にApple PayまたはGoogle Payを表示
  3. 購入ボタンを押してペイメントシートを表示
  4. 決済に必要な情報を編集・選択
  5. Stripeの決済処理を実行(サブスクまたは単品購入)
  6. 完了or失敗

前提条件

  • ブラウザに支払い方法を追加済みであること。Chromeにカードを保存、またはSafariで使うにはWalletにカードを保存。
  • HTTPS通信。開発/本番環境で必要です。Ngrokと言うサービスを使うとデプロイなしでHttps通信が可能になるので便利です!(ローカル環境を共有できるんですね...知らなかった。笑 まじで便利ですね)
  • Apple Payを使用するためには、ドメインを登録する。開発/本番環境で設定が必須です。
  • https://stripe.com/docs/stripe-js/elements/payment-request-button からdomain association fileを取得

設定

  1. domain association fileを次のようにホストする。https://your doman here.com/.well-known/apple-developer-merchantid-domain-association
  2. Stripe経由でAppleにドメインを登録(Stripeのダッシュボードで登録しました)
  3. 1と2が問題なく完了するとApple Payのテストができるようになります。

テスト方法

Google Payをテストするための環境設定は特にありませんが、Google Payのテストカードを取得するにはテストカード スイートから「ユーザーグループに参加」を見つけて、グループに参加してください。参加するだけでテスト用のGoogle Payを使えるようになります。Goole Payのテスト決済のやり方がわからずStripeのテストカードでカード登録を行ったりしていたらなんと二日くらい無駄にしました。。。

Apple Payはセキュリティー上fakeカードを登録することができないらしく、リアルカードを登録して決済テストを行います。ドキュメントによると、

Apple won’t let you add a fake credit card to Passport and the iPhone. However, if you’re using Apple Pay and your test Stripe API keys, we’ll recognize the situation when the Apple Pay request is made and return a successful test card token. To test a payment in live mode, you’ll need to use a working credit card.

Stripe側がtest API keyかどうかを判定するらしいので請求されません。

実装

Stripeのドキュメントを参考に解説していきます。

import React, {useState, useEffect} from 'react';
import {PaymentRequestButtonElement, useStripe} from '@stripe/react-stripe-js';

const CheckoutForm = () => {
  const stripe = useStripe();
  const [paymentRequest, setPaymentRequest] = useState(null);

  useEffect(() => {
    if (stripe) {
      const pr = stripe.paymentRequest({
        country: 'JP', 
        currency: 'jpy',
        total: {
          label: 'Demo total',
          amount: 1099,
        },
        requestPayerName: true,
        requestPayerEmail: true,
      });

      // Check the availability of the Payment Request API.
      pr.canMakePayment().then(result => {
        if (result) {
          setPaymentRequest(pr);
        }
      });
    }
  }, [stripe]);

  if (paymentRequest) {
    return <PaymentRequestButtonElement options={{paymentRequest}} />
  }

  // Use a traditional checkout form.
  return 'Insert your form or button component here.';
}

StripeがPayment Request Buttonを使えるかどうかを判定し、使える場合はユーザーのブラウザに合わせてボタンが表示されます。SafariはApple Pay、ChromeはGoogle Payになります。Apple Pay利用可能の場合はresultが{applepay: true}になります。

PaymentRequestにはshippingOptions(配送方法)を追加することもできます。複数登録することができるため配列になっています。

 const pr = stripe.paymentRequest({
        country: 'US',
        currency: 'usd',
        total: {
          label: 'Demo total',
          amount: 1099,
        },
        requestPayerName: true,
        requestShipping: true,
        requestPayerEmail: false,
        requestPayerPhone: true,
        shippingOptions: [{
            id: 'payment id',
            label: 'ネコポス (クロネコヤマト)',
            amount: totalAmount,
     }],
});

ユーザーから取得できる情報は、name(フルネーム), shipping address(配送先), email(メール), phone(電話番号)ですがemailが不要であればfalseまたは指定しないと非表示になります。これら全て必須キーではないためオブジェクトから省いても大丈夫です。必須情報はcountrycurrencytotalです。
https://stripe.com/docs/js/payment_request/create

ユーザーの住所変更を検知する場合はpaymentRequestに用意されているイベントリスナーを利用します。以下のコードはドキュメントのサンプルコードです。eventから変更後の住所を取得し、サーバー側で配送料を計算させていますね。

paymentRequest.on('shippingaddresschange', function(ev) {
  if (ev.shippingAddress.country !== 'JP') {
    ev.updateWith({status: 'invalid_shipping_address'});
  } else {
    // Perform server-side request to fetch shipping options
    fetch('/calculateShippingPrice', {
      data: JSON.stringify({
        shippingAddress: ev.shippingAddress
      })
    }).then(function(response) {
      return response.json();
    }).then(function(result) {
      ev.updateWith({
        status: 'success',
        shippingOptions: result.supportedShippingOptions,
      });
    });
  }
});

配送先によって配送料金が変わるという前提のはずなので、shippingOptions、厳密にはamount表示の更新をするためにupdateWith()関数を呼び出しています。requestShippingをtrueにすると、配送先入力が必須になるのでshippingOptionsを更新が必要みたい?です。(配送先なし = requestShipping = falseで検証してないので憶測ですが多分あってると思います。)

const shippingOptions = [{
   id: 'payment id',
   label: 'ネコポス (クロネコヤマト)',
   amount: totalAmount,
}],

以下のイベントはStripeがWalletまたはChromeに保存されたカードを処理しpaymentMemthodのidを生成してくれます。eventからpaymentMethod.idを取得できるようになっています。決済処理はここに含めると良いと思います。tokenイベントもあるので、プロジェクトに合わせてどのイベントを利用するか選ぶと良いと思います。

paymentRequest.on('paymentmethod', async (event) => {
      // 決済ロジック with event.paymentMethod.id 
      // if(payCompleted) {
      //     event.complete('success') // 必須
      // } else {
      //     event.complete('fail') // 必須
      // }
});

また、決済開始から30秒以内にeventのcompleteを呼び出す必要があります。決済開始から30秒を超えるとペイメントシートで支払い失敗のエラーが発生します。サーバー側で決済完了するのにペイメントシートでエラーが発生する場合は、決済処理を30秒以内に最適化すると良いと思います。

イベントの詳細はここで確認できます!
詳しくはこちらから: https://stripe.com/docs/js/payment_request/events/on_paymentmethod

ボタンのデザイン

オプションにstyleを含めることでデザインを変更することが可能です。

const options = {
  paymentRequest,
  style: {
    paymentRequestButton: {
      type: 'default',
      // One of 'default', 'book', 'buy', or 'donate'
      // Defaults to 'default'

      theme: 'dark',
      // One of 'dark', 'light', or 'light-outline'
      // Defaults to 'dark'

      height: '64px',
      // Defaults to '40px'. The width is always '100%'.
    },
  }
}

<PaymentRequestButtonElement options={options} />

また、Apple Payの場合以下のボタンが表示されますが
スクリーンショット 2020-12-04 19.38.36.png

Google Payは以下のボタンが表示されます。Google PayのデザインにならないのでGoogle Payを本番環境で利用するためにはGoogleのレビューが必要になると思いましたが、審査は不要でした。これはStripeの仕様で、開発環境も本番環境もこのボタンのデザインになります。

スクリーンショット 2020-12-04 19.37.49.png
詳しくはこちらから:
https://stackoverflow.com/questions/58027517/displaying-stripe-payment-request-button-as-a-google-pay-button

所感

ECサイトにApple Payのチャリーンって音が出るようになった時テンション上がりました。また新機能を開発したら記事をあげます!最後まで読んでいただきありがとうございました!

?☕️

https://www.wantedly.com/projects/360188

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

【TypeScript】useContextとuseStateを組み合わせて、子孫コンポーネントから直接先祖コンポーネントのstateを編集する

実務でReactをいろいろ経験させてもらってから、ようやくReact Hooksの便利さに気づき始めたラスカルです。こんにちは。

今回は、タイトルの通りではあるのですが、useContextとuseStateを組み合わせることで、子孫コンポーネントから、propsのバケツリレーをせずに先祖コンポーネントのstateを更新する実装をTypeScriptで実装したいと思います。

(強調したのは、jsでの実装はあるもののtsでの実装がすぐに見つからなかったからです?)

前提条件

import React, { useState } from 'react'

//親コンポーネント
const Parent: React.FC = () => {
    const [count, setCount] = useState(0)
    return (
        <>
         <Child />
        </>
    )
}

//子コンポーネント
const Child: React.FC = () => {
    return (
        <>
         <GrandChild />
        </>
    )
}

//孫コンポーネント
const GrandChild: React.FC = () => {
    return (
        <>
         <button></button>
         <button></button>
        </>
    )
}

親コンポーネントはuseStateを使って、状態管理をしています。
このcountの値を更新するためには、setCountを使ってあげる必要がありますね。
これを孫コンポーネントでボタンをおすイベントで、行いたいというわけです。

結論

useContextを使い、propsのバケツリレーをしなくても、孫要素から直接親要素のstateを更新できるようにします。

手法としてはuseContextで直接count, setCountを孫コンポーネントに送ってしまいます。

import React, { useState, useContext } from 'react'

//親コンポーネント

//useContextの初期値を設定。
const CountContext = React.createContext({} as {
  count: number
  setCount: React.Dispatch<React.SetStateAction<number>>
})

const Parent: React.FC = () => {
    const [count, setCount] = useState(0)
    return (
        <>
          //孫コンポーネントを含む子コンポーネントをuseContextで定めた変数で囲む。
          //valueでcountとsetCountをオブジェクトで渡している点に注意
          <CountContext.Provider value={{ count, setCount }}>
           <Child />
          </CountContext.Provider >
        </>
    )
}

//子コンポーネント
//特に変更なし
const Child: React.FC = () => {
    return (
        <>
         <GrandChild />
        </>
    )
}

//孫コンポーネント
const GrandChild: React.FC = () => {
    // 親要素で指定した変数を受け取る
    const {count, setCount} = useContext(CountContext)
    return (
        <>
         //親要素のuseStateがそのまま使える!
         <button onClick={() => setCount(count + 1)}>+</button>
         <button onClick={() => setCount(count - 1)}>-</button>
        </>
    )
}

ここで大事なのは親コンポーネントの

const CountContext = React.createContext({} as {
  count: number
  setCount: React.Dispatch<React.SetStateAction<number>>
})

の部分です。

jsでは

const CountContext = React.createContext()

と、初期値を特に設定しなくてもいいようですが、(いい意味で)型に厳しいTypeScriptでは初期値を必ず設定してあげる必要があるようです。

補足

もし親コンポーネントと孫コンポーネントが別ファイル(というか、その状況の方が多い気もするけど)の場合、

  • 親要素のuseContextで設定した変数にexportをつけてあげる
  • 孫要素でその変数をインポートする

必要があります。

【親コンポーネント】

Patrent.tsx
import React, { useState } from 'react'

//useContextの初期値を設定。
export const CountContext = React.createContext({} as {
  count: number
  setCount: React.Dispatch<React.SetStateAction<number>>
})

const Parent: React.FC = () => {
    const [count, setCount] = useState(0)
    return (
        <>
          //孫コンポーネントを含む子コンポーネントをuseContextで定めた変数で囲む。
          //valueでcountとsetCountをオブジェクトで渡している点に注意
          <CountContext.Provider value={{ count, setCount }}>
           <Child />
          </CountContext.Provider >
        </>
    )
}

【子コンポーネント】

Child.tsx
//特に変更なし
const Child: React.FC = () => {
    return (
        <>
         <GrandChild />
        </>
    )
}

【孫コンポーネント】

GrandChild.tsx
import { CountContext } from './Parent.tsx'
const GrandChild: React.FC = () => {
    // 親要素で指定した変数を受け取る
    const {count, setCount} = useContext(CountContext)
    return (
        <>
         //親要素のuseStateがそのまま使える!
         <button onClick={() => setCount(count + 1)}>+</button>
         <button onClick={() => setCount(count - 1)}>-</button>
        </>
    )
}

最後に

propsのバケツリレーがなくなったので、改修がかなりしやすくなりましたね。
ただuseContextは今回で言う親コンポーネントと孫コンポーネントの依存性をバリバリに強くしてしまうので、再利用性がやや失われてしまいます。

ご利用は計画的に?

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

axios をモックする方法 (React + TypeScript)

はじめに

HTTPクライアントでお馴染みの axios のモック化を試したかったので、
それに関しての導入記事となります。

モックに関しては、 axios-mock-server を利用します。

フロントは、 React + TypeScript でサンプルを記述します。

環境

  • macOS Catalina 10.15.6
  • node: v15.3.0
  • yarn: 1.22.10
  • TypeScript: 4.1.2
  • React: 17.0.0
  • axios-mock-server: 0.19.0

1. パッケージの導入

$ yarn add axios
$ yarn add --dev axios-mock-server

npm じゃなくて yarn を利用しています。

2. axios を利用した HTTPクライアント の作成

axios を利用した形の HTTPクライアント部分を実装します。
とりあえず最低限の get/post のみです。

src/data/rest.ts
import axios from "axios";

type Data = { [key: string]: string | number | boolean | object }

const rest = (() => {
  const client = axios.create({
    baseURL: '', // 今回は未指定 (通常は env などから参照させる)
    timeout: 15000,
  });
  return {
    client,
    get: <T = any, R = AxiosResponse<T>>(url: string): Promise<R> => {
      return client.get(url);
    },
    post: <T = any, R = AxiosResponse<T>>(url: string, data: Data): Promise<R> => {
      return client.post(url, data);
    }
})();

export { rest };

この時点ではまだモックを考慮していません。
また、本サンプルでは POST は未使用ですが一応定義しておきます。

3. API のインターフェースを定義

今回仮に User一覧 を返すAPIを想定します。
一旦クエリパラメータは無しで。

Method URL
GET http://localhost:3000/user/list

レスポンスの型定義

src/data/type/index.ts
type User = {
  id: number;
  name: string;
}

export type { User };

APIの実装部分

src/data/api/getUsers.ts
import { rest } from '../rest';
import { User } from '../type';

const getUsers = async (): Promise<User[]> => {
  const url = "/user/list";
  try {
    const { data } = await rest.get<User[]>(url)
    return data;
  } catch (error) {
    throw new Error(error);
  }
}

export { getUsers };

コンポーネント側から呼び出す場合はこんな感じです。

src/component/Users.tsx
import React, { useEffect } from 'react';
import { getUsers } from '../data';

const Users: React.FC = () => {
  useEffect(() => {
    getUsers().then((users) => {
      console.log('# users', users);
    });
  }, []);
  return <span>Users</span>;
};

export { Users };

4. mockserverrc の作成

プロジェクトのルート階層(package.jsonがある場所)に .mockserverrc を作成します。
今回 モックAPI のファイルとして、
/src/data/mock に格納したいので、そこのパスを指定しています。

.mockserverrc
{
  "input": "./src/data/mock"
}

5. モックAPI の作成

モックAPIを格納するディレクトリは以下の通りです。
(見ての通りディレクトリ構造に沿って、自動的にルーティングが設定されます)

├── data
│   └── mock
│       ├── $mock.ts  ... ※自動生成されるファイル (後述で説明)
│       └── user
│           └── list.ts

User一覧のモックAPIの実装です。

src/data/mock/user/list.ts
import { MockMethods, MockResponse } from 'axios-mock-server';
import { User } from '../../type';

const list: MockMethods = {
  get: async (): Promise<MockResponse> => {
    const data: User[] = [
      {
        id: 1,
        name: 'A'
      },
      {
        id: 2,
        name: 'B'
      }
    ];
    return [200, data]; // 200 はステータスコード
  }
};

export default list; // ここは `default export` にしないと動かない

6. モックAPIをビルドするスクリプトを定義

package.json の scripts にモックAPIのビルド設定を追記します。
-c で 設定ファイルを指定しています。

package.json
  ...
  "scripts": {
    ...
    "mock:build": "axios-mock-server -c .mockserverrc"
  },

7. モックAPIのビルド

以下のコマンドを実行します。

$ yarn mock:build

$mock.ts が自動生成されていればOKです。
生成されたコードは以下の通りです。
自動生成されたファイルは、適宜.gitignoreに追加してください。

data/mock/$mock.ts
/* eslint-disable */
import { AxiosInstance } from 'axios'
import mockServer from 'axios-mock-server'
import mock0 from './user/list'

export default (client?: AxiosInstance) => mockServer([
  {
    path: '/user/list',
    methods: mock0
  }
], client, '')

8. axios とモックの紐付け

手順 2. で生成した HTTPクライアント に修正を加えます。
自動生成した mock モジュールを import し、
関数呼び出しの引数に axios の client を渡しております。

また、お好みで ログ出力 や 遅延設定 は行なえます。

src/data/rest.ts
import axios, { AxiosResponse } from "axios";
+ import { mock } from './mock';

  ... 省略 ...

+ const useMock = true;
+ if (useMock) {
+   mock(rest.client)
+     .enableLog()
+     .setDelayTime(500);
+ }

export { rest };

9. 起動コマンドの追加 及び 動作確認

このままだとモックAPIを改修する度に、
mock:buildコマンドを実行しないといけないので、
独自の起動コマンドを追加します。

package.json の scripts に以下を追加します。

package.json
  ...
  "scripts": {
    ...
    "mock:build": "axios-mock-server -c .mockserverrc"
+   "mock:start": "axios-mock-server -c .mockserverrc && react-scripts start"
  },

react-scripts startyarn start で実行するコマンド内容です。

ローカルでの実行は以下のコマンドとなります。

$ yarn mock:start

enableLog()を有効にしていると、API呼び出し時にログが出力されます。
スクリーンショット 2020-12-04 16.51.51.png

まとめ

以上、axios をモックする方法となります。
サンプルコードは以下にアップしています。
https://github.com/unpii/react-axios-mock-example

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

TypeScript+React+Cloud Firestore+Stateモナドで、ブラウザで動くボードゲームの対戦ツールを作った (3/3 UI・通信編)

前回はボードゲーム、ナショナルエコノミーオンライン対戦ツールにおけるゲームメカニクス部のStateモナドを用いた実装手法について解説しました。
今回はCloud Firestoreとの通信、およびReactを用いたUIの実装を行います。

はじめに

通信とUIの実装はメカニクス部と異なり実際に動かしながら動作を確認する必要があります。webpack-dev-serverを用いるなり、create-react-appを用いるなりしてローカルに開発環境を構築し、ブラウザからアクセスして見た目を確認しつつ行うのが手っ取り早いでしょう。
身内にのみ限定して共有する予定であったことから、本対戦ツールでは認証などの仕組みを実装しませんでした。したがって本記事で解説する実装は、あまり「安全」な構成にはなっていないことをご留意ください。

Cloud Firestoreとの通信

接続

Firebase SDKのinitializeApp関数を用いてCloud Firestoreとの接続を確立します。

import firebase from "firebase/app";

const db = firebase.initializeApp({
    apiKey: "*******-*******************************",
    authDomain: "****.firebaseapp.com",
    databaseURL: "https://****.firebaseio.com",
    projectId: "****",
    storageBucket: "****.appspot.com",
    messagingSenderId: "************",
    appId: "*******"
  }).firestore();

スクリプトの冒頭で作成したこのfirebase.Firestore型を全体で用いることで送受信を行います。

Cloud Firestoreのデータ構造は、最小単位を「ドキュメント」として、それを保有する「コレクション」によって成り立っています。ドキュメントはKVS型のレコードであり、実質的にはJSONと同じです。したがって、メンバすべてが数値、文字列、あるいはその配列やオブジェクトであるようなGame型をそのままJSONとして送受信することができます。
実際には、ゲーム内各種操作のログも保持・共有できるように、合わせた型として次のGameAndLogを定義して、それを送受信します。

game-and-log.ts
type GameAndLog = {
    game: Game,
    log: string[];
}

受信

ゲーム状態の取得はいちどきりではなく、書き込みが行われるたびにリアルタイムに更新される必要があります。これはコールバックの登録によっても購読できますが、今回はrxfireを用いてObservableの形でゲーム状態を購読することにします。

fetch.ts
import * as firebase from "firebase";
import { Observable } from "rxjs";
import GameAndLog from "entity/gameandlog";
import { docData } from "rxfire/firestore";

export function fetchGame(db: firebase.firestore.Firestore, id: string): Observable<GameAndLog> {
    return docData(db.collection("games").doc(id));
}

事前準備としてCloud Firestoreのコンソール上で、「games」コレクションを作成しておきました。「games」コレクション内の適当な「ゲームID」ドキュメントをGameAndLog型として購読します。
あとはこれを必要な個所でsubscribeすれば、リアルタイムに更新されるGameAndLogが得られます。

送信

ゲーム状態の送信は、適切な操作がなされた場合にそれに対応するStateモナドと旧GameAndLogから新たなGameAndLogが作成された際に行われます。

fetch.ts(続き)
export function updateGame(db: firebase.firestore.Firestore, id: string, game: GameAndLog) {
    db.collection("games").doc(id).update(game);
}

新たなゲームの開始

送受信メソッドに含まれる「ゲームID」は、Cloud Firestoreに対し新たなドキュメントを作成させる形で生成することができます。適当な手段で決定したリーダークライアントがこのメソッドを呼ぶことにより新規ゲームを開始します。

function createGame(db: firebase.firestore.Firestore, id: string, game: Game) {
    db.collection("games").add({game: game, log: []})
        .then(doc => db.collection("rooms").doc(`room${id}`).update({game_id: doc.id}));
}

ゲーム開始前のロビーの状態を保持するコレクションとして「rooms」コレクションを定義しており、これにaddメソッドで生成されたゲームIDを書き込むことで「部屋が進行中のゲームで埋まっている」ことを表現しています。

UI

特別変わったことをするわけではなく、Game型で定義される現在のゲーム状態を用いて各種コンポーネントを順に作成していきます。

ゲーム状態の購読

ゲーム状態がObservableで得られるため、次のような購読用カスタムフックを作成するとよいでしょう。

use-observable.ts
import * as React from "react";
import { Subscription, Observable } from "rxjs";

export default function useObservable<T>(observable: Observable<T>): [T | null, any | null, () => void] {
    const [subscription, setSubscription] = React.useState<Subscription | null>(null);
    const [value, setValue] = React.useState<T | null>(null);
    const [error, setError] = React.useState<any | null>(null);

    const ref = React.useRef(subscription);

    React.useEffect(() => {
        setSubscription(observable.subscribe(v => {
            setValue(v)
        }, setError));
        return () => ref.current?.unsubscribe();
    }, []);

    return [value, error, () => subscription?.unsubscribe()];
}

Reactコンポーネントの作成

ナショナルエコノミーはすべてのコンポーネントがカードで構成されたゲームであり、「手札」や「所有不動産」など、カードを並べる似た形式のコンポーネントを多く用いることになります。
コンポーネントの作成手段についてはいくらでも記述が見つかると思われるので、ここでは一例としてcardsコンポーネントだけを取り上げます。

cards.tsx
import * as React from "react";
import Card from "./atoms/card";
import * as style from "./cards.styl";
import { CardName } from "model/protocol/game/card";
import { Building } from "model/protocol/game/building";
import Workers from "./atoms/workers";

type Props = {
    title: string,
    tooltip?: string,
    cards?: CardName[],
    buildings?: Building[]
};

const hand: React.FC<Props> = props => {
    const cards = props.cards ? props.cards.map((c, i) => <Card card={c} key={`${i}-${c}`} />) : null;
    const buildings = props.buildings ? props.buildings.map((b, i) => (
        <Card card={b.card} key={`${i}-${b.card}`}>
            <Workers owners={b.workersOwner} />
        </Card>
    )) : null;

    return (
        <section className={style.cards}>
            <h2 title={props.tooltip}>{props.title}</h2>
            {cards ? <ul>{cards}</ul> : null}
            {buildings ? <ul>{buildings}</ul> : buildings}
        </section>
    );
};

export default hand;

まとめ

今回の記事でCloud Firestoreとの通信、およびUIの構成について説明したため、これでゲーム作成に必要な要素を構築できるようになりました。
Firebase Authenticationを利用して、ゲームプレイにユーザ登録を要求するなど、触れていない部分も多くありますが、とりあえずStateモナドとCloud Firestoreの組み合わせ方については解説できたのではないかと思われます。

参考文献

Cloud Firestore

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

react-konvaでお絵描き

canvasを組み込んだwebサービスを作る機会があり、その際に利用したreact-konvaが使いやすかったので紹介です。

今回はreact-konvaとhookを利用して簡単なお絵描き機能を作っていきます。
以下のような感じです。
https://codesandbox.io/s/elastic-agnesi-pn76b
test.gif

react-konvaとは

canvasのjsフレームワークkonva.jsを名前の通りreactで利用できるものです。
konva.jsのオブジェクトクラスがコンポーネントとして提供されています。

konva.js

https://konvajs.org/docs/overview.html

react-konvaはStageというコンポーネントを土台に、その上にいろいろな要素を載せて実装を進めていきます。
例えば、LayerコンポーネントをStage上に複数載せることで、絵を描くときにあると嬉しいレイヤー機能を簡単に実装することができます。
イメージとして以下のようなコンポーネント階層を作っていきます(公式より抜粋)
スクリーンショット 2020-12-04 13.14.32.png

hookを使った実装ではstateにオブジェクト要素を詰め込んでいき、その内容をcanvas上に描画していきます。

canvas上に絵を描くという目的を達成するために必要な処理は大まかに以下です。
①mouseDownなどのeventに反応してstateに線の描画位置や色設定などを詰め込んでいく。
②Layerコンポーネント内でstateの値を走査し、オブジェクトを描画していく。

canvasの描画内容をstateで管理できるため、すごく楽に実装を進められます。

実装イメージ(いろいろ省いて抜粋)

sample.js
const App = () => {
  const [lines, setLines] = React.useState([]);
  const handleMouseDown = (e) => {
    const pos = e.target.getStage().getPointerPosition();
      // mouseDownなどのeventに反応してstateに値(線の描画位置や色設定など)を詰め込んでいきます。
      setLines([...lines,{points: [pos.x, pos.y], color, size}
    ]);
  };

  return (
    <>
       <Stage
         onMouseDown={handleMouseDown}
       >
          <Layer>
            {/* stateを走査して詰め込んだ値を描画していきます。 */}
            {lines.map((line, i) => (
              <Line
                key={i}
                points={line.points}
                stroke={line.color}
                strokeWidth={line.size}
                tension={0.5}
                lineCap="round"
              />
            ))}
          </Layer>
        </Stage>
      </>
  );
};
export default App;

最後に

業務はサーバーAPI構築がメインのためReactの勉強がてらと思いreact-konvaを選んだのですが、webは技術進歩が目覚ましく、すぐに置いていかれてしまいますね、、

(記事の内容と直接関係ないのですが)特に今では当たり前のように使われているオンラインエディター(今回はCodeSandbox)が使いやすく驚きました。

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

C4DとThree.jsで製品の3D表示ページを開発する

完成品の表示ページ:

https://capricorncd.github.io/blog/dist/three/index.html#/ClockObj

C4D

1. C4Dモデリング

C4Dを使って製品のモデルを作成する。

c4d.png

ご注意:

Discの使用を避けること、ブラウザで解析できないため。

各ジオメトリをマップする必要があります。グループマップは使用しないほうがいい。

2. *.objファイルをエクスポートする

file -> Export -> Wavefront OBJ(*.obj)

export.png

ソース実装

開発環境:Node.js/Webpack4/React16/Three.js

ソース:https://github.com/capricorncd/blog/tree/master/demos/three

1. Install

# "three": "^0.120.1"
npm i -S three
# or
yarn add three

2. ソース

src/components/ClockObj/core.js

import {
  AmbientLight,
  DirectionalLight, PerspectiveCamera,
  Scene, WebGLRenderer
} from 'three'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

let scene, renderer

/**
 * load resource 
 * @returns {Promise<unknown>}
 */
 function loadResource() {
   return new Promise((resolve, reject) => {
     const objLoader = new OBJLoader()
     const mtlLoader = new MTLLoader()
     // テクスチャファイルをロードする
     mtlLoader.load('static/clock.mtl', mtl => {
       // オブジェクトをロードする前に、まずテクスチャデータを設定します
       objLoader.setMaterials(mtl)
       // オブジェクトをロードする
       objLoader.load('static/clock.obj', res => {
         resolve(res)
       }, undefined, reject)
     }, undefined, reject)
  })
}

/**
 * 初期化
 */
function _init(el, obj) {
  // コンテナサイズを取得する
  // windowの場合、window.innerWidthとwindow.innerHeightで取得する
  const width = el.offsetWidth
  const height = el.offsetHeight

  // シーンを作成する
  scene = new Scene()
  // オブジェクトをシーンに追加する
  scene.add(obj)

  // 周囲光を作成する
  const ambientLight = new AmbientLight(0x666666)
  ambientLight.position.set(100, -100, -200)
  scene.add(ambientLight)

  // 指向性ライトを作成する
  const light = new DirectionalLight(0xcccccc, 1)
  light.position.set(2000, 1000, 1000)
  scene.add(light)

  // カメラを作成する
  const camera = new PerspectiveCamera(45, width / height, 1, 80000)
  camera.position.set(-150, -50, 300)

  // レンダラーを作成する
  renderer = new WebGLRenderer({
    antialias: true
  })
  // レンダリング領域のサイズを設定する
  renderer.setSize(width, height)
  // 背景色を設定する
  renderer.setClearColor(0x000000, 1)
  el.appendChild(renderer.domElement)

  const orbitControls = new OrbitControls(camera, el)
  orbitControls.addEventListener('change', render)

  function render() {
    renderer.render(scene, camera)
  }
  render()
}

/**
 * init
 */
export function init(el) {
  loadResource().then(res => {
    _init(el, res)
  }).catch(console.error)
}

/**
 * destroy
 */
export function destroy() {
  if (!scene || !renderer) return
  scene.remove()
  renderer.dispose()
  scene = null
  renderer = null
}

src/components/ClockObj/index.jsx

import React, { useEffect, useRef } from 'react'
import { destroy, init } from './core'

function ClockObjDemo() {
  const elRef = useRef()
  useEffect(() => {
    init(elRef.current)
    return () => {
      destroy()
    }
  }, [])
  return <main className="font-size-zero" ref={elRef} />
}

export default ClockObjDemo

完成品URL

https://capricorncd.github.io/blog/dist/three/index.html#/ClockObj

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

C4DとThree.jsで製品の3D表示ページの開発

完成品の表示ページ:

https://capricorncd.github.io/blog/dist/three/index.html#/ClockObj

C4D

1. C4Dモデリング

C4Dを使って製品のモデルを作成する。

c4d.png

ご注意:

Discの使用を避けること、ブラウザで解析できないため。

各ジオメトリをマップする必要があります。グループマップは使用しないほうがいい。

2. *.objファイルをエクスポートする

file -> Export -> Wavefront OBJ(*.obj)

export.png

ソース実装

開発環境:Node.js/Webpack4/React16/Three.js

ソース:https://github.com/capricorncd/blog/tree/master/demos/three

1. Install

# "three": "^0.120.1"
npm i -S three
# or
yarn add three

2. ソース

src/components/ClockObj/core.js

import {
  AmbientLight,
  DirectionalLight, PerspectiveCamera,
  Scene, WebGLRenderer
} from 'three'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

let scene, renderer

/**
 * load resource 
 * @returns {Promise<unknown>}
 */
 function loadResource() {
   return new Promise((resolve, reject) => {
     const objLoader = new OBJLoader()
     const mtlLoader = new MTLLoader()
     // テクスチャファイルをロードする
     mtlLoader.load('static/clock.mtl', mtl => {
       // オブジェクトをロードする前に、まずテクスチャデータを設定します
       objLoader.setMaterials(mtl)
       // オブジェクトをロードする
       objLoader.load('static/clock.obj', res => {
         resolve(res)
       }, undefined, reject)
     }, undefined, reject)
  })
}

/**
 * 初期化
 */
function _init(el, obj) {
  // コンテナサイズを取得する
  // windowの場合、window.innerWidthとwindow.innerHeightで取得する
  const width = el.offsetWidth
  const height = el.offsetHeight

  // シーンを作成する
  scene = new Scene()
  // オブジェクトをシーンに追加する
  scene.add(obj)

  // 周囲光を作成する
  const ambientLight = new AmbientLight(0x666666)
  ambientLight.position.set(100, -100, -200)
  scene.add(ambientLight)

  // 指向性ライトを作成する
  const light = new DirectionalLight(0xcccccc, 1)
  light.position.set(2000, 1000, 1000)
  scene.add(light)

  // カメラを作成する
  const camera = new PerspectiveCamera(45, width / height, 1, 80000)
  camera.position.set(-150, -50, 300)

  // レンダラーを作成する
  renderer = new WebGLRenderer({
    antialias: true
  })
  // レンダリング領域のサイズを設定する
  renderer.setSize(width, height)
  // 背景色を設定する
  renderer.setClearColor(0x000000, 1)
  el.appendChild(renderer.domElement)

  const orbitControls = new OrbitControls(camera, el)
  orbitControls.addEventListener('change', render)

  function render() {
    renderer.render(scene, camera)
  }
  render()
}

/**
 * init
 */
export function init(el) {
  loadResource().then(res => {
    _init(el, res)
  }).catch(console.error)
}

/**
 * destroy
 */
export function destroy() {
  if (!scene || !renderer) return
  scene.remove()
  renderer.dispose()
  scene = null
  renderer = null
}

src/components/ClockObj/index.jsx

import React, { useEffect, useRef } from 'react'
import { destroy, init } from './core'

function ClockObjDemo() {
  const elRef = useRef()
  useEffect(() => {
    init(elRef.current)
    return () => {
      destroy()
    }
  }, [])
  return <main className="font-size-zero" ref={elRef} />
}

export default ClockObjDemo

完成品URL

https://capricorncd.github.io/blog/dist/three/index.html#/ClockObj

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

【入力値反映されない】Reactでクラスを使ったときにフォームでハマった話【Cannot read property 'state' of undefined】

この記事は「株式会社オープンストリーム "小ネタ" Advent Calendar 2020」の 3 日目の記事です。

React でフォームを実装したときにいきなりハマった点を共有したいと思います。

環境

$ node --version
v14.15.0
package.json
{
...
  "dependencies": {
    "next": "10.0.3",
    "react": "17.0.1",
    "react-dom": "17.0.1"
  },
  "devDependencies": {
    "@types/node": "^14.14.10",
    "@types/react": "^17.0.0",
    "typescript": "^4.1.2"
  }
}

ちなみに私はVue(Nuxt)を使っていてReactに入門しています…!

state に紐づけたテキストボックスに入力しても反映されない

これはReactのドキュメントに書いてあるので、初めに読めば防げる問題ですが…

React で管理したいテキストボックスには handleChange 関数と onChange イベントを入れないと「入力しても反映されない」現象が起きます。

src/App.tsx
import React, { Component } from 'react';
import './App.css';

type Props = {}

type State = {
  todo: TodoItem[],
  currentText: string
}

type TodoItem = {
  title: String
}

class App extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state =  {
      todo: [
        { title: 'JavaScript覚える' },
        { title: 'jQuery覚える' },
        { title: 'ES2015覚える' },
        { title: 'React覚える' }
       ],
       currentText: ''
    }

    this.handleChange = this.handleChange.bind(this)
  }

  handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    this.setState({
      currentText: event.target.value
    })
  }

    render() {
    return (
    ...

        <input type="text" value={this.state.currentText} onChange={this.handleChange}/> <input type="button" value="追加" onClick={this.addTodo} />
      </div>
    );
  }

さらに、TypeScript で書くときはテキストボックスに入力したときの Event の型も指定します。
any にしなくても次の Event 型から使うことができます。

any 型で諦めない React.EventCallback - Qiita
https://qiita.com/Takepepe/items/f1ba99a7ca7e66290f24

クラス内の関数でstateを操作するとCannot read property 'state' of undefinedと出る

ボタンをクリックしたときにフォームで入力した値を使いたいですよね?ところがボタンをクリックしたときに次のメッセージが表示されます。

TypeError: Cannot read property 'state' of undefined

クラスの「中で」関数を書く場合、state など this から使うものは 関数をアロー関数にしないと関数が呼び出されたときに this がバインドされていない状態になってエラーになります。

アロー関数で書いてみると次の通りになります。

src/App.tsx
class App extends Component<Props, State> {

  ...
  addTodo =()=> {
    this.state.todo.push({
      title: this.state.currentText
    })

    this.setState({
      todo: this.state.todo,
      currentText: ''
    })
  }

  deleteTodo = (i: number)=> {
    this.state.todo.splice(i, 1)
    this.setState({
      todo: this.state.todo
    })
  }

  render() {
    return (
      <div>
        <h1>TODOアプリ</h1>
        <nav>
        <ul>{this.state.todo.map( (todo, i) => {
          return <li key={i}><input type="button" value="☓" onClick={() => this.deleteTodo(i)}/> {todo.title}</li>
        })}</ul>
        <input type="text" value={this.state.currentText} onChange={this.handleChange}/> <input type="button" value="追加" onClick={this.addTodo} />
      </div>
    );
  }

参考

(コードの元ネタ)
React で作る TODO アプリ前編 – React 入門 - to-R Media
https://www.to-r.net/media/react-tutorial13/

React.js で Form を扱う - Qiita
https://qiita.com/koba04/items/40cc217ab925ef651113

any 型で諦めない React.EventCallback - Qiita
https://qiita.com/Takepepe/items/f1ba99a7ca7e66290f24

「TypeError: Cannot read property ‘state’ of undefined」に出会ったら Javascript の”this”について真剣に考えよう
https://applingo.tokyo/article/1422

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

FNS歌謡祭のHPに実装されているアニメーションを自分も作成したい!!! ハンズオン!!

はじめに

お疲れ様です。
DMM WEBCAMP Advent Calendar 2020の4日目を担当させていただきます。:fist:
メンターの@koseiinfratopです。:basketball_player_tone5:

 みなさん先日OAされたFNS歌謡祭はみましたか?:dancer_tone4:
バンタンや3代目JSBなど豪華アーティストが出演されていて本当におもしろい番組でしたね。なかでもユーミンと嵐のコラボは感動的でした。:guardsman_tone4:
OA当日僕はふと思いました。嵐はいつごろ出番なのだろうかと。:rolling_eyes:

そこでFNS歌謡祭の公式HPを訪れたことで今回のAdvent Calendarで何を書くがが決定しました。


一度公式HPを訪れていただけるとわかるのですが、アーティストの画像にカーソルを乗せてみてください。カーソルを乗せると薄黒いボックスとアーティスト名が出てきます。僕は疑問に思いました。これはどうやって実装しているのかと。。。
ということで今回は疑問に思ったことを解消するためにFNSのHPのようにカーソルを画像に載せるとアニメーションが発火するような機能を実装しようと思います。

↓今回実装したアニメーション
qiita_2.gif


  • 開発効率を上げたかったため今回はホットリロードが可能なReact.jsでコーディングしました。
  • しれっとiTunesAPIも使用しています。


1.html(js)を記述

music.js
 ...*iTunesAPI関係の処理は割愛

return(


<div className="searchresult">
  {artistData.map(artistdata => (
    <ArtistData 
      key={artistdata.CollectionId.toString()}
      id={artistdata.CollectionId}
      name={artistdata.ArtistName}
      album={artistdata.AlbumName}
      albumUrl={artistdata.AlbumUrl}
      genre={artistdata.AlbumGenre}
      release={artistdata.AlbumRelease}               
    />

    )
   )}
</div>
);
artistdata.js
import React from 'react';

const ArtistData = (props) => {

    return(

        <div className={props.id ? 'album': 'noalbum'}>
            <div className="flex">
                <img src={props.albumUrl} alt={props.album} className="albumImage"/>
                <div>
                    <p>ジャンル: <b>{props.genre}</b></p>
                </div>

            </div>
            <div className="mask">
                <div className="caption">{props.album}</div>
            </div>
            <div>
                <p>アーティスト: <b>{props.name}</b></p>
                <p>アルバム名: <b>{props.album}</b></p>
                <p>リリース日: <b>{props.release}</b></p>
            </div>



        </div>
    );
}

export default ArtistData

2.CSSの記述

*ポイントだけコメントアウトを用いて解説します。

music.css
.album {
    width:          300px;
    height:         230px;
  /* overflow・・・アルバムクラスのdivタグ範囲内に内容が収まらない場合(今回で言うとmaskクラスのdivタグがはみ出る)の処理
    overflow:       hidden; /* 表示させないようにしている */

    margin:         10px 8px 10px 16px;
    position:      relative; 
    border:        ridge 10px #87CEFA;
}
.mask {
    width:          100%;
    height:         100%;
    position:       absolute;
    top:            -100%; /* 枠の上に配置し非表示にする。 */
    opacity:            0;  /* マスクスクラス内を透明化(0)にすることで非表示にする。*/

    background-color:   rgba(0,0,0,0.4);
    transition:         all 0.6s ease; 
}  

.caption {
    font-size:      130%;
    text-align:    center;
    color:          #fff;
}

img.alabumImage {
    width: 80%;
    height: 80%;
    position: absolute;
}

.album:hover .mask {  アルバムクラス内をhover(カーソルを乗せる)時に発火する。
    opacity:        1;  /* マスクを完全に不透明表示にする */
    padding-top:        80px;   /* ホバーで下にずらす */
    top: 0; /* 先ほどのtop: -100% から top: 0;にすることにより下から降りてくるように見せることができる */
}

完成!!!


デモンストレーション


qiita_article.gif

詳細な動きform内にアーティスト(曲名も可)を入力し検索ボタンをクリックすることでitunesapiからアーティストが発売したアルバムを取得し、表示しています。

今回のmusic.js, music.cssの全コード(※artistdata.jsは上記のコードが全コードです。)

music.js
import React, {useState } from 'react';
import axios from 'axios';
import ArtistData from './artistdata.js';
import '../styles/music.css'
const Music = () => {
    const [artist, setArtist] = useState('');
    const [artistData, setArtistData] = useState([{
        CollectionId: '',
        ArtistName: '',
        AlbumName: '',
        AlbumUrl: '',
        AlbumGenre: '',
        AlbumRelease: '',
    }]);

    async function itunesGet(params){
        try{
            const prm = params.trim();
            const response = await axios.get(`https://itunes.apple.com/search?term=${prm}&entity=album`)
            const responsedata = response.data.results
            const responseAPI = responsedata.map(value => {
                return {
                    CollectionId: value.collectionId, 
                    ArtistName: value.artistName,
                    AlbumName: value.collectionName, 
                    AlbumUrl: value.artworkUrl100,
                    AlbumGenre: value.primaryGenreName,
                    AlbumRelease: value.releaseDate,

                };
            }
            );
            setArtistData(responseAPI);
            console.log(artistData);
        }catch(error) {
            const {
                status, statusText
            } = error.response;
            console.log(`Error! HTTP Status: ${status} ${statusText}`)

        }

    };

    return (
        <div>
            <form
                onSubmit = {e => {
                    e.preventDefault();
                    const artistnameElement = e.target.elements["artist"];
                    console.log(artistnameElement.value);

                    itunesGet(artistnameElement.value);

                    setArtist(artistnameElement.value);

                    artistnameElement.value = '';

                }}
            >
                <input type="text" id="artist"
                    placeholder="アーティスト名または曲名を入力してください"
                />
                <button type="submit">検索する</button>


            </form>
            <p className="result">検索結果: <b>{artist}</b></p>
            <div className="searchresult">
            {artistData.map(artistdata => (
                <ArtistData 
                    key={artistdata.CollectionId.toString()}
                    id={artistdata.CollectionId}
                    name={artistdata.ArtistName}
                    album={artistdata.AlbumName}
                    albumUrl={artistdata.AlbumUrl}
                    genre={artistdata.AlbumGenre}
                    release={artistdata.AlbumRelease}

                />

            )
            )}
            </div>

        </div>
    )
}

export default Music
music.css
* {
    margin: 0 auto;
    padding: 0;
    box-sizing: border-box;
}


form > :first-child  {
    outline: none;
    border: 1px solid #aaa;
    transition: all .3s;
    border-radius: 2px;
  }
form > :first-child {
    width: 400px;
    font-size: 18px;
    height: 24px;
    padding: 2px 8px;
  }
form > :nth-child(1):focus {
    box-shadow: 0 0 7px #1abc9c;
    border: 1px solid #1abc9c;
}

form > :last-child {
    margin-top: 4px;
    margin-left: 7px;
    font-size: 16px;
    height: 40px;
    padding: 2px 8px;
}
form button {
    border: 1px solid #ccc;
    background-color: #FFFFFF;

    border-radius: 2px;
    cursor: pointer;
    box-shadow: 0px 2px 2px 0px rgba(0,0,0,.1);
}
form button:hover {
    box-shadow: 0px 2px 2px 2px rgba(0, 0, 0, .1);

}
.result {
    text-align: center;
}
.searchresult {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-around;
}
.noalbum{
    display: none;
}
.flex {
    padding: 3px 1px;
    display: flex;
    flex-direction: row;
}
.flex > :nth-child(2) {
    padding-top: 1.7em;
    padding-bottom: 0.5em;


}
.flex > :nth-child(2):nth-child(2){
    font-family: 'Courier New', Courier, monospace;
}

.album {
    width:          300px;
    height:         230px;
    overflow:       hidden; 
    margin:         10px 8px 10px 16px;
    position: relative;
    border: ridge 10px #87CEFA;
}
.mask {
    width:          100%;
    height:         100%;
    position:       absolute;
    top:            -100%;
    opacity:            0; /* マスクにする */

    background-color:   rgba(0,0,0,0.4);
    transition:     all 0.6s ease;
}  

.caption {
    font-size:      130%;
    text-align:         center;
    color:          #fff;
}

img.alabumImage {
    width: 80%;
    height: 80%;
    position: absolute;
}

.album:hover .mask {
    opacity:        1;  /* マスクを完全に不透明表示する */
    padding-top:        80px;   /* ホバーで下にずらす */
    top: 0;
}

まとめ

- 「このサイト、アプリはどのように実装されているのだろうか?」と疑問を持ち実際に自分で実装することで自分の技術力UPに繋がるということを改めて知ることができたそんな師走上旬でした。。。

裏話FNS公式HPのデベロッパーツールのSourceパネルと睨めっこしながら実装したのは内緒。。。



参考資料

2020 FNS歌謡祭 - フジテレビ
itunes api

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