20200326のReactに関する記事は12件です。

react hook の useReactRouter の push でデータを渡す方法

<Button onClick={() => history.push({ pathname: '/page1', state: { aaa: "ここやで" }})}>

const { location } = useReactRouter();
{location.state.aaa}

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

【TypeScript】Next.jsでqueryパラメータを取得する方法

getInitialPropsの引数に({ query }: { query: any })を渡す

以下のようなイメージです。

Page.getInitialProps = async ({ query }: { query: any }) => {
  console.log(query);
  return query;
};

URLが 「/page?id=hogehoge」だった場合は { id: 'ZNDhKYaXyAnEcG2vqhUA' } を返します。

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

正気を失わないReact導入

概要

ReactとTypeScriptが使いたいだけの記事です。
公式サイトのチュートリアルを見るとcreate-react-appをするよう促されますが、ブラックボックスになっているのが嫌なので自力で環境構築をしていきます。

npmの導入

npmを導入してください

npmのプロジェクトの作成

mkdir myReactSample

npm init

webpackの導入

以下の手順ではwebpackの公式チュートリアルを参考にwebpackの導入を進めます

webpackのインストール

コンソール
npm install --save-dev webpack webpack-cli

webpackモジュールとwebpack-cliモジュールを両方developerモードでインストールするということですね。多分。

ファイルの作成

  myReactSample
  |- package.json
+ |- /dist
+   |- index.html
+ |- /src
+   |- index.js

このような階層になるようにindex.htmlindex.jsを作ります。

index.htmlの内容を作成

dist/index.html
<!doctype html>
<html>
  <head>
    <title>Getting Started</title>
  </head>
  <body>
    Hello!
  </body>
</html>

index.htmlをこのようにします。

index.htmlファイルをブラウザで開くと、当然Hello!と表示されます。

index.jsの内容を作成

src/index.js
function component() {
  const element = document.createElement('div');
  element.innerHTML = "Webpack!"
  return element;
}

document.body.appendChild(component());

このようにします。body<div>Webpack!</div>を追加しているだけですね。

index.htmlからmain.jsを読み込む

dist/index.html
<!doctype html>
<html>
  <head>
    <title>Getting Started</title>
  </head>
  <body>
    Hello!
    <script src="main.js"></script>
  </body>
</html>

bodyに<script src="main.js"></script>を追加します。webpackではデフォルトでsrc/index.jsをエントリーポイントとしてdist/main.jsにコンパイルします。そのため、dist内のindex.htmlは相対パスでmain.jsを指定して読み込めばよいです。

webpackでのビルド

コンソール
npx webpack

このコマンドを打ち込むと、src/index.jsdist/main.jsにコンパイルされます。

  myReactSample
 |- package.json
 |- /dist
   |- index.html
+  |- main.js
 |- /src
   |- index.js

このような階層になっているはずです。この状態でindex.htmlをブラウザで開くとHello!の下にWebpack!が表示されるはずです。

ここまででWebpackの導入が終わりです。次にWebpack DevServerの導入をします。

webpack DevServerの導入

Webpack Devserverのインストール

Webpack DevServerをインストール

コンソール
npm install --save-dev webpack-dev-server

プロジェクトのルートにwebpack.config.jsをつくって以下のようにします。

webpack.config.js
var path = require('path');

module.exports = {
  mode: 'development',
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9000
  }
};

コンソールでWebpack DevServerを起動します。

コンソール
npx webpack-dev-server

localホストでサーバーが起動します。どのポートで起動するかはコンソールのログに出力されるので、そのポートを指定してブラウザで開いて下さい。

うまく行かない場合はwebpack.config.jsでコロンが足りないなどでエラーになっている事があります。

ここまで来れば、index.jsを編集すると自動でリビルドが走ります。

コンテンツ監視の設定

webpack.config.js
var path = require('path');

module.exports = {
  mode: 'development',
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9000,
    watchContentBase: true
  }
};

devServer:の中にwatchContentBase: trueを追記します。
コンソールでWebpack DevServerを再起動します。Ctrl + cしてから、

コンソール
npx webpack-dev-server

をすると、jsファイルだけでなく、htmlファイルを更新した際も自動でリビルドが走ります。

babelの導入

babelのインストール

コンソール
$ npm install --save-dev @babel/core @babel/polyfill @babel/preset-env @babel/react babel-loader

webpack.configの変更

jsxのビルドのためにbabelを導入します。

webpack.config.js
var path = require('path');

module.exports = {
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: "babel-loader",
            options: {
              presets: [
                "@babel/preset-env",
                "@babel/react"
              ]
            }
          }
        ]
      }
    ]
  },
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9000,
    watchContentBase: true
  }
};

Reactのインストール

コンソール
npm install --save-dev react react-dom

Reactを使う!

index.htmlに<div>root<div>を追加して、index.jsでReactから読みます。

dist/index.html
<!doctype html>
<html>
  <head>
    <title>Getting Started</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="main.js"></script>
  </body>
</html>

dist/index.js
import React from 'react'
import ReactDOM from 'react-dom'

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    )
  }
}

function App() {
  return (
    <div>
      <Clock />
      <Clock />
      <Clock />
    </div>
  )
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

これで動くはずです。パソコンの電源が切れたのでTypeScriptなど続きはまた今度です。

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

React + TypeScript の開発環境を作る方法を丁寧に

仕事で新しい機能を開発する際に、フロントエンドにはVue.jsをよく採用しています。しかし個人的な開発でReactを使ってみたところ、書き味を気に入ってしまって、最近はReact推しです。

さて、開発環境構築の記事を書くにあたって、記事の有効期限が気になりました。JavaScriptアプリケーション周辺技術の栄枯盛衰の速度を考えると、単純に叩いたコマンドやファイルの内容を列挙していくだけでは、使用しているツールのバージョンの違い等で容易にエラーを起こすことが考えられます。ツール単体の導入方法は公式ページにあるんですから、そっちを見たら良いですしね。使用するツールのバージョンを全て固定することも考えられますが...。

そんなわけで、有効期限が少しでも長くなることを狙ったReactアプリ開発環境構築の記事を書く試みです。

方針:

  • コマンドや設定の詳細は適時省き、一次ソース(公式ページ)を参照できるようにします。
  • 最終的に index.html、main.js、index.cssを出力し、それだけで完結するSPAアプリになります。

この記事で選択する技術です。今自分が新しいJSアプリを作るならこうなります。選択理由も併記。

  • React: 好き。TypeScriptと相性がいい。
  • TypeScript: 型。Elmが恋しい。
  • Webpack: ビルド構成の自由度の高さ。Create React AppはPostCSSを足しにくいので却下。
  • PostCSS: 最低限autoprefixerを入れたい。
  • Prettier: 機械にできることは機械任せ、人間は創造力を発揮しましょう。

それでは順番に導入していきましょう。

プロジェクト作成

mkdir my-app
cd my-app
git init
npm init -y

WebpackでJavaScriptをビルドできるようにする

https://webpack.js.org/guides/getting-started/
このページを見ながら、Pure JavaScriptをビルドできるようにします。
ただし、index.jsは以下の内容でいいでしょう。lodashのインストールは不要です。

src/index.js
alert('JS Loaded');

最終的に以下のようなファイル構成になります。

 my-app
 |- package.json
 |- webpack.config.js
 |- /dist
   |- main.js
   |- index.html
 |- /src
   |- index.js
 |- /node_modules

このステップでの確認事項:

  • package.jsonにnpm run buildコマンドの内容を記述し、webpackコマンドが実行されるようにした。
  • webpack.config.jsで入力と出力を設定した。
  • npm run buildコマンドを打った時、src/index.jsを入力としてdist/main.jsが出力される。

webpack-dev-serverを導入する

開発時に便利なlive reloading(ファイルを更新したら画面を自動でリロードしてくれる)とHot Module Replacement(HMR、ファイルを更新したら画面全体をリロードするのではなくモジュール単体を更新してくれる)はこの時点で入れておきます。
https://webpack.js.org/guides/development/#using-webpack-dev-server
このページの"Using webpack-dev-server"を参考に設定し、npm run startした時にブラウザでdist/index.htmlが開かれるようになればOKです。
https://webpack.js.org/guides/hot-module-replacement/
このページを参考に、HMRを有効にしておきます。まだ別モジュールを作っていないのですが、このページで行っているように別ファイルを作って試してみるのもいいでしょう。

このステップでの確認事項:

  • webpack.config.jsにdev server用の設定をした。
  • package.jsonでnpm run startコマンドの内容を記述し、そこではwebpack-dev-serverが起動するようにした。

Prettierでコードフォーマットできるようにする

https://prettier.io/docs/en/install.html
このページを見てインストール。
https://prettier.io/docs/en/cli.html
このページを見て使い方を知る。prettier --write .と書けばいいそうですから、

package.json
{
   ...
   "scripts": {
     ...
     "format": "prettier --write ."
   }
}

としておきましょう。実行してみると分かりますが、distディレクトリ内のファイル等、フォーマットしてほしくないファイルもあります。
https://prettier.io/docs/en/ignore.html#ignoring-files
このページを参考に、.prettierignoreファイルを用意して、distディレクトリはフォーマットしないようにします。

このステップでの確認事項:

  • package.jsonでnpm run formatコマンドの内容を記述し、prettierが実行されるようにした。

TypeScriptで書けるようにする

https://webpack.js.org/guides/typescript/
WebpackのページにTypeScriptのセットアップ方法があります。
この記事を書いた時点ではコンパイラとしてts-loaderを使い、ES5まで変換しています。ゆえにbabelは使いません。
https://www.typescriptlang.org/docs/handbook/compiler-options.html
tsconfig.json compilerOptionsの設定値は確認しておきましょう。ただ、設定値に悩まないコツは、不要な設定をしないことだと今のところ思います。何か問題が起こって、必要に迫られた時に設定を追加変更していくぐらいでも良いです。逆に最初からstrictに行くぜ!って人は、"strict": true, "allowJs": false, "noImplicitAny": true, "strictNullChecks": trueでどうでしょう。

このステップでの確認事項:

  • npm run buildすると、TypeScriptで書かれたファイルがJavaScirptファイルに変換された形でdistディレクトリに出力される。

React導入

ここでどこを参照したら良いか悩みます。いくつかの情報を組み合わせることにしました。
https://blog.usejournal.com/creating-a-react-app-from-scratch-f3c693b84658
WebpackでReactアプリを作成する手順として、React公式ページからリンクされた記事です。今babelを使わないのでそれを差っ引いて参考にします。
https://www.typescriptlang.org/docs/handbook/react-&-webpack.html
TypeScript公式ページの、ReactとWebpackセットアップ解説。Node.jsでホストすることを想定しているようなのでそれを踏まえて参考にします。
https://reactjs.org/docs/components-and-props.html
React v16現在、コンポーネントの定義方法がFunctionとClassの2つあります。前者が新しい方法です。
https://reactjs.org/docs/hooks-intro.html
便利なReact HooksもFunctionコンポーネント前提です。Functionコンポーネントを採用することにしましょう。
https://reactjs.org/docs/getting-started.html
もしかしたら将来React Getting Startedのページに適切な一次情報が増えるかもしれないので、そちらも探してみてください。

パッケージインストール

npm install react react-dom
npm install --save-dev @types/react @types/react-dom source-map-loader

esModuleInteropについて

TypeScript公式ページ通りにtsconfig.jsonを設定してみます。

tsconfig.json
{
     "compilerOptions": {
         "outDir": "./dist/",
         "sourceMap": true,
         "noImplicitAny": true,
         "module": "commonjs",
         "target": "es6",
         "jsx": "react"
     }
}

src/index.tsxをこう書くと...

src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App.js";
ReactDOM.render(<App />, document.getElementById("root"));

エラー。
Screen Shot 2020-03-26 at 7.55.31.png
https://stackoverflow.com/a/56348146
なぜこうなるかの説明がありました。ReactはCommonJSモジュールのようですね。
このままだと以下のようにimportしないといけなくなるわけです。

src/index.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";

tsconfig.jsonに"esModuleInterop": true設定を足すと、最初に書いたようにimportできるようになります。こっちを採用します。

Reactセットアップ

tsconfig.json
{
     "compilerOptions": {
         "outDir": "./dist/",
         "sourceMap": true,
         "noImplicitAny": true,
         "module": "commonjs",
         "target": "es6",
         "jsx": "react",
         "esModuleInterop": true
     }
}
webpack.config.js
 const path = require("path");

 module.exports = {
   entry: "./src/index.tsx",
   output: {
     filename: "main.js",
     path: path.resolve(__dirname, "dist")
   },
   module: {
     rules: [
       {
         test: /\.tsx?$/,
         use: "ts-loader",
         exclude: /node_modules/
       },

       {
         enforce: "pre",
         test: /\.js$/,
         loader: "source-map-loader"
       }
     ]
   },
   resolve: {
     extensions: [".tsx", ".ts", ".js"]
   },
   devtool: "source-map",
   devServer: {
     contentBase: "./dist",
     hot: true
   }
 };
src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./app/App.tsx";
ReactDOM.render(<App />, document.getElementById("root"));
src/app/App.tsx
import React from "react"

const App: React.FC = () => {
    return <div>Hello React App</div>
}

export default App
dist/index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Getting Started</title>
  </head>
  <body>
    <div id="root"></div><!-- Reactをマウントする場所を用意 -->
    <script src="main.js"></script>
  </body>
</html>

ここまで書けたらnpm run startしてみてください。コンソールにエラーが出ず、画面にHello React Appと表示されたら成功です。

src/app/App.tsxのReact.FCについて補足しておきます。JavaScriptでReact Functionコンポーネントを定義する時は下のようになります。

App.tsx
const App = () => {
    return <div>Hello React App</div>
}

Functionコンポーネント用のinterface FunctionComponentが@types/reactに定義されています(公式での説明が見つかりませんでしたが)。この省略形としてtype FCが定義されています。propsの型チェック等に便利なので使用していきます。

HMR対応

さて、npm run start(webpack-dev-serverで画面表示)している状態で、src/app/App.tsxを編集保存してみてください。ブラウザが自動リロードして画面が更新されると思います。本来HMRではAppが描画している部分だけが更新してほしいので、対応します。
https://webpack.js.org/guides/hot-module-replacement/
https://webpack.js.org/concepts/hot-module-replacement/
https://webpack.js.org/api/hot-module-replacement/

src/index.tsx
import React from "react";
import ReactDOM from "react-dom";

const render = () => {
    const App = require('./app/App').default;
    ReactDOM.render(<App />, document.getElementById("root"));
}

render();

if (module.hot) {
    module.hot.accept('./app/App', () => {
        render();
    })
}

module.hot.acceptの第一引数で指定したファイルが更新されると第二引数の関数が実行されるので、再度renderします。
TypeScriptで書いているので、module.hotに関する型情報が必要です。
Screen Shot 2020-03-26 at 14.17.59.png

npm i -D @types/webpack-env

src/app/App.tsxを編集保存してブラウザがリロードせずに画面が更新されれば成功です。

PostCSS導入

https://github.com/postcss/postcss-loader
src/index.cssを用意しsrc/index.tsxで import "./index.css"; した上で、上のページのとおり設定していきます。

このステップでの確認事項:

  • index.tsxからindex.cssをimportした
  • webpack.config.jsでpostcss-loaderのルールを追加した
  • postcss.config.jsを作成し設定を行った

CSSを外部ファイル化したい場合は、以下を見るといいでしょう。
https://github.com/postcss/postcss-loader#extract-css
https://webpack.js.org/plugins/mini-css-extract-plugin/

Webpack設定の補足など

webpack.config.jsのmodeを今まで無視してきました。
https://webpack.js.org/configuration/mode/
npm run build時とnpm run start時それぞれでmodeを切り替えたい。上のページでもCLIからmodeを渡す方法を紹介しています。
https://webpack.js.org/guides/production/
このようにwebpack-mergeを使っているプロジェクトもよく見ます。
個人的には、postcss.config.js等の外部ファイルでも動作を切り替えたいので、cross-envでNODE_ENVを定義することが多いです。
https://www.npmjs.com/package/cross-env

今回dist/index.htmlは自前で作成しましたが、HtmlWebpackPluginを使うと、Webpackが出力するJSやCSSをロードするHTMLファイルを生成してくれます。
https://webpack.js.org/plugins/html-webpack-plugin/

最後に

以上で解説は終了です。
環境構築記事では、このツールとこのツールを組み合わせることが可能か、組み合わせるのにはどうしたら良いかという点にオリジナリティが出ます。そこを書くにはどうしても設定方法を詳細に書くことになります。詳細に書くと記事の有効期限が短くなりがち。結論、環境構築記事は生もの!常に更新しない限りすぐ腐る。ただ、信頼できる一次ソースを参考文献としてちゃんと示すと、読んだ人が判断しやすい記事になりそうだなとは思いました。

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

【React】ReduxToolkitで非同期処理を実装する

概要

  • Reduxを使って非同期処理を実装する際はReduxThunkのようなmiddlewareを使うケースが多いと思います
  • しかしmiddlewareを使うと実装がややこしくなってしまうケースも多く悩ましい場面もあります
  • ReduxToolkitはReduxThunkが内包されていて実装量少なくReduxで非同期処理を作ることができます

事前準備

npx create-react-app redux-thunk-sample
  • 以下のコマンドで起動できます
yarn start
# open http://localhost:3000

ReduxToolkitによる非同期処理の実装

  • 今回はQiitaのAPIをたたいて記事一覧を取得してみます

ライブラリの追加

  • Redux関連ライブラリをインストール
yarn add react-redux @reduxjs/toolkit

通信処理の作成

  • QiitaのAPIを叩く部分を作成します
src/api/qiitaApi.js
export async function getItems() {
  const res = await fetch('https://qiita.com/api/v2/items');
  const json = await res.json();
  if (!res.ok) throw new Error(json.message);
  return json;
}
  • 今回のメインとなるReduxの処理を書くファイルを作成します
src/store/qiitaSlice.js
import { createSlice } from '@reduxjs/toolkit';
import { getItems } from '../api/qiitaApi';

// Slice
export const qiitaSlice = createSlice({
  name: 'qiita',
  // stateの初期値を設定
  initialState: { loading: false, error: null, items: [] },
  reducers: {
    // 通信を開始した時に呼ぶ関数
    fetchStart(state, action) {
      state.loading = true;
      state.error = null;
    },
    // 通信が失敗した時に呼ぶ関数
    fetchFailure(state, action) {
      state.loading = false;
      state.error = action.payload;
    },
    // 通信が成功した時に呼ぶ関数
    fetchSuccess(state, action) {
      state.loading = false;
      state.error = null;
      state.items = action.payload;
    },
  },
});

// Actions
export const { fetchStart, fetchFailure, fetchSuccess } = qiitaSlice.actions;

// 外部からはこの関数を呼んでもらう
export const fetchItems = () => async dispatch => {
  try {
    dispatch(fetchStart());
    dispatch(fetchSuccess(await getItems()));
  } catch (error) {
    dispatch(fetchFailure(error.stack));
  }
};

// Selectors
export const selectQiita = ({ qiita }) => qiita;

// Reducer(must be default export)
export default qiitaSlice.reducer;
  • 通信中かどうかを示すloading、エラー情報を入れるerror、Qiitaから取得した記事を入れるitemsの3つのStateを持ちます
  • 非同期ではない場合はreducersに定義した関数を外部からdispatchしていました
    • 非同期処理の場合は処理の開始時と終了時にStateを更新したくなります
    • なので外部から直接reducersに定義した関数をdispatchせずにワンクッション挟む関数(ここではfetchItems)をdispatchしてもらうことで対応できます

記事一覧を表示するコンポーネントの作成

  • Reduxの処理を呼び出して取得した情報を表示するコンポーネントを作成します
src/components/QiitaItems.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { selectQiita, fetchItems } from '../store/qiitaSlice';

function QiitaItems() {
  const dispatch = useDispatch();
  const { loading, error, items } = useSelector(selectQiita);

  useEffect(() => {
    // fetchItemsを実行
    dispatch(fetchItems());
  }, [dispatch]);

  if (loading) return <p>...loading</p>;
  if (error) return <p>{error}</p>;

  return items.map(item => (
    <div key={item.id}>
      <h2><a href={item.url}>{item.title}</a></h2>
      <p>{item.created_at} by {item.user.id}</p>
    </div>
  ));
}

export default QiitaItems;
  • useEffectでfetchItemsをdispatchしているのでコンポーネント生成時に通信処理を呼び出している感じです
  • 通信中は...loadingと表示されて完了すると記事の情報が表示されるようにしています

Reduxのセッティング

  • Reduxを使うための設定を入れておきます
  • App.jsの修正
src/App.js
import React from 'react';
import { Provider } from 'react-redux';
import { store } from './store';
import QiitaItems from './components/QiitaItems';

function App() {
  return (
    <Provider store={store}>
      <QiitaItems />
    </Provider>
  );
}

export default App;
  • Storeの作成
src/store/index.js
import { configureStore } from '@reduxjs/toolkit';
import qiitaItemsReducer from './qiitaItemSlice';

export const store = configureStore({
  reducer: {
    qiitaItems: qiitaItemsReducer,
  },
});

動作確認

  • うまくいっていれば以下のようになっているはずです

demo.gif

まとめ

  • ReduxToolkitを使うと通信処理もそれほど複雑さを感じずに実装できました
  • 実装されたコードもわかりやすくていいですね
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react-bootstrapでWarning: validateDOMNesting(...): <div> cannot appear as a descendant of <p>

react-bootstrapでこのエラーに遭遇して対処法を調べたのですが日本語の記事が少なくて解決に時間がかかったのでメモとして残しておきます。

エラーの原因

コンソールには以下のエラーが出力されます

Warning: validateDOMNesting(...): <div> cannot appear as a descendant of <p>.

原因がわからずググってみたら、このサイトによると<div>タグを<p>タグでラップしているとこのエラーが起こるようです。
しかし、<p>タグを使っている箇所がない、、、

解決策

原因となっていたのは以下のコードでした

              <Card.Text>
              <Row style={{ clear: "both" }}>
                <SelectButton 
                  isSelected={selectedPictureList.includes(picture)}
                  target={picture}
                  selectedFunc={selectPicture}
                  unselectedFunc={unselectPicture}
                />
                <PicturesCommentNewContainer pictureId={picture.id} />
              </Row>

              </Card.Text>

こちらのサイトによると、<Card.text>の出力は<p>タグになってしまうそうです。
その中のコンテナ(PicturesCommentNewContainer)でdivタグを返す処理をしていたので、エラーが起こっていたみたいですね...

              <Row style={{ clear: "both" }}>
                <SelectButton 
                  isSelected={selectedPictureList.includes(picture)}
                  target={picture}
                  selectedFunc={selectPicture}
                  unselectedFunc={unselectPicture}
                />
                <PicturesCommentNewContainer pictureId={picture.id} />
              </Row>

このように、単純にを使わなければエラーは無事なくなりました
bootstrapめんどい!

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

JavaScript系:Qiitaのタグ統計を浄化するためのタグ詰め合わせ記事

タグだけ

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

ライフサイクルメソッドがわかればReactの50%は理解したと言えなくもない

ライフサイクルの話をする前に・・・

まず前提として下記のことが言えると思う。

Reactとはそもそも、状態の管理、更新を適切に描画に反映するためのライブラリである。さらに、Componentの最も大きな役割は描画(render)をすることである。

効率良く描画の表示、更新をするためにはComponentのライフサイクルを理解することが必須であると言える。

ライフサイクルとは

クラスコンポーネントを扱う際にライフサイクルは大きく分けて3つの期間がある。順番にMounting(マウント時)、Updating(更新時)、Unmounting(マウント解除時)。

公式でも下記のような図が紹介されているのでこちらを元に説明をする。

image.png

React lifecycle methods diagram

Mounting(マウント時)

Componentが表示されるまでの期間のこと。

  1. constructor()
  2. getDerivedStateFromProps() // 一般的ではないライフサイクル
  3. render() // ここで描画される
  4. componentDidMount()

componentDidMountで描画された後に何かしらの変更を加えたい、描画されてから出ないとできないことをここで行うことになる。ちなみに、最初にrenderされるときだけこの期間に入る。

例えばユーザー一覧を表示させるUIだとしたら、基本的なTableの見出しとユーザーの取得が完了するまでの間に表示させる Loading... などがMounting時のrenderに相当する。そこまでの描画が完了したら componentDidMount でユーザー一覧のAPIを叩く。

Updating(更新時)

Componentが表示され、Stateの更新を行う期間のこと。

  1. getDerivedStateFromProps() // 一般的ではないライフサイクル
  2. shouldComponentUpdate() // 一般的ではないライフサイクル
  3. render()
  4. getSnapshotBeforeUpdate() // 一般的ではないライフサイクル
  5. componentDidUpdate()

Stateの更新がされたら、1 ~ 5の順番でメソッドが呼ばれ再描画される。

先ほどの例でいけば、componentDidMount でユーザー一覧のAPIを叩いた後に、取得が完了すればStateのusersにデータが入ることになる。

state = {
  users: [
    // ここの部分に入る
  ]
}

このusersが [] から [{id: 1, name: 'hogehoge'}] と更新されたことを検知して再描画 > TableにUserを表示させることができる。

では、componentDidUpdate() はどのタイミングで使うべきなのか例を挙げてみる。

ユーザーを新たに追加したいとしよう。UIでは一覧上で 新規登録 ボタンを押すとモーダルが出てきて ユーザー名を入力 > 登録とすることで追加することができるとする。

その場合、登録を押すことで APIを叩いてpostしているわけだが、送り終わったことがわかるStateの変更を検知して、ユーザー一覧の再取得をすることができる。

componentDidUpdate(prevProps) {
  if(!prevProps.succeeded && this.props.succeeded) {
    // ユーザー一覧を再取得
  }
}

このときわかりにくいのが、prevPropsである。読んで字のごとく以前のPropsである。では、どういうことかというと componentDidUpdate では更新される以前のPropsまたはStateと更新後のPropsまたはStateを比較することができる。それにより、特定のState(Props)が更新されたときにだけ何かしらの処理をすることができる。

Unmounting(マウント解除時)

他のComponentに移るときにだけ呼ばれる期間。

  1. componentWillUnmount()

現在のコンポーネントを破棄する直前に呼ばれるメソッド。

よく見落とされるのがアニメーションやタイマーを設定を ComponentDidMount でしていた場合は componentWillUnmount で破棄すること。そうしないと新しいコンポーネントのサイクルが始まった後も、その分のメモリが開放されないままになってしまう。

unsafeメソッドについて

実は以前は、他にもライフサイクルメソッドが存在していた。(今もしているけど非推奨)例えば、

  • componentWillMount()
  • componentWillUpdate()
  • componentWillReceiveProps()

である。これらを自身のプロジェクトで見つけたらこっそり書き換えてあげると良い。

ちなみに、完全に個人的な見解だけど、結局ライフサイクルメソッドがたくさんあると、どこで何が変更されたなど、タイミングが難しくなってしまうことが多々ある。人に優しくないコードが簡単にできてしまうのではないかなと。

なので、基本的には componentDidMount, componentDidUpdate でまかなえるようにComponentの設計をすべきであると思う。

その流れが hooksのuseEffectに引き継がれているのかなと。

Functional Componentの場合のライフサイクル

以前までは クラスコンポーネントでないとライフサイクルを使うことができなかった。が、hooksの登場により Functional Component でもライフサイクルを使うことができるようになった。

useEffectを使うことで簡単に 特定Stateの更新を検知することができる。

React.useEffect(() => {
  // なんかやる    
}, [// 検知したいState ])

例えば、エラーコードが検知したら何かやりたい場合は以下のような感じになる。

const [code, setErrCode] = React.useState<string>('')
  React.useEffect(() => {
    if (code) {
      // codeが変更されたら何かやる
    }
  }, [code])

ライフサイクルメソッドの問題点?

状態の管理、更新を適切に描画をするのには必須と言えるライフサイクルメソッドだが、問題が発生するケースもある。

よくあるのが、componentDidUpdate内でifが乱立していて更新の検知が適切に行えているのかわかりにくいことがあげられる。

この問題の根本はライフサイクルメソッドが悪いのではなくComponentの設計に問題があることがほとんど。

Componentがやたらと大きくなりすぎていて多機能すぎていると言える。

次回はComponentの設計あたりを記事にする予定・・・

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

ReactでHTTPボディを送る方法

やりたいこと

reactから req.body.id をnodeへ送って、nodeで、 req.body.id を受け取りたかった。その後、 req.body.id の値を使ってmongoから値を取得する流れ

react

    const fetchData = async () => {
      var params = new URLSearchParams()
      params.append('id', pass)
      const result = await axios.post('http://localhost:3000/signin', params);

node

  app.post('/signin', function(req, res) {
    User.findOne({_id: req.body.id}, function(err,obj) { 
        console.log(obj);

参考

axiosの使い方まとめ (GET/POST/例外処理)
[Web] HTTPリクエストの中身を学んでみた。GETやPOSTの違いなど

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

Laravel7+Reactの環境を構築する

Laravel7 で React を利用した開発を可能にする手順をご紹介いたします。
ここでは Laravel と React はインストール済みとして進めていきます。

開発環境

  • Laravel 7.2.1
  • React 16.13.1
  • PHP 7.4.3

Laravel で React を利用できるようにする

React を利用する Laravel のプロジェクトを作成し、
作成されたプロジェクトフォルダに遷移します。

composer create-project laravel/laravel hoge-project
cd hoge-project

次に Laravel が React を利用するように設定を変更します。
ui artisan コマンドを使用するので、laravel/ui パッケージを先にインストールしています。

composer require laravel/ui
php artisan ui react

最後に下記コマンドを実行すると、Laravel で React を使えるようになります。

npm install && npm run dev

Laravel の動作確認をする

Laravel が正常に動作するか確認します。

php artisan serve

http://127.0.0.1:8000 にアクセスして、Laravel のページが表示されたら正常に動作しています。

スクリーンショット 2020-03-26 7.27.19.png

js ファイルの変更を常時反映させるようにしておく

現状のままだと js ファイルの変更が検知されないので、常時変更を検知してくれるように設定します。
別のターミナルを起動し、Laravel のプロジェクトフォルダ内で npm run watch を実行します。

cd hoge-project
npm run watch

React のサンプルファイルで文字列を表示する

React を利用して画面上に文字列を表示できるよう、既存ファイルを変更していきます。
まずは React を使用しているサンプルファイル(resources\js\components\Example.js)を見てみましょう。

resources\js\components\Example.js
import React from 'react';
import ReactDOM from 'react-dom';

function Example() {
    return (
        // 省略
    );
}

export default Example;

if (document.getElementById('example')) {
    ReactDOM.render(<Example />, document.getElementById('example'));
}

id が "example" の HTML タグに文字列を表示する処理だとわかります。

それでは Example.js が出力する内容を表示する HTML タグを記述しましょう。
http://127.0.0.1:8000 にアクセスすると表示される内容は welcome.blade.php(resources\views\welcome.blade.php)に記述されているので、
下記のように編集します。

resouces/views/welcome.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Laravel</title>
    </head>
    <body>
        <div id="example"></div>
        <script src="{{ mix('js/app.js') }}"></script>
    </body>
</html>

body タグ内に、React 出力データの挿入場所である <div id="example"></div> と、
Example.js を処理するファイル app.js の読み込みを記述します。

http://127.0.0.1:8000 にアクセスし、下記の通りに表示されていれば成功です。

スクリーンショット 2020-03-26 8.02.25.png

もし Example.js 以外のファイル、例えば Hoge.js を読みこませたい場合は、
app.js(resources\js\app.js)を下記のように編集すると良いでしょう。

\resources\js\app.js
require('./bootstrap');
require('./components/Example');

// 下記を追記
require('./components/Hoge');

以上です。

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

Gatsby.jsのsourceNodesライフサイクルでresourceを参照する方法

Gatsby.jsでノードを作るライフサイクルのsourceNodesでresourceのデータを参照したい時に詰まったのでメモ。

経緯

あるresourceのデータに外部画像のURLがあり、それを gatsby-image で使うために、gatsby-source-filesystem の createRemoteFileNode で外部画像用のNodeを生やす必要がありました。

新たにNodeを生やすのでsourceNodesのライフサイクルで

  1. 内部のスキーマに対してGraphQLで対象のURLを取得
  2. そのURLに対してcreateRemoteFileNodeでノードを作成する

で良いのかなと思ったのですが、sourceNodesではまだschemaがないのでGraphQLのAPIを呼べませんでした。

ということで、sourceNodesライフサイクルでresourceを参照する方法です。

方法

getNode API で直接resouceのノードの情報を参照する。以上です。 schemaはないのですが、ノードはあるのでアクセスできるのですね。

const node = getNode("参照したいノードのID")

実際のコードはこちらです。
sourceNodesでfacebookのfeedのresourceから画像情報を取得してcreateRemoteFileNodeから外部画像用のノードを作成する例です

gatsby-node.js
"use strict";
const { createRemoteFileNode } = require(`gatsby-source-filesystem`);

exports.sourceNodes = async ({
  actions: { createNode, createNodeField },
  createNodeId,
  cache,
  store,
  getNode
}) => {
  // クエリは投げられないでNodeから直接取得。
  // 画像がないfeedもあるのでfilterで除外
  const feedData = getNode("ノードID").feed.data.filter(
    u => u.full_picture != null
  );

  await Promise.all(
    feedData.map(async data => {
      // 外部画像のダウンロードとノードの構築
      const fileNode = await createRemoteFileNode({
        url: data.full_picture,
        cache,
        store,
        createNode,
        createNodeId
      });

      await createNodeField({
        node: fileNode,
        name: "feedImage",
        value: "true"
      });

      await createNodeField({
        node: fileNode,
        name: "feedId",
        value: data.id
      });

      return fileNode;
    })
  );
};

参考

以下参考にさせて頂きました。良記事ありがとうございます。

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

GraphQLサーバーにクエリを実行する方法

GraphSQL

Queries And Mutations

Arguments

REST API: リクエスト内のクエリパラメータとURLセグメントのみ
GraphQL: オブジェクトが独自の引数を取得できる -> 複数のAPIフェッチを完全に置き換えることができます.

{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}
{
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 5.6430448
    }
  }
}

Alias

Argumentsの例だと, 異なる引数を持つフィールドを複数もつことができない

NG
graphql
{
human(id: "1000") {
name
height(unit: FOOT)
},
human(id: "1001") {
name
height(unit: FOOT)
}
}

GraphQLでは, Aliasを指定して対応可能.

{
  human1000: human(id: "1000") {
    name
  }
  human1001: hero(id: "1001") {
    name
  }
}
{
  "data": {
    "human1000": {...},
    "human1001": {...}
  }
}

Fragments

再利用可能なユニット
重複するフィールドセットはFragmentsに書き出す.

{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}
{
  "data": {
    "leftComparison": {
      "name": "Luke Skywalker",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "friends": [
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        },
        {
          "name": "C-3PO"
        },
        {
          "name": "R2-D2"
        }
      ]
    },
    "rightComparison": {
      "name": "R2-D2",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

Fragments + variables

query HeroComparison($first: Int = 3) {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}
{
  "data": {
    "leftComparison": {
      "name": "Luke Skywalker",
      "friendsConnection": {
        "totalCount": 4,
        "edges": [
          {
            "node": {
              "name": "Han Solo"
            }
          },
          {
            "node": {
              "name": "Leia Organa"
            }
          },
          {
            "node": {
              "name": "C-3PO"
            }
          }
        ]
      }
    },
    "rightComparison": {
      "name": "R2-D2",
      "friendsConnection": {
        "totalCount": 3,
        "edges": [
          {
            "node": {
              "name": "Luke Skywalker"
            }
          },
          {
            "node": {
              "name": "Han Solo"
            }
          },
          {
            "node": {
              "name": "Leia Organa"
            }
          }
        ]
      }
    }
  }
}

Operation name

特になし
ref: https://graphql.org/learn/queries/#operation-name

Variables

Variable definitions

optional な場合 !を付与する

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}

Default variables

デフォルト値を設定する場合以下のように表現する

query HeroNameAndFriends($episode: Episode = JEDI) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

Directives

Directiveはフィールドまたはフラグメントに添付でき,
サーバーが望む方法でクエリの実行に影響を与える可能性があります

クエリのフィールドを追加および削除するために文字列操作を行う必要がある状況を回避するのに役立ちます

  • @include(if: Boolean) trueの場合にのみ、このフィールドを結果に含める
  • @skip(if: Boolean) trueの場合、このフィールドをスキップします

Mutations

RESTではGETでデータを更新することは推奨されていない.
同様にGraphQLでも、クエリを実装してデータを書き込むことは推奨されない。

書き込みを引き起こす操作は、ミューテーションを介して明示的に送信する必要があるという規則を確立すると便利です

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

Inline Fragments

GraghQLは, interfaceとunion typeを持つ.

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}

Meta Fields

GraphQLサービスから返されるタイプがわからない状況がある場合、クライアントでそのデータを処理する方法を決定する方法が必要である
クエリの任意の時点でメタフィールドである__typenameを要求して、その時点でのオブジェクトタイプの名前を取得できる

{
  search(text: "an") {
    __typename
    ... on Human {
      name
    }
    ... on Droid {
      name
    }
    ... on Starship {
      name
    }
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む