20200713のReactに関する記事は9件です。

Rails & React & Webpacker & MySQL環境構築マニュアル

突然ですが、環境構築って毎朝髭を剃るのと同じぐらい面倒で苦手です。
この記事をご覧になっているということは、少なからずあなたも環境構築に苦手意識があるのではないでしょうか。

社内のメンバーからも「環境構築はコンビニでたむろするヤンキーぐらい苦手」という声を良く耳にします。
私は思います、この環境構築という最初のハードルが、クリエイティブな行動を阻害していると!
環境構築の手間さえ省ければ、きっとこの世の中にはもっと多くのサービスが創出されると確信しています!
そこで今回は、"RailsをAPIサーバーとして利用"し、"Reactで描画を行う"サービスをつくりまくるための環境構築マニュアルを公開します!

以下すべてに当てはまる人が本記事の対象読者です

  • なんかオシャレっぽいからMac使ってます!
  • プログラミングスクール卒業したから個人アプリつくりたいぜ!
  • React.jsっていうJavascriptのモダンなフレームワークを身につけて周りと差を付けたいぜ!
  • react-railsとかのGemを使わない方法でRailsとReact間のやりとりを疎結合にしたい!
  • Docker?なにそれ美味しいの?(本記事ではDockerの解説はしません)

0. 事前インストール

名前 説明
Ruby いわずもがな
Rails いわずもがな
MySQL いわずもがな
brew パッケージ管理 (主にサーバー側)
yarn パッケージ管理 (主にフロント側)

1. Railsアプリの作成

rails new アプリ名 -–skip-turbolinks --webpack=react --database=mysql --api

人生に何回この『rails new』コマンドを打ったかでRailsエンジニアとしての価値が決まると、まことしやかに噂される。
ちなみにオプションは必要であれば書き換えてOKです

2. Webpackerのインストール

Webpackerとは、Rails標準装備のモジュールバンドラーで、Webpackのラッパーです。
バンドラーというのは束ねる人のことです。
HTML、CSS、JSなど色々な形式のファイルを束ねてくれるやつです。

ちなみにラッパーは韻を踏む人のことではありません。
サランラップとかのラッパーです。『包む』という意味です。
Webpackerは内部でWebpackを呼び出しているので、WebpackerはWebpackのラッパーです。

ちなみにフロントエンドに興味があるなら、Webpackの知識はある程度あった方が良いです。
"Babel"とか"ES6"とかそういうワードとセットで覚えるとGOODです!

rails webpacker:install
rails webpacker:install:react

3. MySQLのインストール

今回はDBにMySQLを使ってみます。
私は普段の業務ではPostgreSQLを使用しているのですが、プログラミングスクール卒の方はMySQLに慣れていると思うので。
新規プロダクトのDB選定はその現場で使い慣れているものを使用しているところが多いような気がしています。
違ったらすみません。
ちなみに余談of余談ですが、個人的にはNoSQLのMongoDBとかに興味があったりします。
理由は、「なんとなく知ってたらイケてるエンジニアっぽいから」です。

今回はbrewというパッケージマネージャー経由でMySQLをインストールします

brew install mysql

4. MySQLユーザーの作成

MySQLがインストールできたら、今回のアプリで使用するためのユーザーを作成します。
各コマンドについては、特に詳しく説明する必要もなさそうなので割愛します。

・ルートユーザーにログイン

mysql -u root -p

・ユーザー作成

好きなユーザー名とパスワードを設定

create user 'ユーザー名'@'localhost' identified by 'パスワード';

・作成したユーザーの確認

作成したユーザーが表示されていれば成功

select User,Host from mysql.user;

・権限付与

grant all on *.* to '[ユーザー名]'@'localhost';

・config/database.ymlの設定変更

ユーザーの作成が一通り終わったら、作成したユーザーとRailsを紐付けます。
RailsのDB設定はdatabase.ymlに記述するのがルールです。

default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host: <%= ENV['DATABASE_HOST'] %>

development:
  <<: *default
  database: app_name_development

test:
  <<: *default
  database: app_name_test

production:
  <<: *default
  database: app_name_production
  username: <%= ENV['APP_NAME_DATABASE_USERNAME'] %>
  password: <%= ENV['APP_NAME_DATABASE_PASSWORD'] %>

『app_name_○○』はRails newした時のアプリ名に置き換えてください
usernameやpasswordはGitHubで公開しちゃうと見えてしまうので
gem『dotenv』等を使って隠蔽することをおすすめします。
ちなみにdotenvで作成した『.env』ファイルをGit管理から外しておかないと意味が無いので、作ったら『.gitignore』に『.env』を忘れずに追加しましょう!
「何を言っているのかわからない...」という人は「dotenv 環境変数」とかで調べてみよう!
「わからないことを調べる」のは、エンジニアの基本です!
この『調べる』をいかに深堀りしてできるかが、成長の近道のような気がしています。

5. データベースの作成

rake db:create

6. Railsサーバーの起動

rails s

7. Webで確認

http://localhost:3000/

「Yay! You’re on Rails!」が表示されていれば成功

環境構築はもう少し続きます。
もう6合目ぐらいには来てます。もう少し。

8. Webpackerの設定(任意)

・splitchunks

チャンクを自動分割してくれるWebpackのプラグインです。
ファイルサイズの節約ができたりするけど、別になくても良いです。

config/webpack/environment.jsの変更
const { environment } = require('@rails/webpacker');
environment.splitChunks();
module.exports = environment;
app/views/top/show.html.erb

javascript/packs/の中にある「index」という名前の付いたファイルを参照するの意。

<%# splitchunksを使う場合 %>
<%= javascript_packs_with_chunks_tag 'index' %>

<%# splitchunksを使わない場合 %>
<%= javascript_pack_tag 'index' %>

参考: splitchunks

9. ルーティング設定

config/routes.rb
Rails.application.routes.draw do
  # ルートページ設定
  root "top#show"
end

10. エントリーポイントの作成

・ルートページのコンロトーラー作成

app/controllers/top_controller.rb
class TopController < ApplicationController

  def show
  end

end

・Reactで描画するためのid属性を追加

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>アプリケーションタイトル</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= javascript_pack_tag 'application' %>
  </head>

  <body>
    <div id="root"> ←これです
      <%= yield %>
    </div>
  </body>
</html>

・Reactのエントリーポイント作成

app/javascript/packs/index.jsx

app/views/top/show.html.erbから参照されるファイル
このファイルがReactの入り口です。
非同期、ルーティング、状態管理等、React用のパッケージをimportして設定しています。
各パッケージのインストールは後で行います。

// このファイルがRailsのViewから呼ばれる一番最初のファイルです(EntryPoint)
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import {
  Router,
  Route,
  Switch,
  IndexRoute,
  useLocation
} from 'react-router-dom';
import { createBrowserHistory } from 'history';
import { composeWithDevTools } from 'redux-devtools-extension';

// reducer
import rootReducer from '~/src/reducers/';

// Component
import Top from '~/src/components/tops/';

const middleWares = [thunk];
// 開発環境の場合、開発ツールを使用するための設定
const enhancer = process.env.NODE_ENV === 'development' ?
  composeWithDevTools(applyMiddleware(...middleWares)) : applyMiddleware(...middleWares);
const store = createStore(rootReducer, enhancer);
const customHistory = createBrowserHistory();

render(
  <Provider store={store}>
    <Router history={customHistory}>
      <Route render={({ location }) => (
        <div>
          <Switch location={location}>
            <Route exact path='/' component={Top} />
          </Switch>
        </div>
      )}/>
    </Router>
  </Provider>,
  document.getElementById('root')
)

・Reactコンポーネント作成

app/javascript/src/components/tops/index.jsx

Reactコンポーネントの記述にはjsxという拡張子のファイルを使用します。
JSファイルの中にHTMLを記述します。
最初はJSの中にHTMLタグを書くことに気持ち悪さを感じますが、その内慣れます。

import React from 'react';

const Top = () => (
  <h1>
    <center>アプリケーションのタイトル</center>
  </h1>
)
export default Top;

・Reactリデューサーをまとめる処理の作成

app/javascript/src/reducers/index.js
import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';

import top from '~/src/modules/tops/';

export default combineReducers({
  form: formReducer,
  top,
});

ここでまとめたものがReduxのstoreに格納されます。
Reduxとは状態を一元管理してくれるパッケージのことです。
storeとは状態を格納する箱のことです。Reduxの一番重要な機能です。
Reactの開発において、Reduxの利用は必須ではありませんが、React単体だとプロダクトの規模が大きくなるにつれて状態管理が辛くなるので、初めから入れておいた方がいいです。
LPとか規模の小さいプロダクトならなくても良いです。

・Reactモジュール作成

ディレクトリ構成はducksパターンを採用。
ducksパターンというのは『action type』、『action creator』、『reducer』を1つのファイルにまとめて記述する考え方のことです。設計の概念です。何かをインストールするとかではないです。

app/javascript/src/modules/tops/index.js
// action-type
const TOP_INITIAL = 'TOP_INITIAL';

// reducer
const initialState = {
  top: null,
}

export default function top(state = initialState, action) {
  switch (action.type) {
    case TOP_INITIAL:
      return {
        ...state,
      }
    default:
      return state
  }
}

// action-creator
export const topInitial = () => ({
  type: TOP_INITIAL,
});

通常は『action type』、『action creator』、『reducer』それぞれでファイルを作成するところ、ducksパターンを取り入れると1つのファイルにまとまるので、単純にファイル数が少なくて済みます。
中規模プロダクトでも全然耐えられる設計概念なのでおすすめです。
「action typeって何?」と思った人はRedux公式で調べてみましょう!

11. 必要なパッケージインストール

yarnというパッケージマネージャーを使用してインストールします。
似た様なパッケージマネージャーで『npm』がありますが、『yarn』は『npm』の上位互換です。
yarnでインストールしたパッケージは、ルートディレクトリ直下の『package.json』というファイルに自動で追加されます。
『yarn add パッケージ名』でパッケージの追加
『yarn remove パッケージ名』でパッケージの削除です。

yarn add redux react-redux react-router-dom redux-devtools-extension redux-form redux-thunk axios @babel/preset-react babel-plugin-root-import

もし興味があれば『@reduxjs/toolkit』、『@material-ui/core』もおすすめです

12. パス指定設定ファイルの作成(任意)

独学でReactを少しでも開発したことがある方なら一度はこう思ったはず
「React相対パス地獄なりがち。」
Reactはimport時の相対パス指定地獄に陥りがちです。
そうならないよう『babel-plugin-root-import』を入れることをおすすめします。
実は上記11.の『yarn add』の中にこっそり入っているので、コマンドをコピペして実行した方は私の策略によりすでに入っています。

『.babelrc』というファイルを作ってそこに設定を記述します。
『.babelrc』ファイルを作る場所はルートディレクトリ直下。

.babelrc

{
  "plugins": [
    [
      "babel-plugin-root-import",
      {
        "paths": [
          {
            "rootPathSuffix": "./app/javascript/src",
            "rootPathPrefix": "~/src/"
          },
        ]
      }
    ]
  ]
}

上記設定は『./app/javascript/src』というパス指定を『~/src/』という文字列でも指定できるように設定しているだけです。
これで、Reactコンポーネントでのimport時に『~/src/○○』が使えるようになるので、相対パス地獄から抜け出せます。
ちなみに『"~/src/"』の部分は『"~/"』でも『"@/src/"』でも好きに設定できます。

13. webpack-dev-serverの起動

./bin/webpack-dev-server

自動コンパイルしてくれる開発用サーバーです。
常にコードの監視もしているので、Reactのコードを書き換えると自動でブラウザ上の描画も書き換えてくれます。
(ちなみにRailsのModelやController、Viewなどは監視対象外なので変更しても自動描画はされません。素直に『command + R』でブラウザ更新しましょう。)

お疲れ様でした

これでRails & Reactの開発環境が整った...はずです。
http://localhost:3000/に「アプリケーションのタイトル」が表示されていれば無事成功です!
それでは楽しい3R(Ruby on Rails on React)開発を!

トラブルシューティング

An error occurred while installing mysql2 (0.5.3), and Bundler cannot continue.
Make sure that gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/' succeeds before bundling.

上記エラーメッセージが表示されてbundle installが失敗する場合↓

sudo xcodebuild -license acceptで解決できる場合もある

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

【駆け出し】Reactの導入【create-react-app】

create-react-app とは

Reactの開発環境を簡単に構築できるツールでReact開発元のFacebookが公式に公開しているツールだそうです。

create-react-appを使えるように設定

必要な条件はnodenpmがインストールされていることなのでここから設定していきます。

  • homebrewをインストール  →すでにインストール済 homebrew -vで確認
  • nodebrewをインストール
ターミナル
brew install nodebrew
nodebrew -v
  • nodeをインストール
ターミナル
nodebrew ls-remote       すべてのバージョンを一覧表示
nodebrew install stable    安定版をインストール
mkdir -p ~/.nodebrew/src   エラーが出たので実行
nodebrew ls バージョン確認
v14.5.0

current: none
nodebrew use v14.5.0
echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.zprofile  シェルがzshの場合

ターミナルを再起動する
node -v
v14.5.0                     バージョンが表示されれば完了
  • npmをインストール → nodeと一緒にインストールされている
ターミナル
npm -v
6.14.5

これでcreate-react-appを利用する準備ができました。ここから実際に導入していきます。

create-react-appの導入

ターミナル
cd projects   各自の開発環境に移動
npx create-react-app アプリ名
cd アプリ名
npm start     ローカルサーバーが立ち上がります

localhost:3000

書いてあるとおり、この画面はApp.jsを書き換えると編集出来ます!
これでReactを動かせるようになりました。

参考になればLGTM頂けると嬉しいです!!

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

コーディング規約 & 使用技術に関する資料

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

three.js x React x redux で3Dオセロゲームを作った

はじめに

WebGLの3Dブラウザゲームを作りたいと考えた時、どのようなフレームワークを選択すればよいのでしょうか?three.jsは一番メジャーなWebGLの3DCGライブラリですが、基本的に3DCGを描画する機能しかないので、これ単体でゲームを作るには少し苦しい局面もあります。そこで、Reactとreduxに、UIやゲームの進行管理を任せることでそこそこの規模のゲーム開発ができるのでは?と思ったのでthree.js x React x redux で3Dオセロゲームを作ってみました。

作った物

こちらでプレイできます。
https://arihide.github.io/reversi3d/
スクリーンショット 2020-07-01 0.03.51.png

ソースコードはこちら
UI・3D描画周り: https://github.com/Arihide/reversi3d
オセロ のルール・AI周り: https://github.com/Arihide/reversi.js

実装時に行ったこと

three.jsのオブジェクトもコンポーネントとして扱う

Reactでは React.Component を継承することでコンポーネントを作成しますが、Canvas内に描画される3Dオブジェクトも THREE.Mesh を継承してコンポーネントっぽくしてみました。
以下はオセロ盤のオブジェクトの実装を簡素にした例です。

import store from 'js/store'
import { Mesh } from 'three'

export default class BoardMesh extends Mesh {
    constructor(geometry, material) {
        store.subscribe(this.onChange.bind(this))
    }

    onChange() {
         // 石がひっくり返ったりしたときの処理などゲームが進んだときの処理を書く…
    }
}

流石にReactのコンポーネントほど便利なライフサイクルはないのですが、
reduxのstoreも購読することであたかもView Treeの一部に組み込まれているかのように扱えているのでとても便利になりました。
three.jsは一つのファイルにオブジェクト追加処理を書いていくとどんどん複雑になってしまうので、こういった形で分離できるのはとても良いですね。

AI・ゲームロジック部分の別モジュール化

今回実装するにあたって、UI表示とゲームロジックの部分を別リポジトリに分けました。
UI・3D描画周り: https://github.com/Arihide/reversi3d
オセロ のロジック・AI周り: https://github.com/Arihide/reversi.js
そして、以下のようにUI側のpackage.jsonにパッケージとして追加しました。

package.json
  "dependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-redux": "^7.2.0",
    "react-transition-group": "^4.4.1",
    "redux": "^4.0.5",
    "reversi.js": "git+https://github.com/Arihide/reversi.js.git",
    "three": "^0.118.3"
  }

このようにした1番の理由は、ゲームロジックの部分はUIと比較しても複雑なので、パッケージとして分離した上で単体テストなども用意して動作確認をしたかったからです。

とにかくコアな部分は分離して粒度を小さくして信頼性をあげるのが良いと思います。

Redux DevToolsの活用

ゲーム開発においてデバッグは必要不可欠です!
デバッグはゲーム中で起こりうる全ての状態に対して行われるべきですが、ゲームによっては再現が難しい状態があったりします。
例えば今回のオセロの例だと、
- パスの局面
- 全ての石が同じ色になる局面
- 引き分け
などが挙げられます。(物凄い再現が難しいというわけではないのですが)

こういった特定の状況再現のためにデバッグツールを使用することはよくありますが、特に個人開発だとツールの用意までには手が回らないことも多いです。
なので、今回はRedux DevToolsを利用をしてみました。このツールにはStoreの履歴をJson形式でインポート・エクスポートできるので容易に状態の再現ができるようになります。
こういったツールが利用できるのもまたフレームワークを利用するメリットですね!
スクリーンショット 2020-07-04 15.27.27.png

Web Workerの活用

オセロの最善手計算は物凄い数ゲーム木のノードを探索するので、とても重い処理です。これが行われる最中は3Dの描画がカクついてしまってよろしくないので、Web Workerを使用して処理を別スレッドに移すことで、カクツキを防ぎました。
Workerの呼び出し側と受けてのコードは以下です。

actions.js
export async function computeMove(state) {
    return new Promise(resolve => {
        worker.addEventListener("message", e => {
            resolve(placeDisc(state, e.data))
        }, { once: true })
        worker.postMessage(state.playedMoves)
    })
}
Worker側の実装
self.addEventListener('message', e => {
    board.initialize()

    for (let move of e.data) {
        board.pushMove(move.place)
    }

    alphaBetaPlayer.computeMove(board, bestMove => {
        self.postMessage(bestMove)
    })
})

どうしてもゲームなどは負荷対策が必要になってくるので、マルチスレッドなどの知識も必要になってきます。

まとめ

今回はthree.js x React x redux で3Dオセロゲームを作ってみました。
そのままthree.jsでオブジェクトを量産していくとゴチャっとしてしまう場合もありますが、コンポーネント化しreduxと結び付けることですっきりして結構大規模なゲームも作れるのでは?という感じもしました。

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

useStateとFunctional updates

問題

問題です。以下のコンポーネントでボタンをクリックすると最終的に useItems から返ってくる items はどのような値になるでしょうか?

import React, { useEffect, useState, useRef } from "react";

const useItems = () => {
  const [items, setItems] = useState([])
  const idRef = useRef(0)

  const push = () => {
    const id = idRef.current++
    setItems(items.concat({ id }))
  }

  return {
    items,
    push,
  }
}


export default function App() {
  const { items, push } = useItems()

  return (
    <div className="App">
      <button 
        type="button"
        onClick={() => { push(); push(); push() }}
      >
        PUSH PUSH PUSH!!!
      </button>

      <ul>
        {items.map((item) => <li key={item.id}>{item.id}</li>)}
      </ul>
    </div>
  )
}

メタ読みすれば [0, 1, 2] ではないですよね…

正解は [2] です!

解説

何故でしょう?
push はクロージャから items をとってきて setItems(items.concat({ id })) をしていますが items 自身が更新されるわけではありません。
なので3回呼ばれている同一の push では毎回 setItems([].concat({ id })) され、最終的には setItems([2]) されるからです。

setState は引数として (currentState) => nextState な関数を受け取ることができます(Functional updates)。
これを使って上のような問題が起きないように書き換えましょう。

import React, { useEffect, useState, useRef } from "react";

const useItems = () => {
  const [items, setItems] = useState([])
  const idRef = useRef(0)

  const push = () => {
    const id = idRef.current++
    setItems((currentItems) => currentItems.concat({ id }))
  }

  return {
    items,
    push,
  }
}


export default function App() {
  const { items, push } = useItems()

  return (
    <div className="App">
      <button 
        type="button"
        onClick={() => { push(); push(); push() }}
      >
        PUSH PUSH PUSH!!!
      </button>

      <ul>
        {items.map((item) => <li key={item.id}>{item.id}</li>)}
      </ul>
    </div>
  )
}

こうすれば useItems から返ってくる items は期待通り [0, 1, 2] になります。
なんとなく書いていると意外とあれ?と思うことが起きがちかもなと思いました。

実際に触れるCodeSandboxを置いておきます↓
https://codesandbox.io/s/blissful-robinson-329w2?file=/src/App.js

参考

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

create-react-appのbaseUrlには.を指定できるが、src以外はインポートできない

baseUrlに.をセットして意気揚々とimport {XXX} from 'xxx/xxx';を試みたが、ダメだった。
このIssue commentにある通り、create-react-appはv3.2.0からbaseUrl: '.'を使用できるようになったが、src/から始まるパスしかインポートできない制限がある。
なんてこった…。

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

react: アプリ全体でステートを共有

propsもreduxも使わずに、コンポーネント間でステートを共有することができる

方法

  1. useStateをモジュールに切り分ける。(下記appMode.tsの、function useAppMode参照)
  2. 一番外側のコンポーネントで、モジュール化されたuseState(下記のfunction useAppMode)を呼ぶ。
  3. 以後、任意のコンポーネントで、ステート(下記appMode.tsの mode)、ステートのsetter(下記appMode.tsのsetMode)が参照可能となる。
appMode.ts
import { useState, Dispatch, SetStateAction } from 'react'

export enum AppMode {
  home,
  write,
  read,
}

export let mode: AppMode
export let setMode: Dispatch<SetStateAction<AppMode>>

export function useAppMode() {
  ;[mode, setMode] = useState(AppMode.home)
}

ファイル構成

  • appMode.ts
  • app.tsx

    • home.tsx
    • write.tsx
    • read.tsx

ノートアプリ。
ホーム画面、書き画面、読み画面あり。
アプリモードによって、画面の描画を替える。

app.tsx
import {
  isHomeMode,
  isWriteMode,
  isReadMode,
  useAppMode,
} from '../logic/appMode'
import { Reader } from '../components/reader'
import { Writer } from '../components/writer'
import { Home } from '../components/home'

export default () => {

  useAppMode() // 一番外側のコンポーネントで、モジュール化したuseStateを呼び出し

  return (
    <div id="app">
      {isHomeMode() ? <Home></Home> : null} // 以後、任意の場所で、ステートのgetter, setterを参照できる
      {isWriteMode() ? <Writer></Writer> : null} // 例えば、この行の isWriteMode()のように、modeがWriteか判定できる
      {isReadMode() ? <Reader></Reader> : null} // 例えば、次のファイル、home.tsxのように、modeの値も更新できる。更新するだけで、当然、この行の<Reader></Reader>の表示・非表示が切り替わる
    </div>
  )
}
home.tsx
import { setWriteMode } from '../logic/appMode'
import { setReadMode } from '../logic/appMode' // モジュール化したステートmodeのsetterをimport

export const Home = () => (
  <div>
    <button onClick={() => setWriteMode()}>Write</button>
    <button onClick={() => setReadMode()}>Read</button> // ここでステートmodeの値をAppMode.Readに書き換えている。中で何を行っているかは、次のファイルに書いている
  </div>
)
appMode.ts
import { useState, Dispatch, SetStateAction } from 'react' 

export enum AppMode {
  home,
  write,
  read,
}

let mode: AppMode
let setMode: Dispatch<SetStateAction<AppMode>>

export function useAppMode() {
  ;[mode, setMode] = useState(AppMode.home)
}

export function isHomeMode(): boolean {
  return mode == AppMode.home
}
export function isWriteMode(): boolean {
  return mode == AppMode.write
}
export function isReadMode(): boolean {
  return mode == AppMode.read
}
export function setHomeMode(): void {
  setMode(AppMode.home)
}
export function setWriteMode(): void {
  setMode(AppMode.write)
}
export function setReadMode(): void {
  setMode(AppMode.read)
}

メリット
- any to any の関係に適用可能
- 記述量が減少
- ステートごとに、スコープを分離できる

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

react: stateの共有

stateの共有 (props無し、redux無し)

方法

  1. useStateをモジュールに切り分ける。(下記appMode.tsの、function useAppMode参照)
  2. 一番外側のコンポーネントで、モジュール化されたuseState(下記のfunction useAppMode)を呼ぶ。
  3. 以後、任意のコンポーネントで、ステート(下記appMode.tsの mode)、ステートのsetter(下記appMode.tsのsetMode)が参照可能となる。
appMode.ts
import { useState, Dispatch, SetStateAction } from 'react'

export enum AppMode {
  home,
  write,
  read,
}

export let mode: AppMode
export let setMode: Dispatch<SetStateAction<AppMode>>

export function useAppMode() {
  ;[mode, setMode] = useState(AppMode.home)
}

  • appMode.ts
  • app.tsx

    • home.tsx
    • write.tsx
    • read.tsx
app.tsx
import {
  isHomeMode,
  isWriteMode,
  isReadMode,
  useAppMode,
} from '../logic/appMode'
import { Reader } from '../components/reader'
import { Writer } from '../components/writer'
import { Home } from '../components/home'

export default () => {

  useAppMode() // モジュール化したuseStateを呼び出し

  return (
    <div id="app">
      {isHomeMode() ? <Home></Home> : null} // stateをgetterで参照
      {isWriteMode() ? <Writer></Writer> : null}
      {isReadMode() ? <Reader></Reader> : 
    </div>
  )
}
home.tsx
import { setWriteMode } from '../logic/appMode'
import { setReadMode } from '../logic/appMode'

export const Home = () => (
  <div>
    <button onClick={() => setWriteMode()}>Write</button>
    <button onClick={() => setReadMode()}>Read</button> // stateのsetterを呼び出し
  </div>
)
appMode.ts
import { useState, Dispatch, SetStateAction } from 'react' 

enum AppMode {
  home,
  write,
  read,
}

let mode: AppMode
let setMode: Dispatch<SetStateAction<AppMode>>

export function useAppMode() {
  ;[mode, setMode] = useState(AppMode.home)
}

// getter setter (※不要。上記、mode, setMode, AppModeをexportすればよい。可読性と保守性にメリット。)
export function isHomeMode(): boolean {
  return mode == AppMode.home
}
export function isWriteMode(): boolean {
  return mode == AppMode.write
}
export function isReadMode(): boolean {
  return mode == AppMode.read
}
export function setHomeMode(): void {
  setMode(AppMode.home)
}
export function setWriteMode(): void {
  setMode(AppMode.write)
}
export function setReadMode(): void {
  setMode(AppMode.read)
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react: stateの共有 (props無し、redux無し)

方法

  1. useStateをモジュールに切り分ける。(下記appMode.tsの、function useAppMode参照)
  2. 一番外側のコンポーネントで、モジュール化されたuseState(下記のfunction useAppMode)を呼ぶ。
  3. 以後、任意のコンポーネントで、ステート(下記appMode.tsの mode)、ステートのsetter(下記appMode.tsのsetMode)が参照可能となる。
appMode.ts
import { useState, Dispatch, SetStateAction } from 'react'

export enum AppMode {
  home,
  write,
  read,
}

export let mode: AppMode
export let setMode: Dispatch<SetStateAction<AppMode>>

export function useAppMode() {
  ;[mode, setMode] = useState(AppMode.home)
}

  • appMode.ts
  • app.tsx

    • home.tsx
    • write.tsx
    • read.tsx
app.tsx
import {
  isHomeMode,
  isWriteMode,
  isReadMode,
  useAppMode,
} from '../logic/appMode'
import { Reader } from '../components/reader'
import { Writer } from '../components/writer'
import { Home } from '../components/home'

export default () => {

  useAppMode() // モジュール化したuseStateを呼び出し

  return (
    <div id="app">
      {isHomeMode() ? <Home></Home> : null} // stateをgetterで参照
      {isWriteMode() ? <Writer></Writer> : null}
      {isReadMode() ? <Reader></Reader> : 
    </div>
  )
}
home.tsx
import { setWriteMode } from '../logic/appMode'
import { setReadMode } from '../logic/appMode'

export const Home = () => (
  <div>
    <button onClick={() => setWriteMode()}>Write</button>
    <button onClick={() => setReadMode()}>Read</button> // stateのsetterを呼び出し
  </div>
)
appMode.ts
import { useState, Dispatch, SetStateAction } from 'react' 

enum AppMode {
  home,
  write,
  read,
}

let mode: AppMode
let setMode: Dispatch<SetStateAction<AppMode>>

export function useAppMode() {
  ;[mode, setMode] = useState(AppMode.home)
}

// getter setter (※不要。上記、mode, setMode, AppModeをexportすればよい。可読性と保守性にメリット。)
export function isHomeMode(): boolean {
  return mode == AppMode.home
}
export function isWriteMode(): boolean {
  return mode == AppMode.write
}
export function isReadMode(): boolean {
  return mode == AppMode.read
}
export function setHomeMode(): void {
  setMode(AppMode.home)
}
export function setWriteMode(): void {
  setMode(AppMode.write)
}
export function setReadMode(): void {
  setMode(AppMode.read)
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む