React

tenpuに引き続きフロントエンドの刷新的なことをしている、っていう話です。

  • なぜやっているのか
  • どうやっているのか
  • 具体的にやっていること
  • 課題

なぜやっているのか

blade + javascript、しんどい?
→しんどい(フロントエンドとバックエンドの繋ぎ目部分をやることが多い私としては大体JavaScriptがしんどい)

そもそもこれってなんでトレンドじゃないんだっけ。っていう話は多分何億回もしてるのでカットでいいかな?

自社サービスだし今後大きくグロースしていくことが見込まれるよね。ってことで何かしらのフロントエンドライブラリを採用したかった。

どうやっているのか

とりあえず導入方法を考える。何もない砂漠に木は生えない。
メディアサイトという特性もあるのでSEOをおろそかにするのは良くない。
Tenpuはそもそもページ数が少なく、オーガニック流入による利用者数の増減に大きく影響があるか、と言われると、そうでもないだろう、的なアレがあった。あとそもそもVueである程度作られちゃっていた。

というわけでSEOに関係のないものを手軽に始めたい。
SSRをすればいい、プリレンダリング + spaにすればいい。←大変(だし、僕も理解不足(要するに誰もわからない))

ということで、SEOの影響のない箇所をReactコンポーネントとして埋め込んであげる、という方針になった。
具体的にはユーザーがログインした後の画面とか、コンテンツ内の、メインコンテンツじゃない箇所(ヘッダーとかサイドカラムとか)なら利用できる。

具体的にやっていること

結論から言うと、とりあえずマイページ配下だけ。
それ以外のヘッダーとかはユーザーからのインタラクションは基本的になく、簡単な表示しかしていないので今はいい( シラフジさんがReact書けるって言ってたからそれならやってもらおうかと思ったけどバニラJSが納品されたことで諦めたなんてことはない )

マイページの共通部分はbladeのlayoutとして実装。それ以外の箇所はReactのコンポーネントを埋め込み。
layoutをReactでやろうとすると全体で使いたい値の扱いが困りそうだった(Tenpuで困った)ので、枠はbladeにした。(この辺をやるにはフレームワークがあった方がいいな、と思う、し、 ReduxがしんどくてRecoil(他)、VuexがしんどくてPiniaという流れもあると思っているので安易に採用したくなかった)

実際のコードphp

@extends('mall.layouts.mypage')

@php
if (!isset($errors)) {
  $errors = collect([]);
}

$value = [
  'formUrl' => route('mall.mypage.profile.update'),
  'refusalUrl' => route('mall.mypage.refusal'),
  'customer' => $customer,
  'prefectures' => $prefectures,
  'sexOptions' => \Oms\Helpers\option($sexes),
  'yearRangeOptions' => \Oms\Helpers\option($yearRange),
  'mailMagazineOptions' => \Oms\Helpers\option($mailMagazine),
  'industryOptions' => \Oms\Helpers\option($industies),
  'occupationOptions' => \Oms\Helpers\option($occupations),
  'positionOptions' => \Oms\Helpers\option($positions),
  'scaleOptions' => \Oms\Helpers\option($scales),
  'errors' => $errors->toArray(),
];
@endphp

@section('content')
  <section class="p-mypage__content">
    <h2 class="p-mypage__subTitle">会員情報</h2>
    <p class="p-mypage__txt">
      下記項目にご入力ください。「*」は入力必須項目です。<br>
      入力後、一番下の「保存」ボタンをクリックしてください。
    </p>
    <!-- <p class="c-dataTable__cautionTxt -mt32">※必須項目の入力に誤りがあります。</p> -->
    <div class="p-mypage__dataTable">
      <div
        id="profile-edit-form"
        data-value="{{ json_encode($value) }}"
      >
      </div>
    </div>
  </section>
@endsection

app.tsx

import '@styles/app.scss'

import * as React from 'react'
import { createRoot } from 'react-dom/client'
import ProfileEditForm from "~/Front/Components/ProfileEditForm"
import EmailEditForm from "~/Front/Components/EmailEditForm"
import PasswordEditForm from "~/Front/Components/PasswordEditForm"

if (document.querySelector('#profile-edit-form')) {
  const container = document.querySelector('#profile-edit-form') as HTMLElement
  const root = createRoot(container)

  const {
    formUrl,
    refusalUrl,
    customer,
    prefectures,
    yearRangeOptions,
    sexOptions,
    industryOptions,
    occupationOptions,
    positionOptions,
    scaleOptions,
    mailMagazineOptions,
    errors,
  } = JSON.parse(container.dataset.value)

  root.render(
    <ProfileEditForm
      formUrl={formUrl}
      refusalUrl={refusalUrl}
      customer={customer}
      prefectures={prefectures}
      yearRangeOptions={yearRangeOptions}
      sexOptions={sexOptions}
      industryOptions={industryOptions}
      occupationOptions={occupationOptions}
      positionOptions={positionOptions}
      scaleOptions={scaleOptions}
      mailMagazineOptions={mailMagazineOptions}
      defaultErrors={errors}
    />
  )
}

工夫とか。

  • 値群をReactにどうやって渡すか考えた結果、 data- が一番スマートぽかったのでこうした。
    他にはhiddenとかで埋めてJSで document.querySelector('xxx') とかもあったけどhiddenがなんとなくダサい。
  • @json($value) だとシングルコートかダブルコートがうまいこといかないので json_encode

ビルド環境

大変かと思ってたけど楽だった。大きな理由は多分エントリポイントをユーザー画面と管理画面で分けてるので、リポジトリの中にはVueとReactがいるけど、jsのインポートで混ざってないから。
ほんとにこれだけ?って疑惑は今でも消えてないけどほんとにこれだけだった。Svelteはもう数行多く必要だったけど気のせい。webpack.config.js

  entry: {
    admin: './resources/assets/js/admin.ts', ←こっちはVueだけ
    app: './resources/assets/js/Front/app.tsx' ←こっちはReactだけ
  },

webpack.config.js

  module: {
    rules: [
      { test: /\.vue$/, use: [{ loader: 'vue-loader' }] },
      {
        test: /\.tsx?$/, ←ここを追記
        use: [
          {
            loader: 'ts-loader',
            options: {
              appendTsSuffixTo: [/\.vue$/],
              transpileOnly: true,
              configFile: path.join(rootDir, 'tsconfig.build.json')
            }
          }
        ]
      },
      { test: /\.css$/, use: cssLoaders },
      {

tsconfig.json

  "compilerOptions": {
    "jsx": "react" ←追記
  }

他にやったこと

webpackのバージョンアップの都合でいろんなものを外した。
外した1つにcompressionがあるんだけどこいつが遅くしてる原因だったっぽい。モールでは一旦外している。そのためブラウザに配信されるjs,cssは圧縮されていない。のでこいつはいつか直すんだけど、これを外したら初回、差分共にビルドがめちゃ早くなった。

課題

  • Reactを採用したがこれが結構大変かもしれない。でも使いたかったんだ…
    • Vue3はちょっと不安。Svelteとかは情報ソース的に苦しい。という理由でReactが選ばれたけど、大丈夫かな〜〜〜って不安。
  • 別に記述量は減っていない。なんならめっちゃ書く。(これは単にコンポーネントが洗練されていないだけ、という説)
  • cssとの相性。cssがコンポーネントに閉じていないため、ふとぶっ壊れる可能性がある。
    今はコンポーネントに対応するscssをimportして、コンポーネントとスタイルを1:1で対応づけるように だけ しているんだけど、実際にこれらはコンパイル時にminiCssExtractで1つのcssにバンドルされるため、このファイル構造に関わらずスタイルはグローバルに当たってしまう。なので名前付けには気をつけなければいけない。
    FLOCSSなのか、BEMなのか、よくわからんくなっているため、困っている。

tsx

import React from 'react'

import '@styles/components/AppInput.scss' ←cssのインポート

interface InputProps {
  name: string
  id?: string
  type: 'text' | 'email' | 'checkbox' | 'password'
  placeholder: string
  register: Function
  rules: {[key: string]: string}
  defaultChecked?: boolean
  hasError: boolean
}

const AppInput: React.FC<InputProps> = ({
  id = '',
  name = '',
  type,
  placeholder = '',
  register = () => {},
  rules = {},
  defaultChecked = false,
  hasError = false,
}) => (
  <input
    className={`
      app-input
      ${hasError && '-error'}
    `}
    id={id}
    type={type}
    placeholder={placeholder}
    defalutvalue=''
    defaultChecked={defaultChecked}
    {...register(name, rules)}
  />
)

export default AppInput

まとめと感想

正直開発体験として、めっちゃ楽〜〜〜みたいなものは今の所感じられていないけど、ReactはPHPよりもエコシステムが充実しているし、TSの恩恵もうっすら感じられるのでバニラJSで書くよりはいいだろう、と思う。
学びは割と多いし、やっていて楽しいのでこれはこれで悪くないと勝手に思っている。

コメント

タイトルとURLをコピーしました