- 投稿日:2020-07-13T22:26:29+09:00
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:react3. 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で確認
「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' %>9. ルーティング設定
config/routes.rb
Rails.application.routes.draw do # ルートページ設定 root "top#show" end10. エントリーポイントの作成
・ルートページのコンロトーラー作成
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
で解決できる場合もある
- 投稿日:2020-07-13T21:24:53+09:00
【駆け出し】Reactの導入【create-react-app】
create-react-app とは
Reactの開発環境を簡単に構築できるツールでReact開発元のFacebookが公式に公開しているツールだそうです。
create-react-appを使えるように設定
必要な条件は
node
とnpm
がインストールされていることなのでここから設定していきます。
- 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 ローカルサーバーが立ち上がります
書いてあるとおり、この画面はApp.jsを書き換えると編集出来ます!
これでReactを動かせるようになりました。参考になればLGTM頂けると嬉しいです!!
- 投稿日:2020-07-13T16:05:55+09:00
コーディング規約 & 使用技術に関する資料
Git & GitHub
https://github.com/TEAM-QUANDO/synq-mobile-app/wiki/GitHub-%E9%81%8B%E7%94%A8%E6%96%B9%E6%B3%95
https://qiita.com/KosukeSone/items/514dd24828b485c69a05TypeScript
https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a
React Hooks
https://qiita.com/uhyo/items/246fb1f30acfeb7699da
Recoil
https://blog.uhy.ooo/entry/2020-05-16/recoil-first-impression/
- 投稿日:2020-07-13T10:47:01+09:00
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/
ソースコードはこちら
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形式でインポート・エクスポートできるので容易に状態の再現ができるようになります。
こういったツールが利用できるのもまたフレームワークを利用するメリットですね!
Web Workerの活用
オセロの最善手計算は物凄い数ゲーム木のノードを探索するので、とても重い処理です。これが行われる最中は3Dの描画がカクついてしまってよろしくないので、Web Workerを使用して処理を別スレッドに移すことで、カクツキを防ぎました。
Workerの呼び出し側と受けてのコードは以下です。actions.jsexport 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と結び付けることですっきりして結構大規模なゲームも作れるのでは?という感じもしました。
- 投稿日:2020-07-13T08:37:29+09:00
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参考
- 投稿日:2020-07-13T03:00:20+09:00
create-react-appのbaseUrlには.を指定できるが、src以外はインポートできない
baseUrlに.をセットして意気揚々と
import {XXX} from 'xxx/xxx';
を試みたが、ダメだった。
このIssue commentにある通り、create-react-appはv3.2.0からbaseUrl: '.'を使用できるようになったが、src/から始まるパスしかインポートできない制限がある。
なんてこった…。
- 投稿日:2020-07-13T02:09:44+09:00
react: アプリ全体でステートを共有
propsもreduxも使わずに、コンポーネント間でステートを共有することができる
方法
- useStateをモジュールに切り分ける。(下記appMode.tsの、function useAppMode参照)
- 一番外側のコンポーネントで、モジュール化されたuseState(下記のfunction useAppMode)を呼ぶ。
- 以後、任意のコンポーネントで、ステート(下記appMode.tsの mode)、ステートのsetter(下記appMode.tsのsetMode)が参照可能となる。
appMode.tsimport { 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.tsximport { 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.tsximport { 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.tsimport { 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 の関係に適用可能
- 記述量が減少
- ステートごとに、スコープを分離できる
- 投稿日:2020-07-13T02:09:44+09:00
react: stateの共有
stateの共有 (props無し、redux無し)
方法
- useStateをモジュールに切り分ける。(下記appMode.tsの、function useAppMode参照)
- 一番外側のコンポーネントで、モジュール化されたuseState(下記のfunction useAppMode)を呼ぶ。
- 以後、任意のコンポーネントで、ステート(下記appMode.tsの mode)、ステートのsetter(下記appMode.tsのsetMode)が参照可能となる。
appMode.tsimport { 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.tsximport { 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.tsximport { 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.tsimport { 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) }
- 投稿日:2020-07-13T02:09:44+09:00
react: stateの共有 (props無し、redux無し)
方法
- useStateをモジュールに切り分ける。(下記appMode.tsの、function useAppMode参照)
- 一番外側のコンポーネントで、モジュール化されたuseState(下記のfunction useAppMode)を呼ぶ。
- 以後、任意のコンポーネントで、ステート(下記appMode.tsの mode)、ステートのsetter(下記appMode.tsのsetMode)が参照可能となる。
appMode.tsimport { 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.tsximport { 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.tsximport { 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.tsimport { 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) }