20200217のReactに関する記事は7件です。

SPAの知識ゼロの状態から、React+Reduxの開発環境構築までにやったこと

Webアプリケーションを開発する際に、「今ならSPA(Single Page Application)っしょ」となり、フレームワークの選定からやらせてもらう機会があったので、そのときにやったことを共有します。

前提

本記事作成時の筆者のスキル

  • Webアプリケーションは作ったことがあるが、言語はJava
  • SPA is 何?
  • Javascriptはかじった程度の知識

フレームワークの選定

Googleトレンドや、実績からSPAのフレームワークを以下に絞りました。

  • React
  • AngularJS
  • Vue.js

その時人気だったReactにしておけばいいだろと、Reactにしました。
※近年、Vue.jsが勢力を伸ばしてきているらしいです。

React + α

Reactは様々なライブラリと組み合わせるのが普通のようです。
npm installでどんどんライブラリを追加していくことになりますが、ここでは開発手法に大きく関わるライブラリだけ。

執筆時点での最新バージョンはそれぞれ以下の通りです。あまり古くなっているようなら、参考にならないかもしれません。

プロダクト version
React 16.12.0
Redux 4.0.5
TypeScript 3.7.5
redux-saga 1.1.3
Material-UI 4.9.2
react-intl 3.12.0

エディター

これも流行りなのでVSCodeを使用しました。
結果的に、nodeやgitのコマンドもターミナルのペインで実行できるので、かなり使いやすかったです。

設定と拡張機能をチーム内で共有する

エディタの設定は、プロジェクト直下の.vscode/settings.jsonというファイルで共有できます。
拡張機能は、.vscode/extensions.jsonというファイルを置くことで、VSCode起動時にポップアップが出るようになります。
フォーマッター、保存時の自動フォーマットなどの設定は共有しておくと便利です。

プロジェクトの作成

create-react-appというコマンドを使います。これを使うと起動スクリプトやwebpackのコンフィグなどが隠された状態で、カスタマイズできないので、すぐにejectというコマンドで素のnodeで扱えるようにします。

npx create-react-app my-app
cd my-app
npm run eject

プロジェクトの設定

ディレクトリ構成を考える

こんな感じになりしました。「これが正解!」というパターンは無いようなので、あくまで一例として。

ディレクトリ構成(クリックで開く)
<root>          
├─public          画像ファイルなど。srcから見たとき、"/"がpublic直下
├─build           ビルドされたファイルが作成される先
├─config          create-react-appで勝手の作成される
├─scripts         create-react-appで勝手の作成される+自作のnpmスクリプトを作る場合はここ
├─node_modules          
└─src             src以外のファイルはほとんど触らない    
  ├─i18n          メッセージのyamlと、それを管理するJS
  ├─modules       actionとreducerをまとめたもの
  ├─containers    1画面1コンテナという考え方で、コンテナコンポーネントを作成する。
  ├─comopnents    コンポーネント。画面ごとにサブディレクトリを切る      
  ├─sagas         非同期処理を扱う処理 
  ├─scss          .scssファイル
  └─utils         共通で使う処理。Validatorなど

importを絶対パス指定でできるようにする

これデフォルトじゃないんかい。。。と思いながら設定しました。
方法はいくつかあるようですが、TypeScriptを使っているのでtsconfigで設定できる方法を採用しました。
これをやらないと、"../../../../"地獄になります。

いつもの
npm i --save tsconfig-paths
tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "src"
     ~~~後略~~~
}

メッセージ管理方法を考える

yamlでメッセージ管理できるようにする

yaml-flat-loaderというライブラリを使用して、yamlをjsonとして読み込めるようにします。

いつもの
npm i --save-dev yaml-flat-loader
webpackconfig.jsのrulesに追記
{
  test: /\.yml$/,
  use: [{ loader: 'json-loader' }, { loader: 'yaml-flat-loader' }]
},

メッセージのキーを補完できるようにする

yamlでメッセージ管理する弊害として、コピペがし難いこともあり、補完できるように一手間加えました。
メッセージのキーをオブジェクトとして参照できるようにすることで、メッセージキーの変更やキーの指定ミスがコンパイルエラーになるというメリットもあります。
react-intlの正しい使い方をしているなら、こちらの記事が参考になりますが、JSでデフォルトメッセージを定義して、各言語はyamlというのは面倒です。
メッセージはyamlファイルだけで完結したいので、yamlファイルのキーをjsonにします。

スクリプトを作成し、messageKeysというオブジェクトが各ファイルから参照できるようにします。
ja.ymlに存在するキーをJSONにしています。

スクリプト詳細(クリックで開く)
まずは必要ライブラリのインストール
npm install --save-dev js-yaml
scripts/messageGen.js
'use strict';

var yaml = require('js-yaml');
var fs = require('fs');

// Get document, or throw exception on error
try {
  let doc = yaml.safeLoad(fs.readFileSync('src/i18n/ja.yml', 'utf8'));
  let str = "export const messageKeys =";
  str += JSON.stringify(keyToValue("", doc));
  str += ';\n';
  str += "export default messageKeys;"
  fs.writeFile("src/i18n/messageKeys.tsx", str, (err) => {
    if (err) {
      throw err;
    }
  });
} catch (e) {
  console.log(e);
}

function keyToValue(parentKey, param) {
  let ret = param;
  for (let [key, value] of Object.entries(param)) {
    if (typeof value === 'object') {
        keyToValue(parentKey + key.toString() + ".", value);
    } else if (typeof value === 'string') {
      ret[key] = parentKey + key;
    }
  }
  return ret;
}
package.json(抜粋)
"scripts": {
-  "start": "node scripts/start.js",
+  "start": "node scripts/messageGen.js && node scripts/start.js",
+  "message": "node scripts/messageGen.js"
},

使用例
import messageKeys from "src/i18n/messageKeys";
~~~中略(コンポーネント内)~~~
const intl = useIntl();
intl.formattedMassage({ id: messageKeys.some.message.id });

"npm run message"でsrc/i18n/ja.ymlに存在するキーをjsonにしてくれます。
※エディタ上で補完できるようにするには、messageKeys.jsを一度開く必要がありました。

スタイリング方法を考える

こちらの記事を参考に、"1. クラス名によるスタイリング"と"3. CSS Modules"で悩みましたが、CSS(SCSS)によるスタイリングを採用しました。
理由は、

  • 業務ロジックやイベント処理を記述しているコンポーネントファイルに、見た目のことまで書きたくない
  • 普通のSCSSの方が書ける人が多い
  • ぶっちゃけCSS Moduleよくわからなかった

あたりです。
ただし、これには以下のデメリットがあります。

  • 普通にscssをimportすると、Reactはstyleタグを生成するので影響範囲は全体ということになる
  • 上記動作により、複数回importされると無駄なstyleタグが増えることになる

よって、コンポーネントのrootとなる要素にclassを設定し、その下にネストしたSCSSを書くようにしました。
(結局は開発者の運用努力なので、改善すべき部分ではあります。)

結び

まだまだ色々なことをやった(ような気がします)が、以上が開発開始時の"進め方"を決める段階で時間をかけて考えた部分になります。
これからReactの開発を始める際の参考になれば幸いです。

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

【React】フォーカスアウトするとカンマ区切りで表示される入力欄

経緯

  • 数値を入力する項目がある
  • 桁が大きくなると額がわからなくなる
  • だからカンマ区切りで表示したい
  • それを実現可能な要素はHTMLには無い
    • input=text だと数値以外を入力できる
    • input=number だとカンマを表示できない
  • じゃあ作ろう

仕様

  • input=textをベースとする
  • 入力値は右寄せで表示する
  • バリデーション機能を追加する
    • 正の数、および負の数を入力可とする
    • マイナスは先頭のみ入力可とする
    • 小数点は一度のみ入力可とする
    • 小数点の前には必ず数値の入力があるものとする
    • 末尾が小数点で終わる場合を許容する
  • 未入力、及びバリデーション通過時の背景色を #FFF とする
  • バリデーション不通過時の背景色を #FFBEDA とする
  • 入力欄にフォーカス時に入力した文字列をそのまま表示する
  • 入力欄からフォーカスアウト時にバリデーションを通過した場合はカンマ区切りの文字列を表示する
  • 入力欄からフォーカスアウト時にバリデーションが不通過となる場合は入力した文字列をそのまま表示する
import React, { useState } from 'react';

const InputText = ({ ...rest }) => {
  const [value, setValue] = useState('');
  const [isFocus, setFocus] = useState(false);
  const handleFocus = () => setFocus(true);
  const handleBlur = () => setFocus(false);
  const handleChange = e => setValue(e.target.value);
  const isBlank = value === '';
  const isValid = /^[-]?(\d+)[.]?(\d+)?$/.test(value);
  const displayValue = (() => {
    if (isFocus || !isValid) {
      return value;
    }
    if (isValid) {
      return (+value).toLocaleString();
    }
    return '';
  })();
  const displayStyle = {
    textAlign: 'right',
    backgroundColor: isValid || isBlank ? '#FFF' : '#FFBEDA'
  }

  return (
    <input
      {...rest}
      type="text"
      onFocus={handleFocus}
      onBlur={handleBlur}
      onChange={handleChange}
      value={displayValue}
      style={displayStyle}
    />
  );
}

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

React/Vueを利用したサービス

概要

SPAの代表的なフレームワークであるReactとVue(+Nuxt.js)を利用したサービスについて。

サービス一覧(React)

Vue.js

Vue.jsを導入している国内の企業・サービス一覧
- Nintendo
- https://my.nintendo.com/
- ZOZOテクノロジーズ
- https://speakerdeck.com/amatsukiku/frontend-architecture-design-of-zozo
- 一休com
- 一休.comレストランのスマートフォン検索ページがSPAになりました
- https://user-first.ikyu.co.jp/entry/2018/10/09/080000
- Retty
- Webサービスを支えるユーザログ基盤開発@Retty
- https://engineer.retty.me/entry/2018/12/01/120019
- DMM
- DMM動画サービスの問題を解決しようとしている話(コンポーネント編)
- https://inside.dmm.com/entry/2018/07/12/components
- Gunosy
- 社内管理画面を Vue + Go で作る
- https://tech.gunosy.io/entry/admin-vue-go
- マンガZERO
- https://blog.nagisa-inc.jp/archives/2980
- ITプロパートナーズ
- サービスの管理画面で Vue + element.ui を活用する(table編)
- https://tech.itpropartners.jp/entry/2018/12/04/132144
- M3
- 「レガシーアプリケーションのリニューアルにNuxt.jsで戦う」というタイトルでVue Fes Japan 2018 Reject Conferenceに登壇してきました
- https://suzan2go.hatenablog.com/entry/2018/11/10/225810
- DeNA
- アバター着せ替えアプリ開発におけるフロントエンド技術(Vue.js活用事例) #denatechcon
- https://www.slideshare.net/dena_tech/vuejs-denatechcon-72603570
- Codeal
- コデアルリニューアルと技術的なトピック
- https://www.codeal.work/contents/archives/6448
- CoupLink
- 弊社マッチングアプリ「CoupLink」をVue.jsでSPA化しました
- https://tech.linkbal.co.jp/4834/
- ALIS
- 【ALISのシステム】フロントエンドアーキテクチャ:その1
- https://alis.to/AB2/articles/34ZkxZ1pwylQ

Nuxt.js

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

Reactで改行コード\nの入っているテキストを改行したまま表示する方法

テキストエリア等で改行されたテキストを改行したまま表示したい時がある。

よく見るのはテキスト中の改行コード\n<br />に置換する方法だと思う。

class Message extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      message: 'これは、\nテキスト\nです。',
    }
  }

  render() {
    return (
      <div
        dangerouslySetInnerHTML={{
          __html: this.state.message.replace(/\n/g, '<br />')
        }}
      />
    )
  }
}

しかし、この方法だとXSSの脆弱性を生み出す可能性があるためできるだけ避けたい。

よって、改行タグ<br />をHTMLとして置換するのではなく、JSXの要素として置換できないか考えた。

結果として、以下のような構成になった。

class Message extends React.Component {
  // ...

  render() {
    return (
      <div>{
        this.state.message.split('\n').map((str, index) => (
          <React.Fragment key={index}>{str}<br /></React.Fragment>
        ))
      }</div>
    )
  }
}

テキスト中の改行コードで分割して作られた配列でmap関数で繰り返し処理をして、JSX要素を戻り値とした。
この方法であれば、悪意のあるユーザーがテキストにhtml要素を入力した場合にも文字列として扱われるため、XSSの脆弱性は抑えられる。

なお、map関数内の<React.Fragment />とは、戻り値が1つの要素しか指定でいないReactの制約を外してくれる便利な構文である。

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

Reactで画像をlazyloadしつつpreloadするコンポーネント

FirstViewにはいらんけど、スクロールして登場するまでにはなるべく存在してほしいみたいな画像があったので作ってみました。
正直こんな雑なやり方で合ってるのか分からんのでもっといい方法知ってる人がいたら教えてください頼みます。

import * as React from 'react';

type Props = {
  imgUrl: string;
  alt: string;
};

export default function LazyAndPreloadImage(props: Props) {
  const [loaded, setLoaded] = React.useState(false);

  React.useEffect(() => {
    // preload しておく
    const img = new Image();
    img.src = props.imgUrl;

    // 読み込めたら img タグ描画する
    img.onload = () => {
      setLoaded(true);
    };
  }, []);

  if (loaded) {
    return <img src={props.path} alt={props.alt} />;
  }

  // 雑に空divにしてるけど思い思いのplaceholder入れてください
  return <div></div>;
}

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

TypeScriptを使ってReactのFunctionalComponentに型安全にpropsを渡す

import React, { FunctionComponent } from 'react';

type Props = { title: string }
const Title: FunctionComponent<Props> = props => {
  return <>
    <h1>{props.title}</h1>
  </>
}

または、以下のようにtypeの定義省略することも出来る。

import React, { FunctionComponent } from 'react';

const Title: FunctionComponent<{ title: string }> = props => {
  return <>
    <h1>{props.title}</h1>
  </>
}

使用する時はこのように使う。

function App() {
  return <>
    <Title title="Hello World" />
  </>
}

関連記事

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

react-fetching-library で Suspense 使ってみた。

最近ReactでAPIからデータフェッチする際にuseEffectを使っていましたが、
これを読んだあと、これからはSuspenseを積極的に使っていこうと思い立ちました。
https://overreacted.io/ja/a-complete-guide-to-useeffect/

いい感じに使えるパッケージはないものかと探していると、
4日前(投稿日現在)に出来立てホヤホヤのreact-fetching-libraryを発見。
https://www.npmjs.com/package/react-fetching-library
Doc: https://marcin-piela.github.io/react-fetching-library/#/?id=usesuspensequery
早速使ってみよう。

Suspense使用前

ポートフォリオに投稿したQiitaの記事を載せるために、こんな感じで実装していました。
レンダリングしている内容は省略します?‍♂️

App.js
import React from 'react'
import Writing from 'sections/Writing'

export default () => {
  return (
    <div>
      <Writing />
    </div>
  )
}
sections/Writing.js
import React, { useState, useEffect } from 'react'
import { fetchQiitaArticles } from 'lib/api'

export default () => {
  const [articles, setArticles] = useState([])

  useEffect(() => {
    const handleGetArticles = response => {
      setArticles(response)
    }
    fetchQiitaArticles({ handleGetArticles })
  }, [])

  return (
    // fetchしたarticlesを使ってレンダリング
  )
}
lib/api.js
export const fetchQiitaArticles = async props => {
  const { handleGetArticles } = props
  await fetchGet({
    auth: process.env.REACT_APP_QIITA_AUTH,
    url: 'https://qiita.com/api/v2/authenticated_user/items',
    successAction: handleGetArticles,
  })
}

const fetchGet = async props => {
  const { auth, url, successAction, failureAction } = props

  return await fetch(url, {
    headers: {
      Authorization: `Bearer ${auth}`,
    },
  })
    .then(response => response.json())
    .then(responseJson => {
      // console.log(responseJson)
      if (successAction) {
        successAction(responseJson)
      }
    })
    .catch(error => {
      // console.error(error)
      if (failureAction) {
        failureAction()
      }
    })
}

セットアップ

まずは react-fetching-libraryをインストール。

$ yarn add react-fetching-library

続いてClientの作成。とりあえず今回はオプションなし。

api/Client.js
import { createClient } from 'react-fetching-library'
export const Client = createClient()

そして<ClientContextProvider>でAppを囲う。

App.js
import React from 'react'
import { ClientContextProvider } from 'react-fetching-library'
import { Client } from 'api/Client'
import Writing from 'sections/Writing'

export default () => {
  return (
    <ClientContextProvider client={Client}>
      <Writing />
    </ClientContextProvider>
  )
}

これで、react-fetching-libraryの機能が使用可能になりました!

fetchの用意

api/fetchMyQiitaArticles.js
export const fetchMyQiitaArticles = {
  method: 'GET',
  endpoint: 'https://qiita.com/api/v2/authenticated_user/items',
  headers: {
    Authorization: `Bearer ${process.env.REACT_APP_QIITA_AUTH}`,
  },
}

コンポーネントのラッパー

useEffectを廃止してWritingContainerでラップします。
その際、useSuspenseQueryを使ってデータフェッチを行うようにします。

container/WiritingContainer.js
import React from 'react'
import { useSuspenseQuery } from 'react-fetching-library'
import { fetchMyQiitaArticles } from 'api/fetchMyQiitaArticles'
import Writing from 'sections/Writing'

export default () => {
  const { payload, error, query } = useSuspenseQuery(fetchMyQiitaArticles)

  return <Writing error={error} articles={payload} />
}
sections/Writing.js
import React, { useState, useEffect } from 'react'
import { fetchQiitaArticles } from 'lib/api'


++  export default props => {
++    const { articles, error } = props
--  export default () => {
--    const [articles, setArticles] = useState([])
--    useEffect(() => {
--      const handleGetArticles = response => {
--        setArticles(response)
--      }
--      fetchQiitaArticles({ handleGetArticles })
--    }, [])

  return (
    // fetchしたarticlesを使ってレンダリング
  )
}

Suspenseを使う!

App.js
++ import React, { Suspense } from 'react'
-- import React from 'react'
import { ClientContextProvider } from 'react-fetching-library'
import { Client } from 'api/Client'
++ import WritingContainer from 'container/WritingContainer'
-- import Writing from 'sections/Writing'

export default () => {
  return (
    <ClientContextProvider client={Client}>
++    <Suspense fallback={<p>Loading ...</p>}>
++      <WritingContainer />
++    </Suspense>
--    <Writing />
    </ClientContextProvider>
  )
}

かなりアバウトになりましたが、
これでSuspenseを使ったデータフェッチが可能になりました。

おわりに

とりあえず自分はuseEffectを使わずにデータフェッチができるようになりました。
Suspense界隈のリリース情報はしっかり追っていきたい。

あとreact-fetching-libraryの公式Docがtsで説明されてた。
これからちゃんとtsも勉強したいと思います!

参考

https://marcin-piela.github.io/react-fetching-library/#/?id=usage

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