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

Reactにおける基本的なライフサイクルメソッド

hooksの登場により、classコンポーネントにしかなかった機能が使えるようになり、あらゆる場面で関数コンポーネントを使えるようになりました。

とはいえ、いきなりclassコンポーネントに関する事項を飛ばしてhooksの内容を学習するのはお勧めできません。classコンポーネントの学習から始め、classコンポーネントにおけるライフサイクルメソッドなどの概念を理解してからのほうがhooksの学習に入った際にスムーズに理解できると思います。1

そこで、ここではclassコンポーネントのライフサイクルメソッドについてまとめます。
(尚、ここではshouldComponentUpdateなどの使用されるケースが稀なものは除いています。)

ライフサイクルについて

コンポーネントには、画面上に表示され、再レンダリング(更新)を何度か経て、やがて表示されなくなるまでの流れがあります。この一連の流れがライフサイクルと呼ばれるもので、この流れは3つの期間に大別できます。

マウント時(Mounting)

コンポーネントが画面上に表示されるまでの期間にあたります。この期間では、constructor、render、componentDidMountの主に3つが呼ばれます。

constructor

最初に呼ばれます。後述のrenderメソッドと同様、画面にコンポーネントが表示される前に呼び出されます。stateの初期化などはここで行います。ただし、ここでsetStateメソッドを呼び出してはいけません。尚、データの読み込みもここで行うことは可能ですが、後述のcomponentDidMountで行うことが推奨されています。

render

renderメソッドは、唯一定義が必須となっています。このメソッドが呼ばれると、JSXの内容がDOMに追加され、コンポーネントが画面に表示されます。これが初回のレンダリングになります。

componentDidMount

初回レンダリング後に1度限りで呼ばれます。データの読み込みなどはこちらで行います。

更新時(Updating)

stateが更新されたり、親から新たなpropsを受け取ったりした場合に、この期間で再レンダリングされ、更新されます。以下のrender→componentDidUpdateというこの期間の流れは、コンポーネントが表示されなくなるまでの間、更新が必要になる度に繰り返されます。

render

マウント時に呼ばれたrenderメソッドですが、ここで再び呼ばれ、再レンダリングが行われます。

componentDidUpdate

コンポーネントがアンマウントされる(コンポーネントがDOMから削除され、画面に表示されなくなる)までの間、コンポーネントの再レンダリングが起こった(renderメソッドが呼ばれた)度に呼ばれます。

マウント解除時(Unmounting)

他のコンポーネントを表示するなどの理由により現在のコンポーネントが表示されなくなる(マウントが解除される)ことになったときがこの期間です。

componentWillUnmount

マウント解除時に1度限りで呼ばれます。ここでネットワークリクエストのキャンセルなどを行います。

その他のメソッド

以上の他にも、shouldComponentUpdate、getDerivedStateFromProps、getSnapshotBeforeUpdateといったものがあります。
しかし、これらは滅多に使われないものなので、初めは以上の5つのメソッドを理解できていれば良いと思います。


  1. この理由の1つに、hooksのuseEffectがあります。useEffectは、classコンポーネントのライフサイクルメソッドに相当する機能であり、ライフサイクルメソッドを理解していればuseEffectも理解しやすいです。私はReactを学習するのにUdemyの講座「 Modern React with Redux [2020 Update] 」( https://www.udemy.com/course/react-redux/ )を利用しました。この講座でも、先述の理由から、classコンポーネントの解説がhooksの解説に先行しています。正しい順序で効率的にReactを学習するのにお勧めです。 

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

Reactエラー備忘録2 : Error: Reducer "products" returned undefined during initialization.

概要

Reactアプリを開発する上で発生したエラーの原因と対策を忘れないためのメモです。

状況

某商品紹介用のWebアプリの商品追加画面制作中に発生。

エラー

> Error: Reducer "products" returned undefined during initialization. If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined. If you don't want to set a value for this reducer, you can use null instead of undefined.

コード

export const ProductsReducer = (state = initialState.products, action) => {
  switch (action.type) {
  }
}

原因

reducerのswitch文の中にdefaultのケースを設定していなかったこと

対策

switch文にdefaultのケースを追加することで解決

export const ProductsReducer = (state = initialState.products, action) => {
  switch (action.type) {
    default:
      return state
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React.js + sqlite3 + Material-ui でSPAを作るプロジェクトの準備(構築)

はじめに

限られたLAN内で動作するSPAを作りたかったので、サーバーをセットアップし、ReactでSPAを開発してみる。ちなみに筆者は初Reactだ。いつもならVue.jsで作ろうかって話になるんだけど、今はVue3が出たばかりでUIライブラリが整ってない。今後長く使うSPAになりそうだから、息の長そうな開発環境に手を出してみたのだ。

準備

今回は、Raspberry pi zero W をサーバーにする。Swapは大きくしておこう。

  参考: Raspberry Pi のスワップ領域拡張
  https://qiita.com/nyas/items/f4d0675061ee8cdcc3e7

/etc/dphys-swapfile の CONF_SWAPSIZE はデフォルトで100となっている。1024ぐらいにしておこう。それから、nodeのバージョンは、10か12の偶数のもので。eslint-typescriptがインストールできるように(create-react-appでReact環境構築について)。npm や yarn が動いたときに、インターネット上から関連するモジュールを取得するみたいなので、インターネットに接続されていることが前提。

sshでサーバーにログインし、任意のプロジェクトフォルダを作ってそこにアプリを構築。srcファイルを書き換えて作っていこう。プロジェクトではsqlite3を使ってデータ管理できるようにしよう。あと、UIはオーソドックスでもいいからきれいなやつを使いたいな・・・、ということで今回はMaterial-UIを使う。・・・というようなざっくりとした構想でスタート。

プロジェクトの構築

公式の説明を参照。「新しいシングルページアプリケーションを作成するのに最も良い方法です。」という Create React Appに従う。

# 任意のフォルダに移動(プロジェクトフォルダの親フォルダ)

$ npx create-react-app myapp

しばらく待つ。私の環境では20分ぐらいかかったが、マシンによってはあっという間なんだろうな・・・。

パッケージの追加

さて、プロジェクトフォルダが用意できたところで、似たようなことをすでに行っている先達の手順を参照、確認させていただく。何事も先達はあらまほしきことなり。今回の先輩はこちら。

  Raspberry Pi Zero WでNode.js+SQLite3を動かしてみた
  https://qiita.com/s-suefusa/items/6f4b1df1781c82999816

私の環境では、nodebrewのインストールは不要なのでそこは省略。あくまで参考。丸パクリではない。

とりあえず、プロジェクトフォルダに入る。

$ cd myapp

sqlite3

sqlite3を使えるようにしていこう。

$ npm install sqlite3 --save

-gオプションでグローバルにインストールするか、とも悩んだが、サーバーの故障や拡張のときにプロジェクトのバックアップからのリストアを行いやすいように、--saveでプロジェクトに含める形でセットアップすることにした。

ちなみに、Raspberry Pi OS lite だったため、sqlite3 をソースからビルドしてくれて、インストールに小一時間ほどかかる。

Material-ui

アプリ開発でUI構築はラクしたい~。ポピュラーらしい Material-UI を使ってみよう。

  ポピュラーなReact UIフレームワーク(Material-UI)
  https://material-ui.com/ja/

  material-uiを使ってかっこいいuiのreactアプリケーションを作ってみた
  https://blog.takanabe.tokyo/2015/12/material-uiを使ってかっこいいuiのreactアプリケーションを作ってみた/

  React入門 ~Material UI編~
  https://zenn.dev/h_yoshikawa0724/articles/2020-09-24-react-material-ui

$ yarn add @material-ui/core

これもまた小一時間ほどかかる。

Reactアプリのディレクトリルート/package.jsonに下記の一文を追加する。

忘れないうちにやっとこう・・・。参考情報は次の通り。

  create-react-appで作成したReactアプリをWebサーバにデプロイする手順
  https://qiita.com/matoruru/items/04dc2a325c317e9c50ae

開発環境

開発環境としては、このサーバー上のnodejs react プロジェクトを、リモートのVSCODEで開くとかしていじっていこうと思う。

おわりに

Raspberry pi zero W では、React + sqlite3 + Material-ui のプロジェクトを準備するだけで、3時間弱の時間がかかった。Raspberry pi zero W が、今回作るWEBアプリサーバーとして実働負荷に耐えうるかどうかテストもかねて進めているプロジェクトだが・・・。気長にやろう・・・。

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

Next.js+TypeScriptでマルチプロセス対応カスタムサーバ作成

Next.js+TypeScriptでマルチプロセス対応カスタムサーバ作成

カスタムサーバ

 Next.jsはWebServer機能を標準で内蔵していますが、マルチプロセスや特殊なセッション処理などを組み込む場合には、カスタムサーバという形でWebServer部分を自分で実装する必要があります。

 公式にサンプルはある物の、以外に日本語の情報が少ない、それどころかマルチプロセスやfastifyでの実装記事は皆無だったので、書いていきたいと思います。

マルチプロセス化について

 Next.jsを動かしているNode.jsは基本的にシングルスレッドで動作します。シングルスレッドといってもI/Oアクセスに関しては非同期で行われているため、無駄なブロックは起こらず、実用的な速度で動作することが可能です。

 ところが計算処理などをしている間は当然他の仕事は出来ません。マルチコアCPUなどでハードウエア的に余裕があっても、シングルスレッドである限りはせっかくのリソースが活用できないのです。

 これに対処するにはNext.jsをマルチスレッドではなく、マルチプロセス化するのが有効な手段となります。ありがたいことにNode.jsには、マルチプロセス化を簡単に実装するライブラリが標準提供されているので、カスタムサーバ化のコードを少し書くだけで、その恩恵を受けることが出来ます。

Fastifyに関して

 Node.jsでWebServer機能を実装するフレームワークとして有名なのはExpressです。しかし古い実装を引きずっているため、応答速度が遅いといわれています。今回はベンチマークで上位に位置するFastifyを使ってカスタムサーバを作ります。

インストールが必要な最低限のパッケージ

yarn add cross-env fastify next react react-dom
yarn add -D @types/node @types/react @types/react-dom ts-node-dev typescript

カスタムサーバの実装コード

 以下の二つのファイルを用意します。
 ちなみに環境変数でINDEXというのを子プロセスに渡していますが、workerにIDが振られるので実は無くてもかまいません

server/index.ts
import next from "next";
import * as os from "os";
import * as cluster from "cluster";
import { parse } from "url";
import fastify from "fastify";

const dev = process.env.NODE_ENV !== "production";
const clusterSize = Math.min(os.cpus().length, 4);
const portNumber = 3000;

if (cluster.isMaster) {
  for (let i = 0; i < clusterSize; i++) cluster.fork({ INDEX: i });
} else {
  const app = next({ dev });
  const handle = app.getRequestHandler();
  const server = fastify();
  app.prepare().then(() => {
    server.all("*", (req, res) => {
      return handle(req.raw, res.raw, parse(req.url, true));
    });
    server.listen(portNumber).then(() => {
      console.log(`[${process.env.INDEX}]:http://localhost:${portNumber}`);
    });
  });
}
server/tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "outDir": "../.next",
    "esModuleInterop": true
  }
} 

 tsconfig.jsonを作成しているのは、Next.js管理下のpagesファイルなどとはTypeScriptのビルドの扱いが異なるからです。

スクリプト関係

 devはカスタムサーバ自体の自動リロードのため、ts-node-devを使っています。ただし.nextの中身はNext.js側が調整するので、無視指定が必要です。

package.json
{
  "name": "nextjs-custom",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "dev": "ts-node-dev --ignore-watch \\.next -P server/tsconfig.json server/index.ts",
    "build": "tsc -b server && next build",
    "start": "cross-env NODE_ENV=production node .next/index.js",
    "export": "next export"
  },
  "devDependencies": {
    "@types/node": "^14.14.22",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "ts-node-dev": "^1.1.1",
    "typescript": "^4.1.3"
  },
  "dependencies": {
    "cross-env": "^7.0.3",
    "fastify": "^3.11.0",
    "next": "^10.0.5",
    "react": "^17.0.1",
    "react-dom": "^17.0.1"
  }
}

まとめ

 大したコード量も必要なくカスタムサーバが実装できました。マルチプロセスとFastifyのパワーによって、きっと快適SSRライフが送れることでしょう。

 ただしベンチマークを取った結果、それなりの負荷をかけても効果が顕著に出るのは2プロセスまでというオチでした。ベンチマークに関しては別記事を書く予定です。

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

【React Native】stateを用いた変数管理

更新した値を画面に即時反映させたい

React Nativeにてインタラクティブなアプリを作る際、基本的な処理の流れとしては

  • ボタン等を押してメソッドを実行
  • メソッド内で計算、計算後の値を算出(場合によっては結果をDBに反映)
  • 画面に反映

が一般的な流れである。
stateを理解してこの一連の流れが出来るようになったらアプリ作りが捗ったので紹介。

カウンターアプリの例

上記アプリの例として、カウンターアプリが上げられる。
スクリーンショット 2021-01-27 8.46.40.png

画面要素

  • 現在のカウント
  • カウントアップボタン
  • カウントダウンボタン

仕様

「カウントダウンボタン」または「カウントアップボタン」を押したら、現在のカウントが増減

実装的な処理詳細

  • 「カウントダウンボタン」または「カウントアップボタン」を押した際、現在のカウントをインクリメントする
  • インクリメントした後の数字が現在のカウントに反映される

クラス内の変数を更新しただけでは、画面は再描画されない

class無いに変数を用意して数字を更新したとしても、画面の数字は更新されない。

<View style={styles.container}>
      <TouchableOpacity style={styles.button} onPress={() => 
        {
          counter++
          console.log("after count:" + counter)
        }
      }>
        <Text style={styles.buttonText}>+1</Text>
      </TouchableOpacity>
      <TouchableOpacity style={styles.button} onPress={() => 
        {
          counter--
          console.log("after count:" + counter)
        }
      }>
        <Text style={styles.buttonText}>-1</Text>
      </TouchableOpacity>
      <Text style={styles.paragraph}>
       current count:{counter}
      </Text>
    </View>

ブラウザで実際に試してみる

リンクに動く(数字は動かない)React Nativeのサンプルをおいておきました。
https://snack.expo.io/3n9E_BhFU

変数を更新するだけは再描画されない

描画を更新するには、状態変化をReactに通知して、renderを再度呼んでもらう必要がある。

stateによる状態管理、変更

stateとは、値の変更が常時監視され、値が変わったら画面が再描画される変数である。

stateの定義

stateはuseStateを用いて初期化し、以下のように宣言できる。
useStateの引数は変数の初期値である。

const [counter, setCounter] = useState(0)

counterは数を保持する変数、setCounterはcounterを更新するためのsetterである。

stateの勘所は、setterを用いて値を更新することでstateの変更を通知し、画面を再描画してもらうことである。

setter経由で更新した場合、Reactで更新を検知し

値の更新

counterを直接更新するのではなく、setCounterを用いて更新を行う。

setCounter(counter + 1)

引数に更新後の値を渡すことで値が引数の値で更新される。
今の値に対して差分更新する場合は、上記例のように現在の値と差分で計算すれば良い。

ブラウザで実際に試してみる

リンクにsateを用いて正常に動くReact Nativeのサンプルをおいておきました。
https://snack.expo.io/NdXVNsFnA

参考:学習に用いたUdemyのコース

react nativeの経験が無い中でアプリを作り始めて、こちらのコースで学習しながら進めている。
今回はstateを学んだのでその紹介。

The Complete React Native + Hooks Course [2020 Edition]
https://www.udemy.com/course/the-complete-react-native-and-redux-course/

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

eslintのススメ

まとめ

eslintとは

  • 「どっちでも書ける記述方法のうちこっちにして」と決めたルールセットとそれに違反しているかどうかを自動検出(リント)・訂正(フォーマット)するツール。
  • つまり、いわゆる静的解析ツール:リンター。そこから実際にファイルを正しく書き換えてしまうフォーマッターも兼ねられる。

eslintの良いところ

  • 人によってバラバラになってしまいがちな記述方式に一定の統一感を出せる。コードを読む速度が上がる。
  • どっちでもいい時に迷わなくて済む。時短。
  • 変更合戦になりにくくなる。これも時短。
  • ルールの追加などをプラグイン方式で行うので、チームで好きにルールの厳しさを決められる。
  • プラグインはルールセットなんだけど、その中でも個別にオンオフできるし、全部オンとか、react/recommended などおすすめルールセットもある。
  • みんな使ってる。(情報少ないとかでは多分困らない。)

導入方法

yarn add -D eslint

そして packege.jsonscripts のなかに

packege.json
"scripts":{
    ...
    "lint": "eslint --fix --ext .jsx,.js,.tsx,.ts .",
    ...
},...

という感じの行を挟むとその後

yarn lint

を実行するだけで、eslintが起動するようになる。
--fix は勝手に直しちゃうオプションなのでなしがいい場面もあるかも
--ext で範疇とする拡張子を選ぶ
最後の . は現在のディレクトリ内を探して、という意味。

細かい設定

無視したいファイル(build/などは無視すべき.node_modulesだけはデフォルトで無視される)はルートに置いた.eslintignoreに記述しておくと無視してくれるが、ファイル増えるのが嫌な人は.eslint.js内の"ignorePatterns":に書いてもOK
その他の設定は .eslint.js に記述する。(これがチームごとの秘伝のタレ的になりがち)
おすすめとしては、取りあえす有名なプラグインいくつかいれて、hogehoge/recommendみたいなrecommendのルールセットだけオンにして、個別ルールのオンオフはまずは触らない。
それで進めていってエラーが出た場合、このルール逆の方がいいなとか、無視したいなとなった時に個別にいじる。

つまり、設定で主に触るべきは3箇所

  • "plugins":どのプラグインを導入するか?(これをしただけではルールはオンにならない)
  • "extends":どのルールセットを導入するか?(プラグイン入れなくても最初からeslint内にあるルールセットもあるよ。)
  • "rules":個別対応のルール

(僕個人はちょっとハードコアでいっぱいプラグインいれてhogehoge:allみたいな全部オンでどうしても無理なのをオフにするような形で開発してるけど、これをする理由は、こういう記述方式も世の中にあってそれを理想と考える人もいるのねと無理矢理教えてもらえるので学びになるから。チームでやるのはしんどすぎるように思う。)

プラグイン紹介

"@typescript-eslint/eslint-plugin" TypeScript専用のセット おすすめ度5
"@typescript-eslint/parser" TypeScript対応するのに絶対必要なやつ おすすめ度5
"eslint-config-prettier" prettierと競合するルールをオフにするやつ おすすめ度5
"eslint-plugin-ava" よく知らないけど、いっぱい盛りセット。嫌いじゃないけどあんまり使われてない。おすすめ度2
"eslint-plugin-eslint-comments" コメントの書き方セット。別にいらん?おすすめ度2
"eslint-plugin-import" import時のルールおすすめ度3
"eslint-plugin-jsx-a11y" アクセシビリティ守ろうセット。おすすめ度3
"eslint-plugin-react" reactのやつ。おすすめ度5
"eslint-plugin-react-hooks" react-hooksのやつ。おすすめ度4
"eslint-plugin-simple-import-sort" インポート・エクスポート順だけ。"eslint-plugin-import"はエクスポートないからそのためだけに仕方なく。おすすめ度2
"eslint-plugin-sonarjs" sonarqubeっていう言語によらない汎用リンターがあって、そこのルール。おすすめ度3
"eslint-plugin-unicorn" よく知らないけど、いっぱい盛りセット。若干癖つよ感。おすすめ度2
"eslint-plugin-prettier" prettierをeslintから起動するのではなく、別にprettierコマンドを打つ派閥が存在する。おすすめ度3

たまに無効化したい場合

1行だけ無効化したい場合、エラーが出た行の1行上にコメントで

// eslint-disable-next-line camelcase

JSXElement内だと

{/* eslint-disable-next-line jsx-a11y/anchor-has-content */}

数行分だと

/* eslint-disable camelcase */
interface ArticleRecord {
  article_id: number
  published_at: Date
  title: string
}
/* eslint-enable camelcase */

これでファイル全体を囲むと、当然ファイル全体で無効化できる。最後のeslint-enableは無くても良いけど、あった方がいいかも。

設定例

TypeScript/Reactの場合、定番の楽な設定はこんな感じ。これベースにお好みでプラグイン追加したりすると良いように思う。

yarn add -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-plugin-react eslint-plugin-react-hooks
eslint.js
{
  "env": {
    "browser": true,
    "node": true,
    "es2020": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react-hooks/recommended",
    "plugin:react/recommended",
  ],
  "globals": {
    "Atomics": "readonly",
    "SharedArrayBuffer": "readonly",
    "React": "writable"
  },
  "ignorePatterns": [
    "build",
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaFeatures": { "jsx": true },
    "ecmaVersion": 2020,
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "plugins": [
    "@typescript-eslint",
    "react",
  ],
  "rules": {
      // ここに個別対応したいものを書く
  },
  "settings": { "react": { "version": "detect" } }
}

[クリックで開く] おまけ(ハードコアの実際使ってるやつ)

特に "@typescript-eslint/prefer-readonly-parameter-types"
こいつが強敵。ある程度意識して書いてるけど、今回はオフにしている。
学びのためなんで、eslint-config-prettierも使わずに設定している。

eslint.js
{
  "env": {
    "browser": true,
    "node": true,
    "es2020": true
  },
  "extends": [
    "eslint:all",
    "plugin:@typescript-eslint/all",
    "plugin:ava/recommended",
    "plugin:eslint-comments/recommended",
    "plugin:import/errors",
    "plugin:import/react",
    "plugin:import/typescript",
    "plugin:import/warnings",
    "plugin:jsx-a11y/strict",
    "plugin:react-hooks/recommended",
    "plugin:react/all",
    "plugin:sonarjs/recommended",
    "plugin:unicorn/recommended"
  ],
  "globals": {
    "Atomics": "readonly",
    "SharedArrayBuffer": "readonly",
    "React": "writable"
  },
  "ignorePatterns": [
    "__memo__",
    "packages/api/dist",
    "packages/app/public/*.js",
    "packages/shared/dist",
    "templates"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaFeatures": { "jsx": true },
    "ecmaVersion": 2020,
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "plugins": [
    "@typescript-eslint",
    "ava",
    "eslint-comments",
    "import",
    "jsx-a11y",
    "react",
    "simple-import-sort",
    "sonarjs",
    "unicorn"
  ],
  "rules": {
    "jsx-a11y/anchor-is-valid": "off",
    "jsx-a11y/label-has-for": "off",
    "jsx-a11y/no-onchange": "off",

    "unicorn/no-nested-ternary": "off",
    "unicorn/no-null": "off",
    "unicorn/no-useless-undefined": "off",
    "unicorn/prevent-abbreviations": "off",

    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/indent": "off",
    "@typescript-eslint/no-magic-numbers": "off",

    "react/forbid-component-props": "off",
    "react/function-component-definition": "off",
    "react/jsx-child-element-spacing": "off",
    "react/jsx-curly-newline": "off",
    "react/jsx-handler-names": "off",
    "react/jsx-max-props-per-line": "off",
    "react/jsx-newline": "off",
    "react/jsx-no-bind": "off",
    "react/jsx-no-literals": "off",
    "react/jsx-one-expression-per-line": "off",

    "array-bracket-newline": "off",
    "array-element-newline": "off",
    "capitalized-comments": "off",
    "function-call-argument-newline": "off",
    "function-paren-newline": "off",
    "id-length": "off",
    "implicit-arrow-linebreak": "off",
    "line-comment-position": "off",
    "lines-around-comment": "off",
    "max-len": "off",
    "max-lines-per-function": "off",
    "max-statements": "off",
    "multiline-ternary": "off",
    "newline-per-chained-call": "off",
    "no-confusing-arrow": "off",
    "no-continue": "off",
    "no-inline-comments": "off",
    "no-mixed-operators": "off",
    "no-ternary": "off",
    "no-undef-init": "off",
    "no-undefined": "off",
    "no-underscore-dangle": "off",
    "sort-imports": "off",
    "sort-keys": "off",
    "wrap-regex": "off",

    "simple-import-sort/imports": "warn",
    "simple-import-sort/exports": "warn",

    // "unicorn/custom-error-definition": "warn",
    "unicorn/no-keyword-prefix": "warn",
    "unicorn/no-unsafe-regex": "warn",
    "unicorn/no-unused-properties": "warn",
    "unicorn/numeric-separators-style": "warn",
    "unicorn/string-content": "warn",

    "eslint-comments/no-restricted-disable": "warn",
    "eslint-comments/no-unused-disable": "warn",
    // "eslint-comments/no-use": "warn",
    // "eslint-comments/require-description": "warn",

    "ava/no-cb-test": "warn",
    "ava/prefer-power-assert": "warn",
    "ava/test-title-format": "warn",

    "import/no-restricted-paths": "warn",
    "import/no-absolute-path": "warn",
    "import/no-dynamic-require": "warn",
    // "import/no-internal-modules": "warn",
    "import/no-webpack-loader-syntax": "warn",
    "import/no-self-import": "warn",
    "import/no-cycle": "warn",
    "import/no-useless-path-segments": "warn",
    // "import/no-relative-parent-imports": "warn",

    "import/export": "warn",
    "import/no-extraneous-dependencies": "warn",
    "import/no-mutable-exports": "warn",
    "import/no-unused-modules": "warn",

    // "import/unambiguous": "warn",
    "import/no-commonjs": "warn",
    "import/no-amd": "warn",
    "import/no-nodejs-modules": "warn",

    "import/first": "warn",
    "import/exports-last": "warn",
    "import/no-duplicates": "warn",
    "import/no-namespace": "warn",
    "import/extensions": "warn",
    // "import/order": "warn",
    "import/newline-after-import": "warn",
    // "import/prefer-default-export": "warn",
    // "import/max-dependencies": "warn",
    "import/no-unassigned-import": "warn",
    "import/no-named-default": "warn",
    // "import/no-default-export": "warn",
    // "import/no-named-export": "warn",
    "import/no-anonymous-default-export": "warn",
    "import/group-exports": "warn",
    "import/dynamic-import-chunkname": "warn",

    "unicorn/filename-case": [
      "warn",
      { "cases": { "camelCase": true, "pascalCase": true, "kebabCase": true } }
    ],

    "@typescript-eslint/comma-dangle": [
      "warn",
      {
        "arrays": "always-multiline",
        "objects": "always-multiline",
        "imports": "always-multiline",
        "exports": "always-multiline"
      }
    ],
    "@typescript-eslint/member-delimiter-style": [
      "warn",
      {
        "multiline": { "delimiter": "none", "requireLast": false },
        "singleline": { "requireLast": false }
      }
    ],
    "@typescript-eslint/naming-convention": [
      "warn",
      {
        "selector": "default",
        "format": ["strictCamelCase", "StrictPascalCase"]
      },
      {
        "selector": "variable",
        "format": ["strictCamelCase", "StrictPascalCase", "UPPER_CASE"],
        "trailingUnderscore": "allow"
      },
      {
        "selector": "function",
        "format": ["strictCamelCase", "StrictPascalCase"]
      },
      {
        "selector": "parameter",
        "format": ["strictCamelCase"],
        "leadingUnderscore": "allow"
      },
      {
        "selector": "property",
        "format": ["strictCamelCase", "StrictPascalCase"]
      },
      {
        "selector": "parameterProperty",
        "format": ["strictCamelCase"]
      },
      {
        "selector": "method",
        "format": ["strictCamelCase"]
      },
      {
        "selector": "accessor",
        "format": ["strictCamelCase"]
      },
      {
        "selector": "enumMember",
        "format": ["strictCamelCase"]
      },
      {
        "selector": "class",
        "format": ["StrictPascalCase"]
      },
      {
        "selector": "interface",
        "format": ["StrictPascalCase"]
      },
      {
        "selector": "enum",
        "format": ["strictCamelCase"]
      },
      {
        "selector": "typeAlias",
        "format": ["StrictPascalCase"]
      },
      {
        "selector": "typeParameter",
        "format": ["StrictPascalCase"]
      }
    ],
    "@typescript-eslint/no-extra-parens": ["warn", "functions"],
    "@typescript-eslint/no-type-alias": [
      "warn",
      {
        "allowAliases": "always",
        "allowCallbacks": "always",
        "allowConditionalTypes": "always",
        "allowMappedTypes": "always"
      }
    ],
    // "@typescript-eslint/prefer-readonly-parameter-types": [
    //   "warn",
    //   { "ignoreInferredTypes": true }
    // ],
    "@typescript-eslint/prefer-readonly-parameter-types": "off",
    "@typescript-eslint/quotes": "off",
    "@typescript-eslint/object-curly-spacing": ["warn", "always"],
    "@typescript-eslint/semi": ["warn", "never"],
    "@typescript-eslint/space-before-function-paren": [
      "warn",
      { "named": "never" }
    ],

    "react/jsx-filename-extension": [2, { "extensions": [".tsx"] }],
    "react/jsx-indent": [
      "warn",
      2,
      { "checkAttributes": true, "indentLogicalExpressions": true }
    ],
    "react/jsx-indent-props": ["warn", 2],
    "react/jsx-max-depth": ["warn", { "max": 4 }],
    "react/jsx-props-no-spreading": [
      "warn",
      { "custom": "ignore", "explicitSpread": "ignore" }
    ],
    "react/no-multi-comp": ["warn", { "ignoreStateless": true }],

    "dot-location": ["warn", "property"],
    "func-style": ["warn", "declaration", { "allowArrowFunctions": true }],
    "max-classes-per-file": ["warn", 2],
    "no-console": ["warn", { "allow": ["warn", "error"] }],
    "no-void": ["warn", { "allowAsStatement": true }],
    "object-property-newline": [
      "warn",
      { "allowAllPropertiesOnSameLine": true }
    ],
    "one-var": ["warn", "never"],
    "padded-blocks": ["warn", "never"],
    "quote-props": ["warn", "as-needed"],

    "sonarjs/no-duplicate-string": ["warn", 4]
  },
  "settings": { "react": { "version": "detect" } }
}


https://github.com/EvgenyOrekhov/eslint-config-hardcore#readme
こんなのもあるよ。やらんけど。

関連した話題

prettier

前述の通り「prettierはフォーマッターであり、リンターではないぞ派閥」がいるため、そういう時はこう

package.json
"scripts":{
  ...
  "format":"prettier  \"**/*\" --write --ignore-unknown",
  ...
}

つまりprettierは別に yarn add -D prettier でいれて別スクリプトにして yarn format コマンド叩く。
この派閥の利点はeslintの適用範囲を超えてprettierを適用するという設定値が自然なところ。例えばhtmlファイルとか、cssファイルとかはeslintに入れずに、prettierにはかけるとか。

「フォーマッターもリンターも一緒でええ」派閥はeslint-plugin-prettierを入れるといい。
(prettierはvscodeにプラグインとして入れてて保存時に自動で常にやってくれるようにしてるから、わざわざscriptsにしない派閥もあるらしいけど、それするにしても、scripts化しとくと何かと自動化できたりして良いですよ)
(あとeslint-config-prettierって名前似てて何って感じだが、これは競合するルールをオフにするやつなので、どちらの派閥も入れた方が良い)

また、prettierは完全にno config派閥と package.jsonなどに

package.json
  "prettier": {
    "semi": false,
    "singleQuote": true
  },

とか書いて、この二つだけは設定する派閥がある。僕はvercel信者なので、後者。

husky, lint-staged

yarn add -D husky lint-staged
して、から以下のように package.json に書くとコミット時に毎回lint-staged内のコマンドが走って、git add で追加した、つまりstage内のファイルのみが捜査される。失敗するとコミットできなくなる。ハスキー犬のようにやかましい。(どのチームでも入れると良いとは思わない)
huskyはgitコマンドに連動して自動で走らせるやつで、lint-stagedはstage内だけ走らせるってやつ。

package.json
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "**/*": "yarn format",
    "*.{js,ts,tsx}": "yarn lint"
  },

あと

あんまり関係ないけど他に入れてるツール紹介

npm-check-updates

yarn add -D npm-check-updates

バージョン更新を自動で確認してくれる。 インストールせずにnpx npm-check-update とか yarn dlx npm-check-update (yarn v2のみのやつ)で利用しても良いツールかも。安易なバージョンアップはすぐ問題を起こすので要注意。

sort-package-json

yarn add -D sort-package-json

package.jsonを綺麗にしてくれるやつ。以下のようにしている。

"scripts":{...
  "format": "sort-package-json package.json && prettier \"**/*\" --write --ignore-unknown",...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jQueryを知っているだけの怠惰なひとが1時間以内にVue, React, Angularを完全に理解する (コピペするだけのサンプル付)

動機とこのページの趣旨

jQuery ...もう14年モノらしい。

業務で基本的にjQueryを10年ほど利用してきたが、スキルマップ作成とやらでVue, React, Angular の経験を問われたため、知ったかぶりしたいので調べた各種資料。以下を順に読んで index.html を3ファイル作るだけで1時間以内にVue, React, Angular「完全に理解した」顔をしよう。タイトルの「怠惰」はプログラマの美徳なのでお飾りフレーズとして書いてしまったが、コピペという怠惰はプログラム的には怠惰では無い、むしろ闇なので、これで一通りかじったらむしろナニモワカラナイの絶望の谷に堕ちてみることをお勧めする。そんな趣旨。

時代は「Angular」「React」「Vue」の3大フレームワークに集約 らしい...

Hello Vue world のために以下を読め

さて時間がないので早速始めよう!
Vue.jsでできること8選。凄さが分かる実用例スニペット集
凄い!けれどその凄さを実感している場合ではない。
Vue.jsで Hello World!

ひとまず元気に挨拶、Hello world

HTML表示:
image.png

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    <title>Title</title>
</head>
<body>
<div id="app">
    <!-- testValの内容を表示する -->
    {{ testVal }}
</div>
</body>
<script>
    const app = new Vue({
        el: '#app',
        data: {
            testVal: 'Hello World!'
        }
    });
</script>
</html>

早速動かせた実感を得たら以下。
Vue.jsで湯婆婆を実装してみる
公式(日本語)
Vue.js は公式を読めと随所に書いてあるので、公式サイトを読むのが最も手っ取り早く完全に理解できる。

Hello React World のために以下を読め

次、React。こちらはまず。
React (JavaScript) で湯婆婆を実装してみる

元気に湯婆婆

令和のHelloworld湯婆婆。
HTML表示:
image.png

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>React 湯婆婆</title>
  <script src="https://unpkg.com/react/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/babel-standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
    (() => {
      'use strict';

      const {useState} = React;
      function Yubaba() {
        const [name, setName] = useState('');
        const newName = name.substr(Math.floor(Math.random() * name.length), 1);

        return (
          <div>
            <p>契約書だよそこに名前を書きな</p>
            <input type='text' value={name} onChange={e => setName(e.target.value)}/>
            <p>フン{name}というのかい贅沢な名だねぇ</p>
            <p>今からお前の名前は{newName}いいかい{newName}だよ分かったら返事をするんだ{newName}!!</p>
          </div>
        );
      }

      ReactDOM.render(
        <Yubaba/>,
        document.getElementById('root')
      );
    })();
  </script>
</body>
</html>

ReactでHello Worldをする前に...
Facebook公式のcreate-react-appコマンドを使ってReact.jsアプリを爆速で作成する
以上で一通りセットアップはできる。

ReactでHelloWorldをしてみよう!
公式(日本語)
をサラリと読もう。

Hello Angular World のために以下を読め

ここまでで30分経っただろうか。もう少しだ。
AngularJSでHello World
AngularJS で Hello World

元気にHello world! (3回め)

HTML表示:
image.png

<!DOCTYPE html>
<html ng-app>
  <head>
    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js"></script>
    <script>
      var HelloWorld = {
        Controller: function($scope) {
          $scope.name = 'World';
          $scope.greeting = 'Hello';
          $scope.bye = function() {
            $scope.greeting = 'Good-bye';
          };
        }
      };
    </script>
    <link rel="stylesheet" href="css/main.css">
    <title>Hello World</title>
  </head>
  <body>
    <h1>AngularJS Example: Hello World</h1>
    <div ng-controller="HelloWorld.Controller">
      Input your name →
      <input type="text" ng-model="name" size="20">
      <hr>
      <p>{{greeting}} {{name}}!</p>
      <hr>
      <p><button ng-click="bye()">Bye!</button></p>
    </div>
  </body>
</html>

もう湯婆婆なのか何なのかわからなくなっているが、以下。
とほほのAngular入門
公式(日本語)
とほほの解説はとても心強い。

で、jQueryとどう違うの?

3種類動かしてみたところでウンチクくらいは語れるようにしておこう。以下で完璧だ。
JavaScriptが辿った変遷
jQuery愛好家のためのVue.js、React入門(いずれAngularも)
jQuery から Vue.js へのステップアップ

実際に実務でjQueryを置き換えられるかなんて言う顔をするなら以下。
Vue.jsとjQueryで同じ機能を作成し、コードを比較する
Vue.jsとjQueryで同じ機能を作成し、コードを比較する - その2
ReactとjQueryの比較
JavaScript: フレームワーク React/Vue/Angularについて

これで1時間以内かな?

最後に我らのjQuery

ここまでjQueryだけ知っているひとを想定読者にしたので、最後にウッカリこの記事を開いてしまった人のために、念の為jQueryについてもHello worldしておく。逆の境遇においてもこれでjQueryを完全に理解してほしい。

jQueryの基礎
頼まれてもいないのにcssのお餅付きだ。迎春なので鏡餅をCSSで作った参照。
jQuery公式については
https://jquery.com/ と、
http://semooh.jp/jquery/ がテッパンか。

HTML表示:
image.png

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Progate</title>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
  <style type="text/css">
.kagamimochi {
  width: 100px;
  text-align: center;
}
.mikan {
  width: 50px;
  height: 40px;
  background: #e88522;
  border-radius: 50%;
  position: relative;
  z-index: 10;
  display: inline-block;
}
.mikan::after {
  content: "";
  width: 15px;
  height: 5px;
  background: linear-gradient(#4f9c5d 50%, #6cb576 50%);
  border-radius: 50%;
  display: inline-block;
  position: absolute;
  top: 0;
  right: 10px;
  transform: rotate(-20deg);
  display: inline-block;
}
.mochi1 {
  width: 80px;
  height: 40px;
  background: #fff;
  border: 1px solid #000;
  border-radius: 50%;
  display: inline-block;
  margin-top: -15px;
  position: relative;
  z-index: 5;
}
.mochi2 {
  width: 100px;
  height: 50px;
  background: #fff;
  border: 1px solid #000;
  border-radius: 50%;
  display: inline-block;
  margin-top: -20px;
  position: relative;
  z-index: 4;
}
.kami {
  width: 92px;
  height: 92px;
  background: #fff;
  border: 5px solid #f00;
  transform: rotateX(45deg) rotateZ(45deg);
  display: inline-block;
  margin-top: -75px;
  position: relative;
  z-index: 1;
}

</style>
<script>
  $(function(){
    $('#hide-text').click(function(){
      $('#text').slideUp();
    });  
  });
</script>   
</head>
<body>
  <!-- このボタンを押すと -->
  <div class="btn" id="hide-text">説明を隠す</div>

<div class="kagamimochi">
  <div class="mikan"></div>
  <div class="mochi1"></div>
  <div class="mochi2"></div>
  <div class="kami"></div>
</div>

  <!-- この表示が隠れる -->
  <h1 id="text">Hello, World!</h1>
  <script src="script.js"></script>
</body>
</html>

これでどんどん具体的に、ナニモワカラナイを目指せそうです。すべてのJSはここからだ。Enjoy!
以上お粗末様でした。

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

classNameを上書きできるReactコンポーネントを作る方法

classNameにクラス名を割り当ててスタイリングしたコンポーネントを実装すると、
単純に実装しただけではコンポーネントの呼び出し時にclassName属性を新たに追加(注)した際に元々のスタイリングが全て消えてしまう。

注: これをやるには元々のHTMLタグが持つpropsを受け継がせる必要がある

解決策は、classNameの指定部分にクラス名を挿入可能にすればよい。

App.css
.text-red {
  color: red;
}

.text-bold {
  font-weight: bold;
}

.text-big {
  font-size: 24px;
}
App.tsx
import "./App.css";
import classnames from "classnames";

type RedTextProps = { children: React.ReactNode; className?: string };

function RedText({ children, className }: RedTextProps) {
  return <p className={classnames("text-red", className)}>{children}</p>;
}

function App() {
  return (
    <div>
      {/* 赤色 */}
      <RedText>Lorem ipsum dolor sit amet.</RedText>
      {/* 赤色、太字 */}
      <RedText className="text-bold">Lorem ipsum dolor sit amet.</RedText>
      {/* 赤色、太字、サイズ大 */}
      <RedText className={classnames("text-bold", "text-big")}>Lorem ipsum dolor sit amet.</RedText>
    </div>
  );
}

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

NextJs 触ってみた

NextJsを触ってみた。

今更触ってみました。
かなりいいと思います。私は、Reactとかで作成してきましたがページの追加とかファイル作成するだけなのでらくです。

準備

導入部分ですが、いろいろな方が他に記事を書いているのでそちらの方を見た方がいいかもしれません。

reactなどで作成してきたと思いますが、package.jsonを作成しましょう

作成したら下記のコードを実行

npm install react react-dom next

reactとNextJsの関係を追加します。

npm install react react-dom next

package.jsonに下記を追記してください

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

ここまで実行することができます。
※ページ情報がないので404が表示されます。
404.png

ページの追加

typeScriptで記載していこうと考えているので下記のコマンドを実行してください

npm install -D typescript @types/react @types/react-dom @types/node
 // tsconfig.json を作成しましょう。

内容は、こんな感じです。(srcディレクトリで作成していきます。)

{
    "compilerOptions": {
      "sourceMap": true,
      "noImplicitAny": true,
      "module": "esnext",
      "target": "es6",
      "jsx": "preserve",
      "lib": ["dom", "dom.iterable", "esnext"],
      "allowJs": true,
      "skipLibCheck": true,
      "strict": false,
      "forceConsistentCasingInFileNames": true,
      "noEmit": true,
      "esModuleInterop": true,
      "resolveJsonModule": true,
      "isolatedModules": true,
      "moduleResolution": "node"
    },
    "include": ["src/**/*"], 
    "exclude": ["node_modules"]
  }

では、下記のようにフォルダー、ファイルを作成していきましょう

src
 └ pages
     └index.tsx
indextsx
import React from "react";

export default function TopPage() {
  return (
    <div>
   hello world
    </div>
  );
}

これで、ページを追加することができました。

redux

ここまでは、ありふれている内容です。
react といえば状態管理で redux が定番だと思うので導入してみましょう。

ファイル構造は、こんな感じです。

src
 └ store
     ├ store.ts
     └ counter
        ├ slice.ts
        └ selector.ts

sliceを実装しましょう。reducerとactionを同時に行ってくれます。

slice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

export type CounterState = {
  count: number;
};
export const initialState: CounterState = {
  count: 0,
};

const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    incrementCounter: (state, action: PayloadAction<number>) => ({
      ...state,
      count: state.count + action.payload
    }),

    decrementCounter: (state, action: PayloadAction<number>) => ({
      ...state,
      count: state.count - action.payload,
    }),
  },
});

export default counterSlice;

次にselector.ts

import { useSelector } from 'react-redux';
import { CounterState } from './slice';

export const useCounterState = () => {
  return useSelector((state: { counter: CounterState }) => state);
};
store.ts
import { Store, combineReducers } from "redux";
import logger from "redux-logger";
import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
import counterSlice, { initialState as counterState } from "./counter/slice";

const rootReducer = combineReducers({
  counter: counterSlice.reducer,
});

const preloadedState = () => {
  return { counter: counterState };
};

export type StoreState = ReturnType<typeof preloadedState>;

export type ReduxStore = Store<StoreState>;

const middlewareList = [...getDefaultMiddleware(), logger];

export default configureStore({
  reducer: rootReducer,
  middleware: middlewareList,
  devTools: process.env.NODE_ENV !== "production",
  preloadedState: preloadedState(),
});

ここまででreduxの大部分ができました。
後は、これをページにつなげるだけです。

pagesにindex.tsx を作成したと思います。そのディレクトリーに _app.tsx を作成しましょう。

_app.tsx
import React from 'react';
import { AppProps } from 'next/app';
import { Provider } from 'react-redux';
import store from '../store/store';

const MyApp = ({ Component, pageProps }: AppProps) => {
  return (
    <Provider store={store}>
      <Component {...pageProps} />
    </Provider>
  );
};

export default MyApp;

確認してみたいので下記を実装してください

counter
import React from "react";
import { useDispatch } from "react-redux";
import { useCounterState } from "../../state/counter/selector";
import counterSlice from "../../store/counter/slice";

const CounterPage: React.FC = () => {
  const dispatch = useDispatch();
  const state = useCounterState().counter;

  const onClickIncrement = () => {
    dispatch(counterSlice.actions.incrementCounter(1));
  };

  const onClickDecrement = () => {
    dispatch(counterSlice.actions.decrementCounter(1));
  };

  return (
    <>
      <button type="button" onClick={onClickIncrement}></button>
      <button type="button" onClick={onClickDecrement}></button>
      <p>{state.count}</p>
    </>
  );
};
export default CounterPage;

悩んだところ

reactは、基本的にSPAで作成すると思います。疑似的にページのルーティングをしたとしても読み込みなおすのではなく書き直す行為なのでreduxは、問題なく実装できます。

しかしNextJsは、ページ遷移した際 redux の内容が初期化されてしまいます。それは、ページを読み込んでいるからだと思います。
こういったバグを修正するために以下のコードを書き直しました。

store.ts
//修正前
const createStore = () => {
  const middlewareList = [...getDefaultMiddleware(), logger];

  return configureStore({
    reducer: rootReducer,
    middleware: middlewareList,
    devTools: process.env.NODE_ENV !== 'production',
    preloadedState: preloadedState(),
  });
};

//修正後
const middlewareList = [...getDefaultMiddleware(), logger];

export default configureStore({
  reducer: rootReducer,
  middleware: middlewareList,
  devTools: process.env.NODE_ENV !== "production",
  preloadedState: preloadedState(),
});
_apptsx
//修正前
<Provider store={createStore()}>
//修正後
<Provider store={store}>

createStore()が毎回呼び出されているのでせっかく変更した値が初期化されるという内容でした。

まとめ

私自身NextJsを触ったばかりで、まだ何も作成していません。ですが、基本的な内容は、理解しているつもりです。
大企業や、大きいサービスなどもNextJsを使用しているそうです。
ただSSRについては、まだまだ調査しようと考えています。

NextJs自体SSRを推奨していないという記事も見ますし、実際どういった攻撃が危惧されているのか、事件など調べようと思います。

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