普段Vue.jsしか使わない人が初めてReactやってみた(カレンダーアプリ作成)

はじめに

こんにちは、CCIのryo3cciです。

自分は普段Vue.jsを使ってアプリケーションのUIと日々格闘しているのですが、 世の中的にはReactの方がシェアが高い(って昔聞いた気がする)ので、 Reactってどんなもんだろう?と常々思っていました。

ということで、今回Reactを触ってみて、Vue.jsと比べてどうなのか自分なりに体験してみたい、 というのが本記事の主旨です。

作ったもの

今回作ったものは下記のようなブラウザ上のカレンダーアプリ(はりぼて)です。
日付部分をクリックするとモーダルが開き、予定の登録ができるようになっています。
※ただし、裏側のDBやBackendを用意していないので、あくまで画面に反映されるだけです。

初期表示画面(月カレンダー表示)

日選択モーダル

↓2023/02/22の日セルをクリックすると下記のようなモーダルが開き、タイトルと終了日を選択できる

登録後の予定

↓タイトルと終了日を登録すると、モーダルが閉じて親画面にデータ登録される

環境情報

開発基盤
バージョン 種別
Next.js 13.1.6 フロントエンドフレームワーク
Node.js 18.14.1 JS実行環境
React 18.2.0 開発JSライブラリ
他利用nodeパッケージ
パッケージ バージョン 用途
@fullcalendar/react 13.1.6 カレンダー表示
bootstrap 5.2.3 モーダルなどのUI部品
react-datepicker 4.10.0 日付選択
react-bootstrap 13.1.6 モーダルなどのUI部品利用

作ったコード

index.js(TOP画面のカレンダー表示)
import React, { useState } from 'react'
import FullCalendar from '@fullcalendar/react'
import dayGridPlugin from '@fullcalendar/daygrid'
import interactionPlugin from '@fullcalendar/interaction'
import { format } from 'date-fns'
import ModalDayMemo from '/src/component/ModalDayMemo'

export default function IndexPage() {
  // full calendar options
  const headerOptions = {
    start: 'title',
    center: '',
    end: 'prevYear, prev, today, next, nextYear',
  }
  const buttonTextOptions = {
    today: 'Today',
    month: 'month',
    week: 'week',
    day: 'day',
    list: 'list',
  }

  // state
  const [show, setShow] = useState(false)
  const [selectDate, setSelectDate] = useState('')
  const [events, setEvents] = useState([
    { title: 'event 1', start: '2023-02-21' },
    { title: 'event 2', start: '2023-02-23', end: '2023-02-28' },
  ])
  const closeModal = () => setShow(false)
  const showModal = () => setShow(true)

  /**
   * 日付セルをクリックしたときの処理
   * @param {Object} date 選択した日付データ
   */
  function handleDateClick(date) {
    setSelectDate(date.dateStr)
    showModal()
  }

  /**
   * 予定登録処理
   * @param {String} title 予定タイトル
   * @param {Date} endDate 予定終了日
   */
  function registerSchedule(title, endDate) {
    setEvents([
      ...events,
      {
        title: title,
        start: selectDate,
        end: format(endDate, 'yyyy-MM-dd'),
      },
    ])
    closeModal()
  }

  return (
    <div>
      <FullCalendar
        buttonText={buttonTextOptions}
        events={events}
        headerToolbar={headerOptions}
        initialView="dayGridMonth"
        plugins={[dayGridPlugin, interactionPlugin]}
        dateClick={handleDateClick}
      />
      <ModalDayMemo
        show={show}
        title={selectDate}
        closeModal={closeModal}
        registerSchedule={registerSchedule}
      />
    </div>
  )
}
ModalDayMemo.js(日選択時のモーダル表示)

まだメモ機能ないのに命名がMemoなのはご愛嬌

import React, { useState } from 'react'
import { Button, Modal } from 'react-bootstrap'
import DatePicker, { registerLocale } from 'react-datepicker'
import ja from 'date-fns/locale/ja'
import 'react-datepicker/dist/react-datepicker.css'

function ModalDayMemo(props) {
  // state
  const [endDate, setEndDate] = useState(new Date())
  const [scheduleTitle, setScheduleTitle] = useState('')

  // 日本語に直すためのパラメータを作成
  registerLocale('ja', ja)

  /**
   * 予定登録処理
   */
  function registerSchedule() {
    // 親に予定情報を送る
    props.registerSchedule(scheduleTitle, endDate)
  }

  return (
    <Modal show={props.show} onHide={props.closeModal}>
      <Modal.Header closeButton>
        <Modal.Title>{props.title}</Modal.Title>
      </Modal.Header>
      <Modal.Body>
        <div
          style={{
            display: 'flex',
            alignItems: 'center',
            paddingBottom: '0.5rem',
          }}
        >
          <label style={{ width: '5rem' }}>タイトル</label>
          <input
            type="text"
            onChange={(e) => setScheduleTitle(e.target.value)}
          ></input>
        </div>
        <div
          style={{
            display: 'flex',
            alignItems: 'center',
            paddingBottom: '0.5rem',
          }}
        >
          <label style={{ width: '5rem' }}>終了日</label>
          <DatePicker
            selected={endDate}
            onChange={(date) => setEndDate(date)}
            dateFormat="yyyy/MM/dd"
            locale="ja"
            style={{ width: '8rem' }}
          />
        </div>
        <div
          style={{
            display: 'flex',
            alignItems: 'start',
            paddingBottom: '0.5rem',
          }}
        >
          <label style={{ width: '5rem' }}>メモ</label>
          <textarea style={{ width: '20rem' }} />
        </div>
      </Modal.Body>
      <Modal.Footer>
        <Button variant="primary" onClick={registerSchedule}>
          予定を追加
        </Button>
      </Modal.Footer>
    </Modal>
  )
}

export default ModalDayMemo

やってみた感想

実際に作ってみた(かなり簡素な出来ですが...)ところ、下記でVue.jsとの違いを実感しました。

①データバインドした値更新とそのデータの画面反映

今回作ったカレンダーアプリでは、登録した予定の管理と表示にはeventsと命名した配列を用いています。

const [events, setEvents] = useState([
  { title: 'event 1', start: '2023-02-21' },
  { title: 'event 2', start: '2023-02-23', end: '2023-02-28' },
])

Reactではバインドするstateはセッターを使ってデータを書き換えないと画面へ反映されないので、 下記のように、すでに登録されているeventsを展開して上書きするようにしてsetEventで書き換えています。

setEvents([
  ...events,
  {...},
])

一方、Vue.jsで配列をバインドする場合は、Array.push()で値の追加反映ができるので、 ここはVue.jsの方がわかりやすいと思いました。

events.push({...})
②state更新のルール

親画面が子コンポーネントのstateを変更したいというパターンの時に、 Reactで親から子のstateを変更する実装はアンチパターンに相当するというのが寝耳に水でした。

例えば、今回作ったものでいえば、モーダルの開閉を管理しているshowという命名のstateを、 ModalDayMemo.js側に持つように設計するとダメなようです。

Vue.jsでは、子コンポーネントにRef指定をして、子コンポーネントの関数から、 子コンポーネント内でしか使わないデータ(stateに相当)を親側から書き換えるなんて、 サラッとやってました気がしますが、これはReactでは行ってはいけないようです。

まとめ

Vue.jsばっかり触っている自分が、今回初めてReactを触ってみましたが、 印象としてはそれほど抵抗なく実装できたかと思います。

たしかに当初想定していた通り、双方向バインディングが利用できないことで、 多少Vue.jsより実装がメンドいと感じたことはありましたが、 Vue.jsを単方向バインディングだけで実装するとこんな感じかなといった気分で、 そこまで実装に差は感じなかったです。

ただ、あくまで今回は超小規模なアプリの実装なので、これからきちっとした状態管理や、 コンポーネント設計を行うと、差が現れてくるのかもしれません。
その辺は今後改修していくところで発見があればいいなと思っています。

余談

余談になりますが、Reactのアプリを作るときに、 公式から出ているcreate-react-appをパッケージを使って環境の初期構築をしたところ、 アプリを動かす環境を作成するのがめっちゃ楽でよかったです...!
これがあるおかげで環境構築周りのステップをまるまる省略できたので、 初めてUI開発しよう!という人にはReactの方がとっつきやすいんだろうなぁと思いました。