20200212のReactに関する記事は6件です。

JavaScriptしか知らない初心者のReact Native入門

プログラミング歴半年の素人が書いています。

間違いのないようご自身でも良く調べた上でお願いいたします。

対象読者

  • モバイルアプリ開発をしたことがない人
  • JavaScriptの基礎構文がわかる人
  • Reactを知っている人
  • Macの人

わたしです。 試行錯誤の記録となっておりますので、ベストプラクティスではない可能性が十分含まれておりますことご了承ください。

環境構築

Node.jsのインストール

https://nodejs.org/en/download/

上記リンクからダウンロードできます。

ReactNativeのインストール

公式チュートリアルでもおすすめされているように、「Expo CLI」という「ReactNative+便利機能いろいろ」のパッケージをインストールしてみます。

ついでに、お手持ちのスマホのアプリストアで「Expo」を検索し、アプリをスマホにダウンロードしておくとよいでしょう。自分のスマホで、開発中の画面を確認することができます。

$ npm install -g expo-cli

上記のコマンドで「Expo CLI」をインストールできます。

新しいプロジェクトの作成

早速、アプリの開発にとりくみましょう。

$ expo init AwesomeProject

新しいプロジェクトを作成します。今回は「AwesomeProject」という名前をつけました。

途中、スターターテンプレートを何にするか聞かれます。
「Blank」とか「Blank with TypeScript」だとか、いろいろありますが、今回は初めてなので「Blank」を選択してみました。

cd AwesomeProject
npm start

プロジェクトの作成が済んだら、作成したディレクトリに移動して、npm startで起動します。
ブラウザにCLIが表示されたら成功です。

Expoのダウンロードと登録

https://expo.io/

上記からExpoにアクセスし、SignUp します。
お使いのスマホがあれば、公式アプリをダウンロードします。

Expoで開発画面を表示する

npm startしてブラウザに表示された画面に、QRコードがあればそれをスマホで読み取ることで開発中の画面を表示することができます。

ここでエラー!

私の場合はここでエラーがでました。

error Unable to resolve "@react-navigation/native" from "App.js"

うーん、初めてなのでなんのこっちゃモジュール。

ひとまずApp.js公式チュートリアルのとおり、シンプルに書き直すことで解決しました。

App.js
import React, { Component } from 'react';
import { Text, View } from 'react-native';

export default class HelloWorldApp extends Component {
  render() {
    return (
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <Text>Hello, world!</Text>
      </View>
    );
  }
}

ちなみに、Hooksで関数コンポーネントにしても動きました。(react-native: 0.61.4

App.js
import React from 'react';
import { Text, View } from 'react-native';

const HelloWorldApp = () => {
    return(
        <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
            <Text>Hello, World!</Text>
        </View>
    )
}

export default HelloWorldApp

無事、Hello Worldまでたどりつきました。

スクリーンショット 2020-02-12 21.21.43.png

React Native独自のコンポーネント

通常のJSXで使うことができる<div><h1>などのコンポーネントの代わりに<View><Text>を使います。

- <View> : <div><span>
- <Text> : <p>など文字列の表示に使用

画像を表示する

「Hello, World!」の代わりに画像を表示します。
React Nativeでは<img>のかわりに<Image>が用意されているので、インポートして使います。

画像ソースを指定するプロパティは、srcではなくsourceです。

App.js
import React from 'react';
// Imageコンポーネントをインポート
import { Text, View, Image } from 'react-native';

const HelloWorldApp = () => {
    // 画像へのパスを定義
    let pic = {
        uri: 'https://i.picsum.photos/id/237/300/200.jpg'
    }

    return(
        <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
            <Image source={pic} style={{width: 300, height: 200}} />
        </View>
    )
}

export default HelloWorldApp

PropsとStateは通常のReactと同じ

同じ...だと思います。

まとめ

...更新中

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

reactのコンポーネントの書き方メモ

reactのコンポーネントの書き方

app.js

import React from 'react';
import logo from './logo.svg';
import './App.css';

import TestComponent from './components/TestComponent.js';
import ButtonComponent from './components/ButtonComponent.js';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload!!!!!.
        </p>
        <TestComponent text="うふふふ" />
        <TestComponent text="あははは" />
        <ButtonComponent text="あははは" bgColor="red" />
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

TestComponent.js

import React from "react";
import styled from "styled-components";


const TestComponent = props => {
  return (
    <Test>
      {props.text}
    </Test>
  );
};

const Test = styled.p`
  padding: 0 40px 40px 40px;
  font-size: 1.4rem;
`;

export default TestComponent;


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

Reactのルーティングライブラリの実装を読んだ&解説してみた

SPAアプリを開発する場合、どのpathにアクセスしてもサーバーからは同じHTML、jsファイルが返却されるので、クライアント側でルーティング機能を実装する必要があります。そしてReactなら、react-router-domといったライブラリを使って実装するのが一般的だと思います。

<BrowserRouter>
  <Switch>
    <Route exact path="">
      <Component />
    </Route>
  </Switch>
</BrowserRouter>

こんな感じで記述しておけば、書いたpathにアクセスすることで指定したコンポーネントが表示され、実質的にルーティングが可能になります。

では、ルーティングライブラリはどんな実装でこの機能を実現しているのか、というのが気になったので、ライブラリの中身をざっと読んでみました。

react-router-domの公式ドキュメントを読むと、上記の実装が最小限っぽかったので、この3つのコンポーネントの実装を追っていきます。

BrowserRouter.js

BrowserRouterの実装は非常にシンプルで、以下の数行だけです。

BrowserRouter.js
import { createBrowserHistory as createHistory } from "history";

/*
* The public API for a <Router> that uses HTML5 history.
*/
class BrowserRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;

historyを生成し、コンポーネントのpropsに乗せてreturnしているだけですね。createBrowserHistoryは何をしているかというと、ブラウザが提供するhistoryオブジェクトをラップし、APIとしてpushメソッドやreplaceメソッドを公開しています。Switchコンポーネント以下のコンポーネントで、propsをconsole.logなどしてみると、historyの中身(=createBrowserHistory)を確認できます。

Switch.js

次にSwitch.jsを見ていきます。ここで少しややこしいのは、react-routerはいくつかのパッケージに分かれていることです。react-router-config、react-router-dom、react-router-native、react-routerの4つがあり、Switch.jsの本体はreact-routerにあります。

react-router-domにもSwitch.jsファイルはありますが、react-router側のSwitch.jsを参照する内容が記述されているだけです。おそらく、コアの機能はreact-router側に集約されているのでしょう。BrowserRouter.jsはその名前の通り、Webブラウザ特有の実装だったため、react-router-domパッケージの中で実装されていました。

Switch.jsの処理も非常に簡潔で、コア部分は30行ほどからの実装になっています。(一部、コメントを削除して、解説コメントを追記しています)

Switch.js
/**
 * The public API for rendering the first <Route> that matches.
 */
class Switch extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Switch> outside a <Router>");

          // 現在のpathを取得
          const location = this.props.location || context.location;

          let element, match;

      // Switchコンポーネント以下に置かれているコンポーネントをループで処理
          React.Children.forEach(this.props.children, child => {
            if (match == null && React.isValidElement(child)) {
              element = child;

              // Routeコンポーネントからpathを取得
              const path = child.props.path || child.props.from;

              // 現在のpathと、Routeコンポーネントのpathが一致している場合、matchにそのpathを代入
              match = path
                ? matchPath(location.pathname, { ...child.props, path })
                : context.match;
            }
          });

          // 現在のpathとmatchするRouteコンポーネントがあった場合は、propsを追加してreturn
          return match
            ? React.cloneElement(element, { location, computedMatch: match })
            : null;
        }}
      </RouterContext.Consumer>
    );
  }
}

Switchコンポーネントの役割は、自身の配下にあるRouteコンポーネント(=children)をループで処理し、現在のpathとマッチするRouteコンポーネントがあるかを判定することです。

Switchコンポーネントの配下にいくつRouteコンポーネントが配置されていても、ループ処理の中でふるいにかけられ、レンダリング時点ではpathにマッチするコンポーネントのみが残ることになります。

Route.js

Route.jsもSwitch.jsと同じくreact-routerパッケージにあります。上2つに比べて少し長いので、前半、後半に分けて見ていきます。

Route.js
/**
 * The public API for matching a single path and rendering.
 */
class Route extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Route> outside a <Router>");

          // 現在のpathを取得
          const location = this.props.location || context.location;

          // computedMatchはSwitch.jsで追加されたprops
          const match = this.props.computedMatch
            ? this.props.computedMatch 
            : this.props.path
            ? matchPath(location.pathname, this.props)
            : context.match;

          const props = { ...context, location, match };

          let { children, component, render } = this.props;

          // Preact uses an empty array as children by
          // default, so use null if that's the case.
          if (Array.isArray(children) && children.length === 0) {
            children = null;
          }

        {/* 後半部分は省略 */}
        }}
      </RouterContext.Consumer>
    );
  }
}

前半部分は特別なことはしていないように見えます。Switch.jsで行なっていた、現在のpathとpropsのpathが一致するかどうかの判定をもう一度行なっているのがよくわかりませんが。。。

後半部分は以下です。

Route.js
/**
 * The public API for matching a single path and rendering.
 */
class Route extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
      {/* 前半部分は省略 */}

       return (
         <RouterContext.Provider value={props}>
           {props.match
             ? children
               ? typeof children === "function"
                 ? __DEV__
                   ? evalChildrenDev(children, props, this.props.path)
                   : children(props)
                 : children
               : component
               ? React.createElement(component, props)
               : render
               ? render(props)
               : null
             : typeof children === "function"
             ? __DEV__
               ? evalChildrenDev(children, props, this.props.path)
               : children(props)
             : null}
         </RouterContext.Provider>
      </RouterContext.Consumer>
    );
  }
}

三項演算子の連打でちょっとイヤになりますが、上から順に見ていきます。

まず、props.matchを評価します。matchしている(=現在のpathとRouteコンポーネントで指定しているpathが同じ)場合、Routeコンポーネントが子コンポーネントを持っているかを見ます。持っている場合、そのchildrenは関数かどうかをチェックします。関数ならば実行する必要があるためです。

Routeコンポーネントが子コンポーネントを持っていなかった場合、propsにcomponentがあるかどうかを見ていきます。あるならば、React.createElementでそのcomponentを描画します。component propsが記述されていなかった場合、renderの行に進むのですが、このrenderがどういった評価になるのかがちょっとわかりません。。。ただ、trueの場合はpropsをrenderし、falseの場合はnull、つまり何も描画しないという挙動になるようです。

ここで三項演算子の頭に戻って(props.match)、pathにmatchしていなかった場合は、childrenの存在を見て、かつそれが関数であるかどうかを確認します。関数であれば実行し、そうでなければnullに到達して一連の三項演算子の評価はおしまいです。

Switch.jsで現在のpathを見て描画すべきRouteコンポーネントを絞り込み、Route.jsはどのようにレンダリングするかを担当する、みたいな感じかと思われます。

Link.js

ここまでの3つのコンポーネントを配置すればルーティングのロジックは稼働しますが、このLink.jsも大切なコンポーネントです。Linkコンポーネントはページ間遷移をサーバーへのリクエストなしで実現するコンポーネントです。

HTMLのaタグをクリックするとイベントが発行され、サーバーへのリクエストが発生します。すると、リクエスト → レスポンス → レンダリングのような流れが再度実行され、これらは基本的に同期的に実行されるので、ユーザーはその時間分、待たされることになります(いわゆるSPAではないWebサイト)。

なので、SPAの内部ページ遷移にはaタグはそのままでは使えません。Linkコンポーネントは、aタグをラップし、サーバーへのリクエストイベントをキャンセルするコンポーネントになっています。その実装は以下です。

Link.js
/**
 * The public API for rendering a history-aware <a>.
 */
const Link = forwardRef(
  (
    {
      component = LinkAnchor,
      replace,
      to,
      innerRef, // TODO: deprecate
      ...rest
    },
    forwardedRef
  ) => {
    return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Link> outside a <Router>");

          const { history } = context;

          // 遷移先のpathを生成(/example)
          const location = normalizeToLocation(
            resolveToLocation(to, context.location),
            context.location
          );

          // 完全なURLを生成(https://example.com/example)
          const href = location ? history.createHref(location) : "";
          const props = {
            ...rest,
            href,
            navigate() {
              const location = resolveToLocation(to, context.location);
              const method = replace ? history.replace : history.push;

              // path遷移を実行
              method(location);
            }
          };

          // React 15 compat
          if (forwardRefShim !== forwardRef) {
            props.ref = forwardedRef || innerRef;
          } else {
            props.innerRef = innerRef;
          }

          return React.createElement(component, props);
        }}
      </RouterContext.Consumer>
    );
  }
);

これもざっくり見ていきます。

まずforwardRefというのが出てきます。これは子コンポーネント内のDOM要素にアクセスするための機能で、useRefに近しい感じです。useRefの場合、子コンポーネント内の特定のDOM要素にアクセスすることはできませんが(親から見ると内部実装は隠蔽されているので)、forwardRefの場合はそれが可能になるようです。

LinkコンポーネントでforwardRefが使われているのは、この機能を実現するためだと思いますが、自身でこの機能を使ったことはないので、ちょっと具体的なイメージは湧きづらいです。

navigate()の中身を見ると、history.replace、history.pushの記述があります。これらは少し上で受け取ったhistoryオブジェクト内に実装されているメソッドで、その途中にwindow.locationにページ履歴を追加する処理があります。これにより、SPAにおいてもブラウザの「戻る」や「進む」の機能が正しく動作するようになります。

ただ、最後まで読み進めてもaタグのイベントをキャンセルする処理はおろか、aタグすら出てきません。それらはどこで実装されているかというと、引数で渡されているLinkAnchorです。

LinkAnchorの記述はLinkの直上にあり、以下のようになっています。

Link.js
const LinkAnchor = forwardRef(
  (
    {
      innerRef, // TODO: deprecate
      navigate,
      onClick,
      ...rest
    },
    forwardedRef
  ) => {
    const { target } = rest;

    let props = {
      ...rest,
      onClick: event => {
        try {
          // LinkコンポーネントにonClick propsが記述されていた場合はそれを実行
          if (onClick) onClick(event);
        } catch (ex) {
          event.preventDefault();
          throw ex;
        }

        // aタグのイベント発行をキャンセルし、navigate()を実行
        if (
          !event.defaultPrevented && // onClick prevented default
          event.button === 0 && // ignore everything but left clicks
          (!target || target === "_self") && // let browser handle "target=_blank" etc.
          !isModifiedEvent(event) // ignore clicks with modifier keys
        ) {
          event.preventDefault();
          navigate();
        }
      }
    };

    // React 15 compat
    if (forwardRefShim !== forwardRef) {
      props.ref = forwardedRef || innerRef;
    } else {
      props.ref = innerRef;
    }

    // aタグに上記のpropsを付与してreturn
    /* eslint-disable-next-line jsx-a11y/anchor-has-content */
    return <a {...props} />;
  }
);

LinkAnchorには本命のaタグのイベント発行をキャンセルする処理が記述されています。

onClickで発行されるイベントをキャンセルすればOKなのですが、開発者が自身でLinkコンポーネントのonClickハンドラに何かしらの処理を追加しているかもしれません。それも全てキャンセルしてしまうのは使い勝手が悪いので、ユーザーが追加したonClickについては実行されるようになっています。

次のif文の中で4つの条件が評価されており、trueだった場合はイベント発行をキャンセル、つまりサーバーへのリクエストを送らないようにしています。代わりにnavigate()を実行していますが、この具体的な処理はLinkコンポーネント側に記述されていました。(const method = replace ? history.replace : history.push;の箇所)

これらの実装により、Linkコンポーネントは「サーバーへのリクエストが起こらないaタグ」のような振る舞いになっています。

まとめ

react-routerのリポジトリには他にもたくさんのファイルがあり、非常に多機能なライブラリなので、全部を見ていくのはさすがに難しかったのですが、上記の4コンポーネントについて知るだけでもSPAにおけるルーティングのロジックが掴め、非常によかったです。

おそらくですが、他のルーティングライブラリも似たような実装になっていると思うので、自身が使っているライブラリの中身を見てみると面白いかもしれません。

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

TypeScript + React + Sass + Babelを利用したWebpack環境構築

TypeScript + React + Sass + Babel利用したWebpack環境構築

最近, TypeScriptを勉強し始めたので
今回はTypeScript + React + Sass + Babelを利用した
Webpackの環境構築を行っていきたいと思います。

前提

  • ターミナルが利用できる
  • Node.jsを利用環境がある
  • npm, yarnの利用環境がある

環境

  • 2020/2/12 時点での最新モジュール
  • Node v11.10.1
  • ES6+
  • TypeScript v3.7.4
  • React.js v16.12.0
  • Webpack v4.41.2

ファイル構成

ファイル構成は以下の通りです。
スクリーンショット 2020-02-12 10.39.16.png

前準備

以下のpackage.jsonを作成し,各npmモジュールを
npm install もしくは, yarn add package.json
を利用してインストールしてください。

package.json
{
  "name": "",
  "version": "1.0.0",
  "description": "",
  "main": "Main.tsx",
  "scripts": {
    "build": "webpack",
    "start": "webpack-dev-server"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0",
    "@babel/core": "^7.7.4",
    "@babel/preset-env": "^7.7.4",
    "@babel/preset-react": "^7.7.4",
    "@types/react": "^16.9.17",
    "@types/react-dom": "^16.9.4",
    "@types/react-router-dom": "^5.1.3",
    "autoprefixer": "^9.7.4",
    "babel-loader": "^8.0.6",
    "babel-minify-webpack-plugin": "^0.3.1",
    "core-js": "3",
    "css-loader": "^3.2.0",
    "html-webpack-plugin": "^3.2.0",
    "mini-css-extract-plugin": "^0.9.0",
    "node-sass": "^4.13.0",
    "package.json": "^2.0.1",
    "postcss-loader": "^3.0.0",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-router-dom": "^5.1.2",
    "sass-loader": "^8.0.0",
    "ts-loader": "^6.2.1",
    "typescript": "^3.7.4"
  },
  "devDependencies": {
  }
}

これで環境構築に必要なモジュールが全てnode_modules配下に
インストールされました。

今回はメイン環境構築ですので, ささっとしたい方については
srcディレクトリ配下のファイルMain.tsxについて,
こちらを参照して各ディレクトリ上に作成してください。
https://github.com/olt556/react_hooks_ts_tmp

Webpackの設定

次にWebpackの設定ファイルとなる,
webpack.config.jsを作成していきます。

webpack.config.js
const path = require('path');
// Babelの機能のminifyを利用するため
const BabelMinifyPlugin = require("babel-minify-webpack-plugin");
// ビルドする際にHTMLも同時に出力するため
const HtmlWebpackPlugin = require('html-webpack-plugin');
// CSSをJSにバンドルせずに出力するため
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    mode: 'development',
    // pathの設定についてですがpathモジュールを使う必要は特にはありません。
    entry: path.resolve(__dirname, 'src/Main.tsx'),
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    devServer: {
        contentBase: path.resolve(__dirname, 'dist'),
        port: 8080,
        historyApiFallback: true, // これがないとルーティングできない
    },
    resolve: {
        modules: [path.resolve(__dirname, 'src'), path.resolve(__dirname, 'node_modules')],
        extensions: ['.ts', '.tsx', '.js']
    },
    module: {
        rules: [
            // scssのローダ設定
            {
                test: [/\.css$/, /\.scss$/],
                exclude: /node_modules/,
                loader: [MiniCssExtractPlugin.loader, 'css-loader?modules', 'postcss-loader', 'sass-loader'],
            },
            // js,ts,tsxのローダ設定
            {
                test: [/\.ts$/, /\.tsx$/, /\.js$/],
                loader: ['babel-loader', 'ts-loader'],
            },
        ],
    },
    plugins: [
        new BabelMinifyPlugin(),
        new HtmlWebpackPlugin({
            publicPath: 'dist', // ビルド後のHTMLの出力先
            filename: 'index.html', //出力するHTMLのファイル名
            template: 'src/html/index.html', //出力するためのHTMLのテンプレート
        }),
        new MiniCssExtractPlugin({
            publicPath: 'dist', // ビルド後のCSSの出力先
            filename: 'app.css', //出力するCSSのファイル名
        }),
    ],
}

webpack.config.jsは以上のようになります。
ちなみに, ローダの設定についてですが,
loader: ['babel-loader', 'ts-loader']
の場合, ts-loaderbabel-loaderの順に読み込まれます。

Babelの設定

Babelの設定は .babelrc に記述していきます。

{
    "presets": [
        ["@babel/preset-env", { //babelの設定
            "useBuiltIns": "usage",
            "corejs": 3 // polyfill用の設定
        }],
        "@babel/preset-react", // react用のbabelの設定
        ["minify",{}] // minifyの設定
    ]
}

TypeScriptの設定

続いてTypeScriptのトランスパイル(コンパイル)設定をtsconfig.json
記述してきます。

tsconfig.json
{
    "compilerOptions": {
        "sourceMap": true,
        "noImplicitAny": true,
        "allowJs": true,
        "strictNullChecks": true,
        "module": "ES6",
        "target": "es5",
        "jsx": "react"
    },
    "include": [
        "src"
    ],
    "exclude": [
        "node_modules"
    ],
}

各々の compilerOptions のオプションについては,
こちらを参照するといいかもしれません。
https://qiita.com/ryokkkke/items/390647a7c26933940470

AutopreFixerの設定

AutopreFixer はPostCSSの機能ですので,
利用できるようPostCSSの設定をpostcss.config.jsに記述していきます。

postcss.config.js
module.exports = {
    plugins: [
        require("autoprefixer")({
            grid: "autoplace",
            browsersList: ["ie >= 11"]
        })
    ],
};

ビルドについて

以上の設定ファイルを作成したのち,
npm run build を実行することで,
エントリーポイントであるMain.tsxsrc配下のファイル
読み込んでビルドを行い, dist以下に出力することができます。

また, npm run start でリアルタイムでビルドを行い,
出力されたファイルが読み込まれた, ローカルサーバが
htttp://localhost:8080 で起動します。

おわりに

間違えや質問などありましたら,
お気軽にコメントしていただけると幸いです。
今回はTypeScriptのテンプレートについては端折っているため,
そちらの記述方法などは,
GitHubを参照していただけたらと思います。

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

ReactでTypeScriptを使い始めたときに知りたかった@types/reactの用途別使い方

はじめに

reactの型定義ファイルには普段の開発では見かけることの少ない型が多く定義されています。
これらを使いこなすことでanyを使わずにきれいにコンポーネントを定義できるケースがあります。

今回は3つシチュエーションとそのときに使うと便利な型をサンプルコードと合わせて紹介します。

propsで要素を受け取って表示したいとき

  • propsの型にReact.ReactNodeを使う
Example1
interface Props1 {
  leftElement: React.ReactNode;
  rightElement: React.ReactNode;
}

const Example1:React.FC<Props1> = ({leftElement, rightElement}) => (
  <div>
    <div>{leftElement}</div>
    <div>{rightElement}</div>
  </div>
)

const Main:React.FC = () => (
  <Example1 leftElement={<p>left</p>} rightElement={'right'} />
)

React.ReactNodeReactElement | number | string | boolean | undefined | nullと定義されています。1

React.ReactElementはタグで囲んだ要素(例:<p>要素</p>)を示します。

Example1のコードではleftElementrightElementの型にReact.ReactNodeを指定することで、文字列や数値,<p>left</p>のようなReactElementを受け取ることができます。

タグ名をpropsで受け取って動的に変更したいとき

  • propsの型にReact.ElementTypeを使う
Example2
interface Props2 {
  tag?: React.ElementType;
}

const Example2: React.FC<Props2> = ({tag: Tag = 'div', children}) => (
  <Tag>{children}</Tag>
)

const Dummy: React.FC = () => (
  <p>dummy</p>
)

const Main2: React.FC = () => (
  <>
    <Example2>div</Example2> {/* <div>div</div> */}
    <Example2 tag={'p'}>p</Example2> {/* <p>p</p> */}
    <Example2 tag={Dummy}></Example2> {/* <Dummy></Dummy> => <p>dummy</p> */}
  </>
)

React.ElementType'span'や'div'などHTMLタグの文字列 | React.ComponentClass | React.FCと定義されています。1

React.ComponentClassReact.Componentを継承したクラスのことで、クラスコンポーネント形式で書かれたコンポーネントの型はReact.ComponentClassになります。

Example2のコードではpropsのtagにReact.ElementTypeを指定していおり、tagは'p'などのHTMLタグだけでなく独自に定義したコンポーネントを受け取ることもできます。

HTML属性と同じpropsを持ったコンポーネントを作りたいとき

  • React.DetailedHTMLProps<React.HogeHTMLAttributes<HTMLHogeElement>,HTMLHogeElement>でHTML属性の型を取得して、propsを定義する
Example3
type Props3 = { 
  name: string
} & React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>

const Example3: React.FC<Props3> = ({name, href, ...props}) => {
  return <a href={href} {...props}>{name}</a>
}

const Main3: React.FC = () => (
  <Example3 name={'main3'} href={'google.com'} />
)

React.DetailedHTMLPropsはジェネリックスに応じてHTML属性のそれぞれの型を示します。
見出し下に示した文のHoge型定義ファイルで定義された、それぞれのHTMLタグに応じた名前にすれば取得できます。
例えば、aタグであればAnchor、formタグであればFormになります。


  1. 説明しやすくするために型定義ファイルの定義とは違う型で説明しています 

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

CORS回避と、ReactからAPIへのPOSTリクエストが正常に処理されなかった話

目的

ReactからaxiosでPOSTのAPIを叩きたい。

環境

React:ローカルの開発環境 http://localhost:3000/、
API:PHPで書かれており、開発サーバにおいてある https://api.example.com/***
同じドメイン上に無い環境です。

状況

ReactからaxiosでAPIにPOSTリクエストを投げているが、CORSエラーで弾かれます。

また、axiosからPOSTリクエストが正常に投げられていない、もしくはPHPが正常にPOSTリクエストを受け取れていないような挙動をします。

こんな状況です。

私は基本的にPHPの開発がメインのため、Reactは初心者です。
もちろんaxiosも初めて使います。

javascriptでAjax処理は書いたことがあるものの、コピペで動けば良い程度の使い方しかしませんでした。

また、とりあえずの解決は出来たものの、各機能の仕様を理解した上での対策ではないため記事に誤りがある可能性しかありません。
ご理解ください。

エラー1 CORSエラー

Access to XMLHttpRequest at 'https://api.example.com/****/****' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

対処

ググった結果
・fetchを使った{ mode : cors }で回避できるらしい
・axiosの設定を変えると回避できるらしい
・API側のheaderを弄れば回避できるらしい
・Chromeの拡張機能でCORS回避できるらしい
という記事を多数見つけ、拡張機能をなるべく使いたくなかったため、とりあえずReact側のコードをいじってみることに。
が、この対処法が悪手でした。

この時はまだ、「CORSってなんぞや???」という状況です。

react
import axios from 'axios'

axios.defaults.baseURL = 'http://localhost:3000'
axios.defaults.headers.post['Content-Type'] = 'application/json;charset=utf-8'
axios.defaults.headers.post['Access-Control-Allow-Origin'] = 'https://api.example.com'

エラーが変わら無いため、API側に下記コードを追加します。

php
  header("Access-Control-Allow-Origin: *");
  header("Access-Control-Allow-Headers: X-Requested-With, Origin, X-Csrftoken, Content-Type, Accept");
  header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH, HEAD");

エラー2 別のCORSエラー

その後も コード変更 → エラー → コード変更 → エラーを何回か繰り返しました。

Access to XMLHttpRequest at 'https://api.example.com/****/****' from origin 'http://localhost:3000' has been blocked by CORS policy: Request header field access-control-allow-origin is not allowed by Access-Control-Allow-Headers in preflight response.

対処

結局あきらめて以下の拡張機能を導入することに・・・。

allow-cors-access-control

ちなみにこの機能を有効化した状態でTwitterを開くと「Twitterおかしい?」というツイートができなくなるので、拡張機能の有効範囲を[ http://localhost:3000/ ]だけに限定する等の対策が必要です。
image.png

エラー3 明らかにおかしい分岐に入る

拡張機能入れたしこれで大丈夫!!
と思いつつ試してみたのですが、いまだエラー解消ならず・・・

Access to XMLHttpRequest at 'https://api.example.com/****/****' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

ただ、エラー文に気になることが書かれています。

It does not have HTTP ok status.

APIがok status(200)を返していないということは、PHP側で弾いている・・・?

原因の切り分け

ステータスコードを調べるためにデベロッパーツールで見てみることに。

image.png

ただし、failedとしか書いておらず、詳細を見ても不明とのこと。

仕方ないのでAPI側で例外処理をスキップすることに。

php
  try{
    if(!is_post_request()){
      throw new Exception("ERROR_INVALID_METHOD");
    }
...
...
  }catch(Exception $e){
    switch($e->getMessage()){
      case "ERROR_INVALID_METHOD":
...
        // コメントアウト!!
    //    set_header( HEADER_TYPE_400 );
      break;
      case "ERROR_INVALID_PARAMETER":
...
        // コメントアウト!!
    //     set_header( HEADER_TYPE_400 );
      break;
...
      default:
        set_header( HEADER_TYPE_500 );
    }
  }

例外を投げている箇所を特定してみると・・・

php
  if(!is_post_request()){
    throw new Exception("ERROR_INVALID_METHOD");
  }

どうやらPOST以外のmethodでリクエストを受け取っており、そのせいで400 Bad Requestを返しているらしい。

・・・・!?!?!?
axiosはPOSTを投げていないのか!?!?

それともPHPが正常にPOSTを受け取れていないのか!?!?

ちなみにis_post_request()はこんな感じ。

php
  function is_post_request(): bool {
    if( $_SERVER['REQUEST_METHOD'] === "POST" ) {
      return true;
    }
    return false;
  }

対処

さて困ったどうしたものか・・・
「axios初めて使うしなあ・・・しっかりPOST送れてるかな~・・・」
「まさかPHPがPOSTの判別を出来ないわけないよな~・・・」
「まあPHP側は大丈夫だと思うけどな~・・・」
「axiosとかcorsとかわかんねーよ・・・」
などと考えつつとりあえず原因がPHPに無いことを確定させたかったため、printデバッグしてみることに。

php
  if(!is_post_request()){
// 2行追加
      var_dump($_SERVER);
      exit;
    throw new Exception("ERROR_INVALID_METHOD");
  }

とりあえずこれで$_SERVER['method']に"POST"が入ってることが確認できるだろうと思いながらReactのページを見てみると・・・
image.png

おお!正常にjsonが返ってきた!
良かったよかった、これで先に進める・・・ え????????

「いやいやexitで強制終了してるんだからjsonが帰ってくるのはおかしいでしょ」
「というか$_SERVERの中身が表示されてないんですけど・・・?」
「どうなってんのこれ!?!?」
大パニックです。。。
ということでもう一箇所にprintデバッグの追加

php
    var_dump($_SERVER);
    exit;
    if(!is_post_request()){
      var_dump($_SERVER);
      exit;
      throw new Exception("ERROR_JSON_INVALID_METHOD");
    }

ようやく$_SERVERの情報が取得できました。

  ["REQUEST_METHOD"]=>
  string(4) "POST"

ただ、正常にPOSTで受け取っているようです。

「もしやis_post_request()関数が正常に機能していない・・・?」
ということになり、

php
// もとに戻した
    if(!is_post_request()){
      var_dump($_SERVER);
      exit;
      throw new Exception("ERROR_JSON_INVALID_METHOD");
    }
php
  function is_post_request(): bool {
// printデバッグ
    die($_SERVER['REQUEST_METHOD']);
    if( $_SERVER['REQUEST_METHOD'] === "POST" ) {
      return true;
    }
    return false;
  }

そして出力された文字列は

"POST"

ほう・・・
「"POST" === "POST"がfalseになるん・・・??」
「POSTの判別方法間違えてる・・・?」
「そもそも文字列の比較ってこれでできるっけ・・・?」
「"POST" == "POST"のほうが良いのかな・・・」
「いや変わんないわ・・・」
「PHPのコンパイル時にdie, exitの挙動が不安定になる・・・?」
「そもそもPHPでfunctionって使っていいの・・・?」
再び大パニック

printデバッグの出力先をエラーログに

なにか良いデバッグ方法は・・・と考えているときに
「これ本当にPOST投げてる?」
「というかPOST以外のリクエストも同時に投げてる?」
という疑問が湧き上がり、printデバッグの出力先を変えてみることに。

php
  error_log($_SERVER['REQUEST_METHOD'],0);
  if(!is_post_request()){
    var_dump($_SERVER);
    exit;
    throw new Exception("ERROR_INVALID_METHOD");
  }
error_log
[Tue Feb 11 17:34:14.278789 2020] [:error] [pid 15147] [client **.**.**.**:38309] OPTIONS, referer: http://localhost:3000/
[Tue Feb 11 17:34:14.664012 2020] [:error] [pid 15147] [client **.**.**.**:38309] POST, referer: http://localhost:3000/

案の定1回のリクエストでOPTIONS、POSTの順番にリクエストが投げられてました。

ということで原因はエラー1の対処で追加したコードの部分でした。

詳しくは省きますが、preflight requestの仕組みが悪さをしていたようです。

貴重な休日を真夜中(2:30頃)まで潰したCORSを私は許さない。。。

あとがき

その後もCORS関連のコードいじってたら結局拡張機能も要らなくなったため消しちゃいました。

デバッグの手法を教えるのって難しいですよね。
echoやdieでのprintデバッグを知っていたとしても、error_logでOSのシスログにエラーを投げる方法は知らなかったりとか。
かといって便利だからとあれもこれも教えても(教えられても)覚えきれないから意味がないみたいな。
こればっかりは場数ですかね。

あと、Twitterを開けない時ってめちゃくちゃ辛くないですか?
「Twitter開けない」ってつぶやきたいのにつぶやけないこの気持ち。
それはまるで好きな子に正直に好きと言えない初恋のような甘酸さ。

最後まで見ていただきありがとうございました。
今恋してるよーって人はいいねお願いします。

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