20200407のReactに関する記事は13件です。

Reactとスプレッドシート(GAS)を使って3日でウェブサイトを作る

概要

2020年4月から独立してフリーランスになったので、事業用のウェブサイトを3日で作成しました。まぁ正確には4日なんですが、4/6(月)は仕事でほとんど作業できなかったんで、実質3日です。嘘はついてません。4日?記憶にございません。

というわけで、作ったサイト

とりあえず、ネットに散らばってるぼくの情報をかき集めてくれるようなポートフォリオサイトを作りました。

使ってるサービス・フレームワーク

  • React + TypeScript :フロントエンド実装
  • BULMA: CSSフレームワーク
  • Google Apps Script (Google スプレッドシート): DB兼APIサーバーとして
  • Firebase Cloud Functions: メッセージ送信用のAPIサーバーとして
  • reCAPTCHA v3: なんとなく
  • Slack Incoming webhook: メールの代わりに
  • Qiita feed / Hatena RSS feed: ブログとってくるよ

アーキテクチャはこんな感じです。

image.png

今のぼくのお仕事状況や、個人的な成果物などはGoogle Spread Sheetで管理して、こんな感じに取得させて表示しています。データの管理画面を作らなくっていいから楽ちん!

image.png

その他の情報も基本的には外のサービスから取得をしてるんで、Webサイトのデプロイは基本しなくてもコンテンツが充実していくようにしてます。メールはぼくがあんまり利用しないんで、ContactでもらったものはSlackに届くようにしています。

GASは本当はサーバーじゃないんで当然普通のサーバーより遅い(といっても測ってないけど)んですけど、個人のポートフォリオ用のウェブサイトに来る人なんて全然いないと思うのできっと無問題。
(本当はこのPost部分もGASにやらせたかったけど、CORS対策されてて無理だった…)

今週中に作る必要があった

なんでGAS使おうと思ったかというと、今週中にウェブサイトを作っておきたかったんですよね。

独立した直後に事業用の名刺を作ったんですが、

上記の隠してる箇所に、まだ作ってもいない(ドメインしかとってない)サイトのURLを載せてました。

「まぁいつか作ればいいし」

って当初はぬるく考えていたのですが、今週早速名刺を渡す用事ができちゃって、これはまずいと…。
ただウェブサイトはポートフォリオにもなりうるんで、外っ面だけでもちゃんと作っておかなきゃなぁと思い、バックエンドを極力実装せずにデータの更新などもできるようにしたいと考えた時、「まぁもうGASでいいや」ってなりました。

結果的にバックエンド側はreCAPTCHAの検証やSlackへの通知処理も含めて1時間ちょいで実装し終わったんで、かなり高速にやりたい機能作れたなと思ってます。
(フロント、というよりCSSが苦手なので、それに一番時間かけてた…)

もうちょい詳しく紹介

そんなわけで、作った中でいくつか機能をピックアップして紹介します。

React + TypeScript

image.png

フロント側はこれで作りました。
Reactは色々な書き方があるっぽいのですが、ぼくはFunctionComponent をメインに使って書きました。 useState とか色々使えて楽ちん。

たとえば、ブログの一覧を出すコンポーネントは以下みたいな感じです。

const initialState: any = null;
export const Blogs : React.FC<{}> = () => {
    // stateとそのsetterを作れる
    const [blogs, setBlogs] = useState<any>(initialState);

    // blogsがinitialStateの時だけ発動する
    // componentDidMountと同じような1度だけの処理ができる
    useEffect(() => {
        receiveData();
    }, [initialState]);

    function receiveData() {
        // データ取得処理
        $.ajax("url", { type: "get" })
         .done(data => { 
            // 取得した内容をblogsに設定する
            // stateが変更されたらReactは描画しなおす
            setBlogs(data);
         });
    }

    function showBlogs() {
        return blogs.map((e, i) => {
           // blogのデータひとつひとつでReactElementを作成
        );
    }

    // 描画時に呼ばれる最終的なReactElement
    return (
        <div className={`container works-root`}>
            <h1 className={"title"}>Blog</h1>
            {showBlogs()}
        </div>
    );
};

本当はjQueryは使わずにFetchAPI使おうと思ったんですが、IE対応されてないんでやめました。
一応はポートレートのサイトだし、IE使ってらっしゃる方が見たりするかもしれないですし。
といっても、IEだとうちのサイト見れないです。以下をやらないと駄目みたい。

React.jsがIE11で動かない問題を解決する
https://qiita.com/t-motoki/items/6d8476c93b49dc2582c4

ありがとうございました。

BULMA

image.png
https://bulma.io/

CSSフレームワークはBULMAを使いました。
ぼくは言語の中ではCSSが一番苦手で、なんで苦手かというとエラー吐いてくれない意図しない表示になった時にどこで影響受けてるのかすぐわからないしで、だからなんかアレなんです(´・ω・`)

特にReactのCSSフレームワークを使うと、中で色んなクラスが知らず知らずのうちに呼ばれたりしてるんで、なんか思てたんとちゃうってなった時、カスタマイズがすごい面倒だし、結構たくさんコンポーネントがあって覚えること多くなるしで、なるべくシンプルなものを探してたどり着きました。
(といっても、BULMAはReactのフレームワークじゃないんですが)

このフレームワークのいいところは、クラスの命名がとてもシンプルでわかりやすく、かつCSSオンリーな部分&カスタマイズしやすい部分ですね。コンポーネントも必要最低限のものだけがある感じなのもいい!

たとえば影付きのパネルを作りたければ以下で実装できます。

<div className="panel">
   <div class="panel-block">
      パネルになるよー
   </div>
</div>

また、Scssで作成されており、変数が定義されてるんでカスタマイズもやりやすいです。

もちろん シンプルな故の逆説的弊害 もあるようですが、3日程度で作れる規模のものを作るなら全然無問題でした。

Google Apps Script (Spread Sheet)

image.png

Googleで無料で使えるスプレッドシートですが、Google Apps Scriptを使うと簡易的なサーバーレスAPIとしても利用できます。データ入力や更新はそのままスプレッドシートでできちゃうので、管理画面を作る手間なども省けます。
また、最近GASはv8になってかなり使いやすくなりました。

設定など詳しいものは以下に記載されてますので、割愛。

今から10分ではじめる Google Apps Script(GAS) で Web API公開
https://qiita.com/riversun/items/c924cfe70e16ee3fe3ba

そして、お仕事状況を返す実装は以下の感じ。

function doGet(e) {
  const spread = SpreadsheetApp.getActive();
  const curent = spread.getSheetByName("SheetName");

  let result = {};
  result.body = curent.getRange("B1").getValue();

  var output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(JSON.stringify({status: 200, data: result}));
  return output;
}

簡単!(`・ω・´) あとはフロント側からAjax使って取得すれば簡単に動的なページが作れちゃいます。

ただ、AjaxでPostはできないっぽい?

本当はGASでPostしてごにょごにょしようと思ってたんですが、以下のようなエラーが発生してできませんでした。

image.png

調査に時間あんまりかけたくなかったんでちゃんと調べてないので、本当はできるのかもしれませんが(´・ω・`)
とはいえGetは普通にできるんで、データの更新や取得はSpread Sheetですることにしました。

QiitaとHatenaのRSSフィード

ブログの取得は、QiitaとHatenaそれぞれのRSSフィードを使ってAjaxで取得をしています。

HatenaのRSSフィードは、ブログのURLに/rss をつけると簡単にできちゃいます。

https://brbranch.hatenablog.jp/rss

Qiitaも同様で、マイページに /feed.atom をつけることでできます。

https://qiita.com/br_branch/feed.atom

ただ、QiitaはCORSの許可がされてないので、そのままだとAjaxで取得できません。そのため、CORS Proxyを利用します。

CORS Anyware
https://cors-anywhere.herokuapp.com/

具体的には、上記の後にQiitaのRSSフィードのURLを付け加えることで、Ajaxでの取得が可能になります。

// AjaxのURLに以下を指定する
https://cors-anywhere.herokuapp.com/https://qiita.com/br_branch/feed.atom

あとは、XMLDocumentとして取得ができるのでそのまま getElementsByTagName などを使ってコンテンツを読み込むだけで自分のウェブサイトに自分の書いた記事を埋め込むことができます。

reCAPTCHA + CloudFunctions + Slack Incoming Webhooks

メッセージ送信部分は CloudFunctionsとSlack Incoming Webhooksを使って実現してます。あと、なんとなくスパム対策としてreCAPTCHA v3も使ってます。

こんな感じで、メッセージを贈ろうとした際に可愛く表示されます。v3の場合、あの鬱陶しい「信号を探せ」みたいなものも表示されず、利用者が何も操作しなくてもスパム対策をしてくれます。

image.png

reCAPTCHAは以下から無料で簡単に導入できます。
https://www.google.com/recaptcha/intro/v3.html

導入記事:

reCAPTCHAは、クライアント側で払い出されたTokenを、サーバー側で検証することでスパム対策ができます。クライアント側はReactならコンポーネントライブラリがあるし、サーバー側の検証も、単にそのトークンをエンドポイントに投げるだけです。

import * as Request from 'request'
// 省略
Request.get("https://recaptcha.google.com/recaptcha/api/siteverify?secret=<シークレットキー>&response=" + request.body.token, (error, resp, body) => {
   if (error) {
       // エラー処理
   }
   const responseBody = JSON.parse(body);
   if (response.success) {
       // 検証OK
   }
   // 不正なトークン
});

また、Cloud Functionsもめちゃ楽にデプロイができます。

Firebase で Cloud Functions を簡単にはじめよう
https://qiita.com/tdkn/items/2ed2b01f2656fc50da8c

ただ、ここで注意しないといけないのは、Firebase Cloud Functions のSpark プランを使ってる場合、reCAPTCHAのエンドポイントのドメインは recaptcha.google.com でないといけません。 www.google.com を使うと、 EAI_AGAIN というエラーが発生しちゃいます。

とはいえ、結局 Firebase Spark プランではぼくがやりたいことできなかったんですけどね(´・ω・`)

image.png

Googleサービス専用なんで、Slackに送れないじゃないやだー。。。
さすがGoogle、マネタイズの方法わかってらっしゃる。。。

ちなみにSparkプランだとSlackに送ろうとする時にやはり EAI_AGAIN が発生します。
まぁ、 Blaze プランも無料枠あるんで、アップデートして使うことにしました。

あと上に書き忘れてたけど、フロント側は Firebase Hostingに乗せてます。これもすんごいデプロイ簡単。

Firebase Hosting でWebサイトを公開する方法
https://qiita.com/gupuru/items/25a6722f6f802d3a5250

Slack Incoming Webhooks

これは、外部からSlackの特定のチャンネルに投稿するためのSlack Appです。

Slack での Incoming Webhook の利用
https://slack.com/intl/ja-jp/help/articles/115005265063-Slack-%E3%81%A7%E3%81%AE-Incoming-Webhook-%E3%81%AE%E5%88%A9%E7%94%A8

Slack AppからWebhooksを使うように設定するだけで、Postリクエストで送りたいメッセージを送れます。今回作ったウェブサイトではreCAPTCHAの認証通ったらリクエストを投げるようにしています。

補足:今回使ったReactライブラリ

まぁ、バックエンド側はさておき、今回はフロント側そこそこ頑張ったので、その際に使った便利なライブラリをいくつか紹介して終わりにしたいと思います。

react-twitter-embed: Twitter埋め込み

https://www.npmjs.com/package/react-twitter-embed

Twitterのタイムライン埋め込みをReactで簡単にできるライブラリです。
ねぇ奥様、こんな感じでかけちゃうんですのよ。

 <TwitterTimelineEmbed sourceType="profile" screenName="br_branch"/>

react-google-recaptcha-v3: reCAPCHAライブラリ

https://www.npmjs.com/package/react-google-recaptcha-v3

reCAPCHAv3のトークンを払い出すためのライブラリです。
これもこんな簡単。

<GoogleReCaptchaProvider reCaptchaKey="<クライアントキー>">
   <GoogleReCaptcha onVerify={(token)=> { /* トークンもらえる */ }} />
</GoogleReCaptchaProvider>

react-loading-skeleton: Skeleton作成

https://www.npmjs.com/package/react-loading-skeleton

ねぇ奥様、 react-loading-skeleton があればロード中のスケルトンだって簡単につくれちゃうんですの。

Apr-07-2020 21-43-27.gif

最後に

そんな感じで、小規模のサイトなら無料で簡単につくれるので良い時代になりましたねぇ。

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

axiosのインポートで詰まった。

解決策

先に解決策を書いておく。
時間がない方はこれだけでも大丈夫。
default exportsなので、下記のように定義する。

import axios from 'axios';

それでは本題。

プロローグ

私はReact + Typescriptで業務に役立つWebアプリを作成している。
実装をしている中でプレーンなhttpリクエストの実装方法が少々煩雑ではないかと思っていた。

httpリクエストってみんながやってるはずなのになんでこんなに長いコードになってしまうんだ。
もっと美しいコードが書きたい。

そして見つけた。
axiosというライブラリを。

これで美しいコードを書くことができる。

第1章 npm install

見つけた以上インストールして、試してみるしかない。
さっそくインストールする。
念の為、ローカルインストールでインストールを行う。

npm install axios --save-dev

第2章 import

とりあえずimportしてみる。

import { axios } from 'axios';

しか〜し、なんかめっちゃエラーでた。
くそ。簡単に美しいコードがかけると思ったのに。

Module '"../../../../../TestTechnology/webpack/react/node_modules/axios"' has no exported member 'axios'. Did you mean to use 'import axios from "../../../../../TestTechnology/webpack/react/node_modules/axios"' instead?

第3章 エラー調査

調べてみるとaxiosって結構有名でインストールしている方がいっぱいいた。
どんな実装をしているか下記サイトで確認してみる。

https://www.haneca.net/react-http-messaging/

あれ、import文の部分が何かおかしい。
{}があるか、ないかこの部分に原因がありそうだ。

調べてみよう。
美しいコードを書くためにはこれぐらいの調査は惜しまないでやらないと。

モジュール化を行うexportには、モジュールごとに複数のexportを行うnamed exportsとモジュールごとに1つのexportを行うdefault exportsがある。

参考サイト:https://qiita.com/senou/items/a2f7a0f717d8aadabbf7

Default exports
フロントの開発でも、1つのモデルに付き1つのモジュールでコンストラクタやクラスが使われる。
ES6のモジュールは、最も重要なexportする値default exportという形で選択できる

参考サイト:https://qiita.com/senou/items/a2f7a0f717d8aadabbf7

なるほど。
ということは{}はnamed exportと呼ばれる複数のexportがある場合に使うimportの方法なのか。
で、{}なしがDefault exportsと呼ばれる1モジュールに付き1exportがある場合に使うのね。

ではaxiosはどちらに当たるのか。
npm_modules/axios/index.jsを確認してみる。

module.exports = require('./lib/axios');

あ、一個だけexportされてる。
だから{}がいらないのか!

まとめ

無事axiosをインストールすることが出来た。
axiosを使うことで美しいコードを書くことができそうだ。
エラーが起きたとき、なぜ起きたのかなぜ治ったのかをちゃんと理解することは重要だと改めて思う。

ちゃんと調べることって大事だね。

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

React + Rails API + axios + react-router-domで複数のフォームを作成する

こんばんは!スージーです!
最近、学習し始めたReactにてフォームの扱い方で苦労したので備忘録。

やりたい事

railsのcreateアクションにてフォームに2つのデータを入力してDBへ保存しビューで一覧表示する。
汚いレイアウトは勘弁して下さい。今回はCRUDの新規作成(Create)と一覧表示(Read)の実装をする。

demo

開発環境

Ruby 2.5.1
Rails 5.2.4
React 16.13.1

参考

Ruby on Rails+ReactでCRUDを実装してみた
https://qiita.com/yoshimo123/items/9aa8dae1d40d523d7e5d

[Rails API][React]axiosを用いて"POST"したい!
https://teratail.com/questions/166335

Ruby on Rails+ReactでCRUDを実装してみた
https://qiita.com/yoshimo123/items/9aa8dae1d40d523d7e5d

MacにNode.jsをインストール
https://qiita.com/kyosuke5_20/items/c5f68fc9d89b84c0df09

まずはRails側から実装開始

apiモードでrails newする

$ rails new neko -d mysql --api
// 「--api」→オプションをつける事でapiモードで作成
// 「-d mysql」→DBにはMySQLを利用

gem 'rack cors'をインストールとモデル・コントローラを作成

Gemfile
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors'# 注目! 29行目あたりにコメントアウトされているのでコメントアウトを外す
$ cd neko
// アプリ階層へ移動
neko $ bundle install
// rack-corsをbundle installする
neko $ rails g model post name:string neko_type:string
// モデルを作成する
neko $ rails db:create
neko $ rails db:migrate
neko $ rails g controller posts
// コントローラー作成。apiモードなのでviewは作成されない
posts_controller.rb
class PostsController < ApplicationController
  def index
    @post = Post.all
    render json: @post
  end

  def create
    @post = Post.create(name: params[:name], neko_type: params[:neko_type])
    render json: @post
  end

end

通常railsではprivate以下にpost_paramsなどメソッド作ってPost.create(post_params)としますが、Reactを使って実装すると以下のエラーが出てしまいました。

Started POST "/posts" for ::1 at 2020-04-06 20:37:54 +0900
Processing by ProductsController#create as HTML
  Parameters: {"name"=>"たま", "neko_type"=>"雑種"}
Completed 500 Internal Server Error in 5ms (ActiveRecord: 0.0ms)



NoMethodError (undefined method `permit' for "たま":String):

なので、今回は一旦@post = Post.create(name: params[:name], neko_type: params[:neko_type])で実装を進めていきます。

ルーティングを設定

routes.rb
Rails.application.routes.draw do
  resources :posts
end

Railsを起動

Reactが3000番ポート、Railsは3001番ポートを使います。
Rails API=>neko直下でrails s -p 3001
React=>neko>react_front直下でnpm start

$ rails s -p 3001
=> Booting Puma
=> Rails 5.2.4.2 application starting in development 
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.4 (ruby 2.5.1-p57), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:3001
* Use Ctrl-C to stop

// Getのエンドポイントにアクセス。
// ターミナルで新しいタブを開いて以下コマンドを叩いてみてjsonでレスポンスあるか確認
neko $ curl -G http://localhost:3001/posts/
=>[ ]
// 空の[]が返ってくるが、DBにデータが入っていないのでここはこのままでOK

Rails側で3000ポートのアクセスを許可する

config/initializers/cors.rb
# Be sure to restart your server when you modify this file.

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.

# Read more: https://github.com/cyu/rack-cors
8行目~16行目あたりにある以下のソースのコメントアウトを外す
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:3000'# 注目! 3000番ポートを許可する為に'http://localhost:3000'に変更する

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

ここまででRails側の実装は終了です。次はReact側を実装していきます。

次にReact側の実装開始

Node.jsをまだインストールしていなければインストールして下さい。

neko $ npm install -g create-react-app
// create-react-appをインストール
neko $ create-react-app react_front
// プロジェクトを作成
neko $ cd react_front
react_front $ npm start
// reactのデフォルトTOP画面がブラウザに表示される

ディレクトリ構造

ディレクトリ構造は以下のようになります。

.
|_app
|_bin
|_config
|_db
|_lib
|_log
|_public
|_react_front # 注目 ここがこれから実装するディレクトリ
| |_node_modules
| |_public
| |_src
| |_.gitignore
| |_package-lock.json
| |_package.json
| |_yarn.lock
|_storage
|_test
|_tmp
|_vender
|_.gitignore
|_.ruby-version
|_config.ru
|_Gemfile
|_Gemfile.lock
|_package-lock.json
|_README.md

ちなみにgitでバージョン管理する場合にリモートリポジトリにpushする時にエラーが起きるかもしれません(エラー内容忘れた)。macのFinderでディレクトリに潜っていくと分かるのですが、Railsの.gitディレクトリとReactの.gitディレクトリが存在します。それぞれプロジェクト作成した時に作成されてしまうからです。

スクリーンショット 2020-04-07 18.47.18.png

キャプチャは既に片方削除していますが、React(react_front内)の.gitディレクトリは削除して下さい。それでエラーは解決できます。

App.js

コードはこちらを参考にカラム名やパスを自分の環境に合わせて変更して利用させて頂きました。

App.js
import React from 'react';
import { BrowserRouter as Router, Link, Switch, Route } from 'react-router-dom';
import List from './Components/List';
import New from './Components/New';
import './App.css';

const App = () => {
  return (
    <Router>
      <div id="App">
        <Header />
          <Switch>
            <Route exact path='/' component={ List }/>
            <Route exact path='/new' component={ New } />
          </Switch>
      </div>
    </Router>
  );
}
const Header = () => (
  <nav>
    <div>
      <Link to="/">SampleTodo</Link>
      <Link to="/new">新規投稿</Link>
    </div>
  </nav>
)

export default App;

Components/List.jsx

画面遷移を簡単に実装する為にaxiosをインストールしておきます。

neko $ npm install axios --save
// 「--save」オプションでpackage.jsonにも反映
List.jsx
import React, { Component } from 'react';
import axios from 'axios'

class List extends Component {
  constructor(props) {
    super(props)
    this.state = {
      posts: []
    };
  }

  componentDidMount() {
    axios.get('http://localhost:3001/posts')
    .then((results) => {
      this.setState({products: results.data})
    })
    .catch((data) =>{
      console.log(data)
    })
  }

  render() {
    const {posts} = this.state
    return (
      <div>
        {posts.map((list) => {
            return <li key={list.id}> { list.name }{ list.neko_type }</li>
            // postsに格納されているdataをmapメソッドを使い1つ1つ取り出し表示させる
          })}
      </div>
    );
  }
}
export default List;

Components/New.jsx

Components/New.jsx
import React, { Component } from 'react';
import axios from 'axios'

class New extends Component {
  constructor(props){
    super(props);
    this.state = {
      name: '',
      neko_style: ''
    };
  }

  handleInputValue = (event) => {
    this.setState({
    // setStateメソッドで更新するstateと新しいstateの値を指定する
      [event.target.name]: event.target.value
    // フォームのname="neko_type"のnameを参照
       // this.setState({title: event.target.value})と同じ書き方となる
    });
  }

  handleSubmit = (e) => {
    e.preventDefault();
    axios({
      method : "POST",
      url : "http://localhost:3001/posts",
      data : { name: this.state.name, neko_type: this.state.neko_type }
    })
    .then((response)=> {
      console.log(this.props)
      this.props.history.push('/');
    })
    .catch((error)=> {
      console.error(error);
    });
  }

  render() {
    const { name, neko_type } = this.state;
    return (
      <div>
        <p>新規投稿</p>
        <div>
          <label>名前 : </label>
          <input type="text" name="name" value={ name } onChange={ this.handleInputValue } />
          // 2つ以上のフォームを扱う場合はname=""を書く
      // これで1つのhandleInputValueイベントで複数のstateの値を更新できる
          <label>猫種 : </label>
          <input type='text' name="neko_type" value={ neko_type } onChange={ this.handleInputValue } />
      // 2つ以上のフォームを扱う場合はname=""を書く
      // これで1つのhandleInputValueイベントで複数のstateの値を更新できる
          <input type="button" onClick={this.handleSubmit} value="Submit" />
        </div>
      </div>
    );
  }
}


export default New;

まとめ

最初は1つのデータだけ送るフォームでCRUDを実装してキャッキャウフフしていましたが、フォームって複数のデータを扱えないと使えない事に気づき、色々な参考記事をかいつまみながら実装しました。
まだまだReactとRails APIの連携に対して理解力が足ないので、もっともっと勉強が必要と感じました。
次は残りの更新(update)削除(delete)を実装して開発中のアプリに載せ替えしようと思います。

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

【React】クラスコンポーネントを関数コンポーネントに書き換え

はじめに

React Way 的にはクラスコンポーネントよりも関数コンポーネントのほうが望ましい。
そこでクラスコンポーネントで書かれたコードを関数コンポーネントに書き換えた。

書き換え前

App.jsx
import React, { Component } from 'react';//オブジェクトをimportする時は{}を使う
import Child from './Child';

class App extends Component {
  render() {
    const characters = [
      { name: 'ヒトカゲ', hp: 80, mp: 30 },
      { name: 'ゼニガメ', hp: 100, mp: 20 },
      { name: 'フシギダネ', hp: 120, mp: 10 }
    ];

    return (
      //React.Fragmentのシンタックスシュガー
      <>
        {/* game属性とcharacters属性がpropsとして子要素へ渡される */}
        <Child game="ぽけもん" characters={characters} />
      </>
    );
  }
}

export default App;
Child.jsx
import React, { Component } from 'react';

class Child extends Component {
  render() {
    // ※this.props.gameでも値は取れるけど分割代入活用
    const { game, characters } = this.props;

    return (
      <>
        <h1>{game}</h1>
        {characters.map(character => (
          // 一意なkeyをつける
          <div key={character.name}>
            <div>{character.name}</div>
            <div>{character.hp}</div>
            <div>{character.mp}</div>
          </div>
        ))}
      </>
    );
  }
}

export default Child;

TSでは

App.tsx
import React, { Component } from 'react';
import Child,{ Character } from './Child';

class App extends Component {
  render() {
    const characters :Character[] = [
      { name: 'ヒトカゲ', hp: 80, mp: 30 },
      { name: 'ゼニガメ', hp: 100, mp: 20 },
      { name: 'フシギダネ', hp: 120, mp: 10 }
    ];

    return (
      <>
        <Child game="ぽけもん" characters={characters} />
      </>
    );
  }
}

export default App;

Child.tsx
import React, { Component } from 'react';

export interface Character{
  name: string,
  hp: number,
  mp: number
}

interface CharactorListProps{
  game: string,
  characters: Character[]
}

//<CharactorListProps>はジェネリクスでpropsの型を指定する
class Child extends Component<CharactorListProps>{
  render() {
    const { game, characters } = this.props;

    return (
      <>
        <h1>{game}</h1>
        {characters.map(character => (
          <div key={character.name}>
            <div>{character.name}</div>
            <div>{character.hp}</div>
            <div>{character.mp}</div>
          </div>
        ))}
      </>
    );
  }
}

export default Child;

書き換え

Child.tsx
-import React, { Component } from 'react';
+import React, { FC } from 'react';//FCはFunctionComponentでもOK

export interface Character{
  name: string,
  hp: number,
  mp: number
}

interface CharactorListProps{
  game: string,
  characters: Character[]
}

-class Child extends Component<CharactorListProps>{
+//FCは関数コンポーネントの型//
+const Child: FC<CharactorListProps> = ({ game, characters }) => {


-render(){
-const { game, characters } = this.props;

  return (
    <>
      <h1>{game}</h1>
      {characters.map(character => (
        <div key={character.name}>
          <div>{character.name}</div>
          <div>{character.hp}</div>
          <div>{character.mp}</div>
        </div>
      ))}
    </>
  );
}

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

ReactのProps

まず

りあクト! TypeScriptで始めるつらくないReact開発 第2版
がとっってもわかりやすい、もうすぐ第3版が出るそう

親コンポーネントから子コンポーネントへpropsを渡す

はじめにJSで書いてみる

App.jsx
import React, { Component } from 'react';//オブジェクトをimportする時は{}を使う
import Child from './Child';

class App extends Component {
  render() {
    const characters = [
      { name: 'ヒトカゲ', hp: 80, mp: 30 },
      { name: 'ゼニガメ', hp: 100, mp: 20 },
      { name: 'フシギダネ', hp: 120, mp: 10 }
    ];

    return (
      //React.Fragmentのシンタックスシュガー
      <>
        {/* game属性とcharacters属性がpropsとして子要素へ渡される */}
        <Child game="ぽけもん" characters={characters} />
      </>
    );
  }
}

export default App;
Child.jsx
import React, { Component } from 'react';

class Child extends Component {
  render() {
    // ※this.props.gameでも値は取れるけど分割代入活用
    const { game, characters } = this.props;

    return (
      <>
        <h1>{game}</h1>
        {characters.map(character => (
          // 一意なkeyをつける
          <div key={character.name}>
            <div>{character.name}</div>
            <div>{character.hp}</div>
            <div>{character.mp}</div>
          </div>
        ))}
      </>
    );
  }
}

export default Child;

TSでも書いてみる

App.tsx
import React, { Component } from 'react';
import Child,{ Character } from './Child';

class App extends Component {
  render() {
    const characters :Character[] = [
      { name: 'ヒトカゲ', hp: 80, mp: 30 },
      { name: 'ゼニガメ', hp: 100, mp: 20 },
      { name: 'フシギダネ', hp: 120, mp: 10 }
    ];

    return (
      <>
        <Child game="ぽけもん" characters={characters} />
      </>
    );
  }
}

export default App;

Child.tsx
import React, { Component } from 'react';

export interface Character{
  name: string,
  hp: number,
  mp: number
}

interface CharactorListProps{
  game: string,
  characters: Character[]
}

//<CharactorListProps>はジェネリクスでpropsの型を指定する
class Child extends Component<CharactorListProps>{
  render() {
    const { game, characters } = this.props;

    return (
      <>
        <h1>{game}</h1>
        {characters.map(character => (
          <div key={character.name}>
            <div>{character.name}</div>
            <div>{character.hp}</div>
            <div>{character.mp}</div>
          </div>
        ))}
      </>
    );
  }
}

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

【React】クラスコンポーネントから関数コンポーネントへ

はじめに

React Way 的にはクラスコンポーネントよりも関数コンポーネントのほうが望ましい。
そこでクラスコンポーネントで書かれたコードを関数コンポーネントに書き換えた。

書き換え前

App.jsx
import React, { Component } from 'react';//オブジェクトをimportする時は{}を使う
import Child from './Child';

class App extends Component {
  render() {
    const characters = [
      { name: 'ヒトカゲ', hp: 80, mp: 30 },
      { name: 'ゼニガメ', hp: 100, mp: 20 },
      { name: 'フシギダネ', hp: 120, mp: 10 }
    ];

    return (
      //React.Fragmentのシンタックスシュガー
      <>
        {/* game属性とcharacters属性がpropsとして子要素へ渡される */}
        <Child game="ぽけもん" characters={characters} />
      </>
    );
  }
}

export default App;
Child.jsx
import React, { Component } from 'react';

class Child extends Component {
  render() {
    // ※this.props.gameでも値は取れるけど分割代入活用
    const { game, characters } = this.props;

    return (
      <>
        <h1>{game}</h1>
        {characters.map(character => (
          // 一意なkeyをつける
          <div key={character.name}>
            <div>{character.name}</div>
            <div>{character.hp}</div>
            <div>{character.mp}</div>
          </div>
        ))}
      </>
    );
  }
}

export default Child;

TSでは

App.tsx
import React, { Component } from 'react';
import Child,{ Character } from './Child';

class App extends Component {
  render() {
    const characters :Character[] = [
      { name: 'ヒトカゲ', hp: 80, mp: 30 },
      { name: 'ゼニガメ', hp: 100, mp: 20 },
      { name: 'フシギダネ', hp: 120, mp: 10 }
    ];

    return (
      <>
        <Child game="ぽけもん" characters={characters} />
      </>
    );
  }
}

export default App;

Child.tsx
import React, { Component } from 'react';

export interface Character{
  name: string,
  hp: number,
  mp: number
}

interface CharactorListProps{
  game: string,
  characters: Character[]
}

//<CharactorListProps>はジェネリクスでpropsの型を指定する
class Child extends Component<CharactorListProps>{
  render() {
    const { game, characters } = this.props;

    return (
      <>
        <h1>{game}</h1>
        {characters.map(character => (
          <div key={character.name}>
            <div>{character.name}</div>
            <div>{character.hp}</div>
            <div>{character.mp}</div>
          </div>
        ))}
      </>
    );
  }
}

export default Child;

書き換え

Child.tsx
-import React, { Component } from 'react';
+import React, { FC } from 'react';//FCはFunctionComponentでもOK

export interface Character{
  name: string,
  hp: number,
  mp: number
}

interface CharactorListProps{
  game: string,
  characters: Character[]
}

-class Child extends Component<CharactorListProps>{
+//FCは関数コンポーネントの型//
+const Child: FC<CharactorListProps> = ({ game, characters }) => {


-render(){
-const { game, characters } = this.props;

  return (
    <>
      <h1>{game}</h1>
      {characters.map(character => (
        <div key={character.name}>
          <div>{character.name}</div>
          <div>{character.hp}</div>
          <div>{character.mp}</div>
        </div>
      ))}
    </>
  );
}

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

【React】ルーティング画面を作ってみた【react-router-dom】

React Router: Declarative Routing for React.js
このサイトを参考に自分なりにアレンジ

import React from 'react';
import { useSelector } from "react-redux";
import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
import Top from './components/Top';
import Nav from './components/Nav';
import SignIn from './components/SignIn';
import SignUp from './components/SignUp';

export default () => {
  return (
    <Router>
      <Switch>
        <Route path="/signin" component={SignIn} />
        <Route path="/signup" component={SignUp} />
        <PrivateRoute path="/" token={token}>
          {/* ↓↓↓ ここに書かれているものが children に渡される ↓↓↓ */}
          <Top />
          <Nav />
          {/* ↑↑↑ ここに書かれているものが children に渡される ↑↑↑ */}
        </PrivateRoute>
      </Switch>
    </Router>
  );
}

const PrivateRoute = ({ children, ...rest }) => {
  const token = useSelector(state => state.token);

  return (
    <Route
      {...rest}
      render={({ location }) => {
        if (isAuthenticated(token)) {
          // 認証済みの場合は、トップページを表示する
          return children;
        }
        if (location.pathname === "/") {
          // URL が "/" の場合はサインインページを表示する
          return <SignIn />;
        }
        // 未認証で、URL が "/" 以外の場合は "/" にリダイレクトする
        // URL が "/" となるので、結果的にサインインページが表示される
        return <Redirect to={{ pathname: "/" }} />;
      }}
    />
  );
}

// 認証判定用のダミー関数
const isAuthenticated = token => token !== null;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

カルーセル実装で気をつけたこと

前提条件

  • React環境で動くこと
  • カルーセルを入れることによりパフォーマンスが低下しないこと

実装

props

  • 表示領域に表示するコンテンツ数
  • keyExtractor
  • カルーセルに表示するデータ(array)
  • カルーセルに表示するコンポーネント(render props)

ライブラリの導入検討

  • react-slick(https://react-slick.neostack.com/ )について導入検討したが、以下理由で断念。
    • パフォーマンスは問題なし
    • リンク数が奇数だった場合にうまく挙動しない
    • 表示数が端数の場合うまく挙動しない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WIP: カルーセル実装で気をつけたこと

前提条件

  • React環境で動くこと
  • カルーセルを入れることによりパフォーマンスが低下しないこと

設計

  • 表示数が端数の場合、端数分だけ遷移するように調整する
    ex.表示領域に表示する数が4なのに対し総数が5の場合、1つ分だけスクロールさせる
  • 画面幅をリサイズした際にカルーセルの表示サイズ、移動距離を可変させる
  • SPページではスクロール表示に切り替える

実装

CarouselComponent全体

import React, { useState, useEffect, useRef } from 'react'
import styled from '@emotion/styled'
import { PressableView, Row, View } from '../../Foundations'
import { layout, motion, color } from '../../../style'
import { Ticker } from '../../../lib/client/Ticker'

const MD_PADDING = layout.grid * 4 //画像間のpadding

interface Props<T> {
  displayContentsNum: number
  keyExtractor: (arg: T, index: number) => string
  list: T[]
  render: (arg: T) => React.ReactNode
}

// カルーセルの移動距離を計算
const createTranslateXByRawWidth = (
  width: number,
  displayContentsNum: number,
  mdPadding: number,
  nowRowNum: number,
  isRemainder: boolean,
  remainder: number
) => {
  const bodyWidth = -width - mdPadding
  const remainderWidth = (bodyWidth / displayContentsNum) * remainder
  const val = isRemainder
    ? bodyWidth * (nowRowNum - 1) + remainderWidth // eslint-disable-line no-mixed-operators
    : bodyWidth * nowRowNum // eslint-disable-line no-mixed-operators

  return `translateX(${val}px)`
}

const CarouselComponent = <T extends any>(props: Props<T>) => {
  const { displayContentsNum, keyExtractor, list, render } = props

  const mdPadding = MD_PADDING
  const totalContentsNum = list.length
  const remainder = totalContentsNum % displayContentsNum
  const totalRowNum = Math.ceil(totalContentsNum / displayContentsNum) - 1
  const isLessThanTotal = totalRowNum > 0

  const elem = useRef<HTMLDivElement>(null)
  const scrollElem = useRef<HTMLDivElement>(null)
  const [bodyWidth, setBodyWidth] = useState(0)
  const [isRemainder, setIsRemainder] = useState(false)
  const [rowIndex, setRowIndex] = useState(0)
  const [leftBtnIsActive, setLeftBtnIsActive] = useState(false)
  const [rightBtnIsActive, setRightBtnIsActive] = useState(isLessThanTotal)
  const [animationIsActive, setAnimationIsActive] = useState(true)
  const rowTransform = createTranslateXByRawWidth(
    bodyWidth,
    displayContentsNum,
    mdPadding,
    rowIndex,
    isRemainder,
    remainder
  )
  const resetScroll = () => {
    if (scrollElem.current) {
      scrollElem.current.scrollLeft = 0
    }
  }

  useEffect(() => {
    const getWidthTicker = new Ticker({
      onTicking: () => {
        if (elem.current) {
          setBodyWidth(elem.current.clientWidth)
        }
      },
      onStart: () => {
        setAnimationIsActive(false)
      },
      onEnd: () => {
        setAnimationIsActive(true)
      },
      throttle: 30,
    })

    const handler = (e: Event) => {
      // Tickingぜす、また2重でわざわざifで判定しているのは、リサイズイベントの最初で確実に走らせて、
      // 二回め以降window.innerWidthプロパティを参照したくないので細かく制御しています。
      if (scrollElem.current) {
        const isScrollLeftNotZero = scrollElem.current.scrollLeft !== 0
        if (isScrollLeftNotZero) {
          const isOverMd = window.innerWidth > layout.breakpoint.md
          if (isOverMd) {
            resetScroll()
          }
        }
      }
      getWidthTicker.dispatch(e)
    }

    window.addEventListener('resize', handler)
    return () => window.removeEventListener('resize', handler)
  }, [])

  useEffect(() => {
    setAnimationIsActive(false)
    setIsRemainder(false)
    setRowIndex(0)
    resetScroll()
    setLeftBtnIsActive(false)
    setRightBtnIsActive(isLessThanTotal)
    requestAnimationFrame(() => setAnimationIsActive(true))
  }, [list])

  const setBtnIsActive = (rowNum: number) => {
    const newLeftBtnIsActive = rowNum !== 0
    const newRightBtnIsActive = rowNum !== totalRowNum
    setLeftBtnIsActive(newLeftBtnIsActive)
    setRightBtnIsActive(newRightBtnIsActive)
  }

  const onButtonLeftClick = () => {
    if (leftBtnIsActive) {
      const nextRowNum = rowIndex - 1
      const isNotRemaindable = nextRowNum === 0 && remainder
      setRowIndex(nextRowNum)
      setBtnIsActive(nextRowNum)
      if (elem.current) {
        setBodyWidth(elem.current.clientWidth)
      }
      if (isNotRemaindable) {
        setIsRemainder(false)
      }
    }
  }

  const onButtonRightClick = () => {
    if (rightBtnIsActive) {
      const nextRowNum = rowIndex + 1
      const isRemaindable = totalRowNum === nextRowNum && remainder
      setRowIndex(nextRowNum)
      setBtnIsActive(nextRowNum)
      if (elem.current) {
        setBodyWidth(elem.current.clientWidth)
      }
      if (isRemaindable) {
        setIsRemainder(true)
      }
    }
  }

  return (
    <View ref={elem} onSwipeRightAuto={onButtonLeftClick} onSwipeLeftAuto={onButtonRightClick}>
      <CarouselButton
        moveDirection="left"
        onClick={onButtonLeftClick}
        isActive={leftBtnIsActive}
        animationIsActive={animationIsActive}
      />
      <CarouselButton
        moveDirection="right"
        onClick={onButtonRightClick}
        isActive={rightBtnIsActive}
        animationIsActive={animationIsActive}
      />
      <ContentsWrap ref={scrollElem}>
        <ContentsRow style={{ transform: rowTransform }} animationIsActive={animationIsActive}>
          {list.map((data, index) => (
            <BoxWrap key={keyExtractor(data, index)} displayContentsNum={displayContentsNum} mdPadding={mdPadding}>
              {render(data)}
            </BoxWrap>
          ))}
        </ContentsRow>
      </ContentsWrap>
    </View>
  )
}

export const Carousel = React.memo(CarouselComponent) as typeof CarouselComponent

const Button = styled(PressableView)({
  display: 'none',

    display: 'flex',
    position: 'absolute',
    top: '50%',
    transform: 'translateY(-50%)',
    width: 48,
    height: 48,
    border: `solid 1px ${color.ui.bgDark}`,
    borderRadius: '50%',
    background: color.pallet.surface,
    zIndex: 10,
    '&::after': {
      position: 'absolute',
      top: 0,
      bottom: 0,
      margin: 'auto',
      content: '""',
      width: 12,
      height: 12,
      borderRight: `2px solid ${color.ui.anotherArea.onWhite}`,
      borderTop: `2px solid ${color.ui.anotherArea.onWhite}`,
    },
    ':hover': {
      borderColor: color.pallet.accent,
      '&::after': {
        borderColor: color.pallet.accent,
      },
    },
  },
})

type CarouselButtonProps = {
  moveDirection: 'left' | 'right'
  isActive: boolean
  animationIsActive: boolean
}

const CarouselButton = styled(Button)<CarouselButtonProps>(({ moveDirection, isActive, animationIsActive }) => {
  const dirStyle = (dir => {
    switch (dir) {
      case 'left': {
        return {
          left: -32,
          '&::after': {
            left: 19,
            transform: 'rotate(225deg)',
          },
        }
      }

      case 'right': {
        return {
          right: -32,
          '&::after': {
            right: 19,
            transform: 'rotate(45deg)',
          },
        }
      }

      default:
        return {}
    }
  })(moveDirection)

  return {
    ...dirStyle,
    [layout.mqOfBreakpoint.md]: {
      transition: animationIsActive ? `all ${motion.duration.expand}ms ${motion.curves.base}` : 'unset',
      display: isActive ? 'flex' : 'none',
      '&::after': {
        transition: animationIsActive ? `all ${motion.duration.expand}ms ${motion.curves.base}` : 'unset',
      },
    },
  }
})

const ContentsWrap = styled(View)({
  overflowX: 'scroll',

    overflow: 'hidden',
  },
})

type ContentsRowProps = {
  animationIsActive: boolean
}

const ContentsRow = styled(Row)<ContentsRowProps>(({ animationIsActive }) => ({
  flexWrap: 'nowrap',

    transform: 'none !important',
  },

    transition: animationIsActive ? `transform ${motion.duration.base}ms ${motion.curves.base}` : 'unset',
    willChange: 'transform',
  },
}))

type BoxWrapProps = {
  displayContentsNum: number
  mdPadding: number
}

const BoxWrap = styled(Row)<BoxWrapProps>(({ displayContentsNum, mdPadding }) => ({
  marginRight: layout.grid,
  flexGrow: 0,
  ':first-of-type': {
    marginLeft: layout.grid * 2,
  },
  ':last-of-type': {
    marginRight: 0,
    paddingRight: layout.grid * 2,
  },

    width: `calc((100% - ${mdPadding * (displayContentsNum - 1)}px) / ${displayContentsNum})`,
    maxWidth: `calc((100% - ${mdPadding * (displayContentsNum - 1)}px) / ${displayContentsNum})`,
    marginRight: mdPadding,
    ':first-of-type': {
      marginLeft: 0,
    },
    ':last-of-type': {
      paddingRight: 0,
    },
  },
}))

props

  • 表示領域に表示する数
  • keyExtractor
  • カルーセルに表示するデータ(array)
  • カルーセルに表示するコンポーネント(render props)

実装中の気づきなど

画像幅については画像自体に持たせずにCarouselComponent内でcalcで算出する

  • 画像幅は下記計算にてcalcで算出。
    • 画像自体には width: 100% 指定で表示できるようにする(高さはratioで自動算出)
// mdPadding ・・・画像間のpadding
// displayContentsNum - 1 ・・・表示領域上の画像間のpaddingの個数
// displayContentsNum ・・・表示領域上の画像の個数

width: `calc((100% - ${mdPadding * (displayContentsNum - 1)}px) / ${displayContentsNum})`,
maxWidth: `calc((100% - ${mdPadding * (displayContentsNum - 1)}px) / ${displayContentsNum})`,

画像間のpaddingは固定で持つ

  • 同サイトに使うカルーセルであればデザインが統一されるため固定で持ったほうがいい
  • デザイナーと調整してどうしても変更が必要になった場合に可変できるよう調整する

カルーセルの表示位置は left ではなく transform をつかって動かす

  • カルーセルの表示位置を left: Xpx で持つとパフォーマンス性が低く、カルーセル自体の動きもなめらかにならないため、transform: translateX(Xpx) で使う
  • willChange: 'transform' を入れることにより、よりなめらかな動きを再現できる

参考:https://www.webprofessional.jp/achieve-60-fps-mobile-animations-with-css3/

  position:left でカルーセル移動させた場合
  

  transform: translateX() でカルーセル移動させた場合
  

カルーセルの移動距離の計算

  • 画像幅同様、calcにて算出したかったがIEでcalcだとtransitionが効かないというバグにぶち当たる。
    そのため今回はcalcではない方法で実装。
  • 移動距離については以下関数で計算。
const createTranslateXByRawWidth = (
  width: number, //CarouselComponentの表示幅(useRefで取得)
  displayContentsNum: number,
  mdPadding: number,
  nowRowNum: number, //移動数(初期値は0, ボタン押下ごとに1増減する)
  isRemainder: boolean,
  remainder: number
) => {
  const bodyWidth = -width - mdPadding // (画像幅 + padding幅) * 画像の個数分
  const remainderWidth = (bodyWidth / displayContentsNum) * remainder // 画像幅 + padding幅
  const val = isRemainder
    ? bodyWidth * (nowRowNum - 1) + remainderWidth // eslint-disable-line no-mixed-operators
    : bodyWidth * nowRowNum // eslint-disable-line no-mixed-operators

  return `translateX(${val}px)`
}
  • 画面幅がリサイズされた際にも関数が発火するようにする
  useEffect(() => {
    const getWidthTicker = new Ticker({
      onTicking: () => {
        if (elem.current) {
          setBodyWidth(elem.current.clientWidth)
        }
      },
      onStart: () => {
        setAnimationIsActive(false)
      },
      onEnd: () => {
        setAnimationIsActive(true)
      },
      throttle: 30,
    })

    const handler = (e: Event) => {
      // Tickingぜす、また2重でわざわざifで判定しているのは、リサイズイベントの最初で確実に走らせて、
      // 二回め以降window.innerWidthプロパティを参照したくないので細かく制御しています。
      if (scrollElem.current) {
        const isScrollLeftNotZero = scrollElem.current.scrollLeft !== 0
        if (isScrollLeftNotZero) {
          const isOverMd = window.innerWidth > layout.breakpoint.md
          if (isOverMd) {
            resetScroll()
          }
        }
      }
      getWidthTicker.dispatch(e)
    }

    window.addEventListener('resize', handler)
    return () => window.removeEventListener('resize', handler)
  }, [])
  • transformはDOM上にstyle属性として指定する

    • StyledComponent上に書くと、値が変化する都度クラスを新規作成してしまうため
  • カルーセルの表示幅についてはuseRefで取得する

const elem = useRef<HTMLDivElement>(null)
const width = elem.current.clientWidth
︙
return (
    <View ref={elem}></View>
)

レンダリング時のみアニメーションを無効化する

// アニメーション実行有無
const [animationIsActive, setAnimationIsActive] = useState(true)

useEffect(() => {
  //アニメーションの実行をfalseにする
  setAnimationIsActive(false)

  // 表示を初期化
  setCarouselPosition(0)
  setNowContentsNum(displayContentsNum)
  setLeftBtnIsActive(false)
  setRightBtnIsActive(totalContentsNum > displayContentsNum)

  // アニメーション実行をtrueにする
  requestAnimationFrame(setAnimationIsActive(true))
}, [totalContentsNum])

mapで繰り返しする要素のkeyの値をkeyExtractorで指定する

  • keyExtractorを使うことで呼び出し元からkeyの値を渡すことができる(render propsと仕組みは似ている)

ライブラリの導入検討

  • react-slick(https://react-slick.neostack.com/ )について導入検討したが、以下理由で断念。
    • パフォーマンスは問題なし
    • リンク数が奇数だった場合にうまく挙動しない
    • 表示数が端数の場合うまく挙動しない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Componentの外部をクリックしたら発火するCustom Hooks【React】

概要

Componentの外部をクリックしたときに発火するイベントを管理したい。

コード

Componentの外部をクリックしたら発火するCustom Hooks作りました。

export const useOutsideClickEvent = (
  ref: MutableRefObject<any>,
  onClick: () => void
) => {
  const clickListener = useCallback(
    (e: MouseEvent) => {
      if ((ref?.current as any).contains(e.target)) {
        return;
      }
      onClick();
    },
    [ref.current, onClick]
  );

  useEffect(() => {
    document.addEventListener('click', clickListener);
    return () => {
      document.removeEventListener('click', clickListener);
    };
  }, []);
};

こんな感じで使うと、refで指定されたdivの外部をクリックされたときにsetIsOpen(false)が実行されます。

const Dialog: FC = () => {
    const ref = useRef(null);
    const [isOpen, setIsOpen] = useState<boolean>(false);
    useOutsideClickEvent(ref, () => setIsOpen(false));

    return (
        <div ref={ref}>
            ...
        </div>
    )
}

注意

ClickEventの実行時にrefで指定したDOMの中に要素が入っていないといけないので、動的にコンテンツを書き換える場合は注意が必要です。

if ((ref.current as any).contains(e.target)) {

追記

似たようなのがありました。
https://usehooks.com/useOnClickOutside/

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

Jest encountered an unexpected token解決法

react-native-cameraを使ったアプリのテストをJestでする際、こんなエラーに遭遇した

  TypeError: Cannot read property 'Aspect' of undefined

  at Object.<anonymous> (node_modules/react-native-camera/src/Camera.js:425:113)
  at Object.<anonymous> (node_modules/react-native-camera/src/index.js:3:38)

他にもreact-native-unimodulesで

Jest encountered an unexpected token

なんかが出たりする

結論から先に書くと、これらのエラーは

jest.mock('{モジュール名}', () => '{クラス名}');

でモック化すると解決した。

react-native-cameraなら

jest.mock('react-native-camera', () => 'Camera');

となる。

Jest公式によると、react-nativeに組み込まれたJestプリセットにはデフォルトのモックが付属しているものの、いくつかのコンポーネントにはそれが無く、ネイティブコードに依存しているためマニュアルでモック化する必要があるとかなんとか

正直Jestのモックとはそもそも何ぞや?というところからわかっておらず現状おまじないと化してるので、後で調べておきたい

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

React propsとsteat

個人メモ

propsとstate

Propsは属性の値を取得する

  constructor(props){
    super(props);
    this.state = {
      msg: 'Hello Component.',
    };
  }
ReactDOM.render(
  <React.StrictMode>
    <App msg="Hello App."/>
  </React.StrictMode>,
  document.getElementById('root')
);

上記ではstateとpropsを定義しています.
この中のpropsだけを取得.

<p>{this.props.msg}</p>

# これで Hello App.  が取得できる

stateを取得

<p>{this.state.msg}</p>

# これで Hello Component.  が取得できる

まとめ

・propsは属性を定義した値を取得(例:<App msg="Hello App."/>)
・stateはthis.stateで定義した値を取得(例:this.state ={msg: 内容})

参考

https://qiita.com/kyrieleison/items/78b3295ff3f37969ab50

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

react-scrollを実装してみた

最近の勉強で学んだ事を、ノート代わりにまとめていきます。
主に自分の学習の流れを振り返りで残す形なので色々、省いてます。
Webエンジニアの諸先輩方からアドバイスやご指摘を頂けたらありがたいです!

smoothなスクロールを実装したい!

まずはreact-scrolをインストール

$ npm install react-scroll

Linkタグを使用します!Linkの後にオプションをつけます。これはgithubにある公式のドキュメントからコピペできます。
to=には移遷したい先のIdを指定しましょう。

smoothスクロールを実装するため、smoothはtrueにしておきます。offsetはpadding-topのようなもので、移遷する位置を指定できます。durationにはスクロールする秒数を指定しましょう。

⬇️Props/Optionsを見てください
react-scroll

TopPage/index.js
import React from 'react';
import { Link } from 'react-scroll'

省略

<nav>
   <ul className="navi">
     <li>
       <Link activeClass="active" to="Home" spy={true} smooth={true} offset={50} duration={500}>
             Home
         </Link>
      </li>

      <li>
         <Link activeClass="active" to="about" spy={true} smooth={true} offset={50} duration={500}>
            about
         </Link>
      </li>
   </ul>
</nav>

省略

 <section className="Home" name="Home">
 </section>

 <section className="about" name="about">
 </section>

上記のようにto=には移遷したい先のIdはname="Home"の様に指定する必要があります。
これでsmoothなスクロールを実現できました!

参考記事

react-scroll
Implementing Smooth Scrolling in React
React-Scrollでsmoothなスクロールを実装してみた。

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