- 投稿日:2020-04-07T21:50:01+09:00
Reactとスプレッドシート(GAS)を使って3日でウェブサイトを作る
概要
2020年4月から独立してフリーランスになったので、事業用のウェブサイトを3日で作成しました。まぁ正確には4日なんですが、4/6(月)は仕事でほとんど作業できなかったんで、実質3日です。嘘はついてません。4日?記憶にございません。
というわけで、作ったサイト
事業用のサイト作っちゃった(
— ⛩branch@個人アプリ開発者⛩ (@br_branch) April 7, 2020・ω・´)<br>React+TypeScript+GoogleAppsScript(スプレッドシート)を主に作って3日で作成しました(
・ω・´)https://t.co/dg4cuhqX5mとりあえず、ネットに散らばってるぼくの情報をかき集めてくれるようなポートフォリオサイトを作りました。
使ってるサービス・フレームワーク
- React + TypeScript :フロントエンド実装
- BULMA: CSSフレームワーク
- Google Apps Script (Google スプレッドシート): DB兼APIサーバーとして
- Firebase Cloud Functions: メッセージ送信用のAPIサーバーとして
- reCAPTCHA v3: なんとなく
- Slack Incoming webhook: メールの代わりに
- Qiita feed / Hatena RSS feed: ブログとってくるよ
アーキテクチャはこんな感じです。
今のぼくのお仕事状況や、個人的な成果物などはGoogle Spread Sheetで管理して、こんな感じに取得させて表示しています。データの管理画面を作らなくっていいから楽ちん!
その他の情報も基本的には外のサービスから取得をしてるんで、Webサイトのデプロイは基本しなくてもコンテンツが充実していくようにしてます。メールはぼくがあんまり利用しないんで、ContactでもらったものはSlackに届くようにしています。
GASは本当はサーバーじゃないんで当然普通のサーバーより遅い(といっても測ってないけど)んですけど、個人のポートフォリオ用のウェブサイトに来る人なんて全然いないと思うのできっと無問題。
(本当はこのPost部分もGASにやらせたかったけど、CORS対策されてて無理だった…)今週中に作る必要があった
なんでGAS使おうと思ったかというと、今週中にウェブサイトを作っておきたかったんですよね。
独立した直後に事業用の名刺を作ったんですが、
あと事業用の名刺作ってきた(
— ⛩branch@個人アプリ開発者⛩ (@br_branch) April 2, 2020・ω・´)<br>ホントは全然別の屋号を考えてたんだけど酔っ払いながら税務署の書類作っちゃったから屋号はBRBranchです(´・_・
) pic.twitter.com/8yIbDgKPqY上記の隠してる箇所に、まだ作ってもいない(ドメインしかとってない)サイトのURLを載せてました。
「まぁいつか作ればいいし」
って当初はぬるく考えていたのですが、今週早速名刺を渡す用事ができちゃって、これはまずいと…。
ただウェブサイトはポートフォリオにもなりうるんで、外っ面だけでもちゃんと作っておかなきゃなぁと思い、バックエンドを極力実装せずにデータの更新などもできるようにしたいと考えた時、「まぁもうGASでいいや」ってなりました。結果的にバックエンド側はreCAPTCHAの検証やSlackへの通知処理も含めて1時間ちょいで実装し終わったんで、かなり高速にやりたい機能作れたなと思ってます。
(フロント、というよりCSSが苦手なので、それに一番時間かけてた…)もうちょい詳しく紹介
そんなわけで、作った中でいくつか機能をピックアップして紹介します。
React + TypeScript
フロント側はこれで作りました。
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
CSSフレームワークはBULMAを使いました。
ぼくは言語の中ではCSSが一番苦手で、なんで苦手かというとエラー吐いてくれないし意図しない表示になった時にどこで影響受けてるのかすぐわからないしで、だからなんかアレなんです(´・ω・`)特にReactのCSSフレームワークを使うと、中で色んなクラスが知らず知らずのうちに呼ばれたりしてるんで、なんか思てたんとちゃうってなった時、カスタマイズがすごい面倒だし、結構たくさんコンポーネントがあって覚えること多くなるしで、なるべくシンプルなものを探してたどり着きました。
(といっても、BULMAはReactのフレームワークじゃないんですが)このフレームワークのいいところは、クラスの命名がとてもシンプルでわかりやすく、かつCSSオンリーな部分&カスタマイズしやすい部分ですね。コンポーネントも必要最低限のものだけがある感じなのもいい!
たとえば影付きのパネルを作りたければ以下で実装できます。
<div className="panel"> <div class="panel-block"> パネルになるよー </div> </div>また、Scssで作成されており、変数が定義されてるんでカスタマイズもやりやすいです。
もちろん シンプルな故の逆説的弊害 もあるようですが、3日程度で作れる規模のものを作るなら全然無問題でした。
Google Apps Script (Spread Sheet)
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してごにょごにょしようと思ってたんですが、以下のようなエラーが発生してできませんでした。
調査に時間あんまりかけたくなかったんでちゃんと調べてないので、本当はできるのかもしれませんが(´・ω・`)
とはいえ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の場合、あの鬱陶しい「信号を探せ」みたいなものも表示されず、利用者が何も操作しなくてもスパム対策をしてくれます。
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
というエラーが発生しちゃいます。
- https://www.google.com/recaptcha/api/siteverify ×
- https://recaptcha.google.com/recaptcha/api/siteverify ○
とはいえ、結局 Firebase Spark プランではぼくがやりたいことできなかったんですけどね(´・ω・`)
Googleサービス専用なんで、Slackに送れないじゃないやだー。。。
さすがGoogle、マネタイズの方法わかってらっしゃる。。。ちなみにSparkプランだとSlackに送ろうとする時にやはり
EAI_AGAIN
が発生します。
まぁ、 Blaze プランも無料枠あるんで、アップデートして使うことにしました。あと上に書き忘れてたけど、フロント側は Firebase Hostingに乗せてます。これもすんごいデプロイ簡単。
Firebase Hosting でWebサイトを公開する方法
https://qiita.com/gupuru/items/25a6722f6f802d3a5250Slack 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%A8Slack 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
があればロード中のスケルトンだって簡単につくれちゃうんですの。最後に
そんな感じで、小規模のサイトなら無料で簡単につくれるので良い時代になりましたねぇ。
- 投稿日:2020-04-07T20:16:40+09:00
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を使うことで美しいコードを書くことができそうだ。
エラーが起きたとき、なぜ起きたのかなぜ治ったのかをちゃんと理解することは重要だと改めて思う。ちゃんと調べることって大事だね。
- 投稿日:2020-04-07T20:05:38+09:00
React + Rails API + axios + react-router-domで複数のフォームを作成する
こんばんは!スージーです!
最近、学習し始めたReactにてフォームの扱い方で苦労したので備忘録。やりたい事
railsのcreateアクションにてフォームに2つのデータを入力してDBへ保存しビューで一覧表示する。
汚いレイアウトは勘弁して下さい。今回はCRUDの新規作成(Create)と一覧表示(Read)の実装をする。開発環境
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/166335Ruby on Rails+ReactでCRUDを実装してみた
https://qiita.com/yoshimo123/items/9aa8dae1d40d523d7e5dMacに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.rbclass 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.rbRails.application.routes.draw do resources :posts endRailsを起動
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にデータが入っていないのでここはこのままでOKRails側で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ディレクトリ
が存在します。それぞれプロジェクト作成した時に作成されてしまうからです。キャプチャは既に片方削除していますが、React(react_front内)の
.gitディレクトリ
は削除して下さい。それでエラーは解決できます。App.js
コードはこちらを参考にカラム名やパスを自分の環境に合わせて変更して利用させて頂きました。
App.jsimport 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.jsximport 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.jsximport 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)
を実装して開発中のアプリに載せ替えしようと思います。終わり
- 投稿日:2020-04-07T19:12:27+09:00
【React】クラスコンポーネントを関数コンポーネントに書き換え
はじめに
React Way 的にはクラスコンポーネントよりも関数コンポーネントのほうが望ましい。
そこでクラスコンポーネントで書かれたコードを関数コンポーネントに書き換えた。書き換え前
App.jsximport 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.jsximport 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.tsximport 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.tsximport 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;
- 投稿日:2020-04-07T19:12:27+09:00
ReactのProps
まず
りあクト! TypeScriptで始めるつらくないReact開発 第2版
がとっってもわかりやすい、もうすぐ第3版が出るそう親コンポーネントから子コンポーネントへpropsを渡す
はじめにJSで書いてみる
App.jsximport 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.jsximport 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.tsximport 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.tsximport 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;
- 投稿日:2020-04-07T19:12:27+09:00
【React】クラスコンポーネントから関数コンポーネントへ
はじめに
React Way 的にはクラスコンポーネントよりも関数コンポーネントのほうが望ましい。
そこでクラスコンポーネントで書かれたコードを関数コンポーネントに書き換えた。書き換え前
App.jsximport 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.jsximport 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.tsximport 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.tsximport 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;
- 投稿日:2020-04-07T17:24:23+09:00
【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;
- 投稿日:2020-04-07T15:51:14+09:00
カルーセル実装で気をつけたこと
前提条件
- React環境で動くこと
- カルーセルを入れることによりパフォーマンスが低下しないこと
実装
props
- 表示領域に表示するコンテンツ数
- keyExtractor
- カルーセルに表示するデータ(array)
- カルーセルに表示するコンポーネント(render props)
ライブラリの導入検討
- react-slick(https://react-slick.neostack.com/ )について導入検討したが、以下理由で断念。
- パフォーマンスは問題なし
- リンク数が奇数だった場合にうまく挙動しない
- 表示数が端数の場合うまく挙動しない
- 投稿日:2020-04-07T15:51:14+09:00
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/
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> )レンダリング時のみアニメーションを無効化する
requestAnimationFrameを使う
https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame
- requestAnimationFrameはlayoutとpaintの間で実行される
- 下記手順で再現
// アニメーション実行有無 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/ )について導入検討したが、以下理由で断念。
- パフォーマンスは問題なし
- リンク数が奇数だった場合にうまく挙動しない
- 表示数が端数の場合うまく挙動しない
- 投稿日:2020-04-07T15:20:25+09:00
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/
- 投稿日:2020-04-07T11:26:07+09:00
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のモックとはそもそも何ぞや?というところからわかっておらず現状おまじないと化してるので、後で調べておきたい
- 投稿日:2020-04-07T09:15:42+09:00
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: 内容}
)参考
- 投稿日:2020-04-07T07:40:04+09:00
react-scrollを実装してみた
最近の勉強で学んだ事を、ノート代わりにまとめていきます。
主に自分の学習の流れを振り返りで残す形なので色々、省いてます。
Webエンジニアの諸先輩方からアドバイスやご指摘を頂けたらありがたいです!smoothなスクロールを実装したい!
まずはreact-scrolをインストール
$ npm install react-scrollLinkタグを使用します!Linkの後にオプションをつけます。これはgithubにある公式のドキュメントからコピペできます。
to=には移遷したい先のIdを指定しましょう。smoothスクロールを実装するため、smoothはtrueにしておきます。offsetはpadding-topのようなもので、移遷する位置を指定できます。durationにはスクロールする秒数を指定しましょう。
⬇️Props/Optionsを見てください
react-scrollTopPage/index.jsimport 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なスクロールを実装してみた。