動機
以前の投稿見た人はわかるかもだけど、Vue.jsでそれなりに満足はしていたし今でもしているのだが、世の中いろんなところで ReactReact 言われててなんとなく「今どき React わかるのは最低要件っス」的な圧を感じるので、一通りやっておかんとなと思ってはじめた。最近だとUnityからも使えるらしい。まじかよ。
で、Vue.jsやってた人がこれはReactだとどうなるの?的な観点からざざっとやってみた
勉強ログ
Step.0
- ホームページ: https://ja.react.dev/
ただここのチュートリアルやってるといつの間にか(Next.js/Remix/Gatsby)を使ったパターンに落とし込まれるのが罠。求めていたのは Vue.js の snap-in-replace としての React だったのでこのトレーニングはスキップした。
Step.1 : Hello Worldを手元で
この手順は公式では既に deprecated 扱いになっている
`npx create-react-app` は既にdeprecatedの扱い。参考: https://zenn.dev/nekoya/articles/dd0f0e8a2fa35f
ここではフレームワークに依存しない学習のためにあえてやっている。使わなくてもスケルトンを git clone
などして自分で用意して、npm install && npm start
すれば同じことになるので大丈夫。
npx create-react-app
するのに Node.js 以外に特に事前に何か入れておく必要はない。
npx create-react-app step1
cd step1
npm start
ディレクトリの構造には特に決まったルールはないようだが、慣例的に以下のようにしている例が多い。
./
├── build/ # <- npm run build で作成される配布用ファイル
├── node_modules/
├── package-lock.json
├── package.json
├── public/ # <- 画像ファイルなどのリソース
└── src/
├── index.js # <- 最初に呼び出されるjs
├── pages/ # <- ページを定義するコード
├── setupTests.js
├── lib/ # <- 外部APIなどを呼び出すためのコード
└── ui/ # <- UIのパーツを定義するコード
内容を変更するには src/App.js
を変更する。試しに以下のように変更してみる
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
Hello React
</div>
);
}
export default App;
変更を保存すると、ホットロードされて表示が更新される。
View Code on CodeSandbox:
Step.2 : 複数のページ。 React Router
公式ページ: https://reactrouter.com/en/main
npx create-react-app step2
cd step2
npm install react-router-dom
npm start
Step.2-1
src/index.js
を以下のように変更する
import React from 'react';
import ReactDOM from 'react-dom/client';
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: <div>This is root page</div>,
},
]);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
保存するとブラウザには This is root page
と出ているはず。
Step.2-2
複数ページを作る。まずRootページ
const Root = () => {
return (
<div id="sidebar">
This is Root page
</div>
);
};
export default Root;
import React from 'react';
import ReactDOM from 'react-dom/client';
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import Root from './pages/Root';
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
},
]);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
This is Root page
と出る
Mainページを作る
const Main = () => {
return (
<div>
This is Main page
</div>
)
}
export default Main
index.js にルートを追加する
import React from 'react';
import ReactDOM from 'react-dom/client';
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import Root from './pages/Root';
import Main from './pages/Main';
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
},
{
path: "/main",
element: <Main />
}
]);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
ブラウザで http://localhost:3000 と http://localhost:3000/main で表示が変わることを確認する
Step.3 : コンポーネントのライフサイクル
- Vuejs.BeforeMount == React.funcBeforeReturn
React では特定のディレクティブというものはなくて、普通の Javascript で処理する。return
する前の処理はすべて BeforeMount の処理ということになる。
こんな感じ
const Root = () => {
// return 前の処理はすべて BeforeMount の処理ということになる
const mysecretstring = "aaabbbccc"
return (
<div id="sidebar">
This is Root page.<br />
{/* {} で囲った部分はjavascriptが実行される */}
My Secret string is {mysecretstring}.
</div>
);
};
export default Root;
- Vuejs.(Mounted|Unmounted) == useEffect
例えばサーバのログアウトAPIを呼び出すなど、画面から移動したときになにかしたいとき、ReactではuseEffectを使う。
import { useEffect } from 'react';
import { initConnection,
connect,
disconnect } from './chat.js';
export default function ChatRoom() {
const beforeMount = () => {
/* マウント前の処理 */
initConnection();
}
useEffect(() => {
/* マウント後の処理 */
connect();
return () => {
/* アンマウント後の処理。クリーンアップ関数とも呼ばれる */
disconnect();
}, []);
return <h1>Welcome to the chat!</h1>;
}
Step.4 変数のリアクティビティ == useState
- ローカル変数はレンダー間で保持されない
- ローカル変数の変更は、レンダーをトリガしない
で、useState
フックを使ってリアクティブな変数を宣言する。
import { useState } from "react";
import NumberHolder from "../ui/NumberHolder";
const Root = () => {
// var number = 1;
const [number, setNumber] = useState(0); // 0 は初期値
const decrement = () => {
//number -= 1
setNumber(number-1)
console.log(number)
return(number)
};
const increment = () => {
//number += 1
setNumber(number+1)
console.log(number)
return(number)
};
return (
<div id="sidebar">
<button onClick={decrement}>-</button>
<NumberHolder number={number}/>
<button onClick={increment}>+</button>
</div>
);
};
export default Root;
const NumberHolder = ({number}) => {
console.log("number", number)
//const number = obj.number
return (
<input name="aaa" value={number} onChange={e => console.log(e)}/>
)
}
export default NumberHolder;
View Code on CodeSandbox:
Step.5 v-for, v-if
Vue.js で言うところの v-for は、Reactではレンダリング時にJSXから呼び出したJavascriptで行うため、専用の句は存在しない。例えば以下のように書く。
import ListContainer from "../ui/ListContainer";
const ListExample = () => {
const items = ["aaa", "bbb", "ccc"];
return (
<div id="list">
This is list demo(aka. v-for of vue.js)
<br />
<a href="/if">v-ifのデモはこちら</a>
<br />
<ListContainer items={items} maxChars={100} />
</div>
);
};
export default ListExample;
import ListItem from "./ListItem";
const ListContainer = ({items}) => {
console.log("number of items:", items.length)
const listItems = items.map(item => {
return (
<ListItem listKey={item} listValue={item}/>
)
})
return (
<ul>
{listItems}
</ul>
)
}
export default ListContainer;
const ListItem = ({listKey, listValue}) => {
console.log(listKey, listValue)
return (
<li key={listKey}>{listValue}</li>
)
}
export default ListItem;
同様に v-if もJSXのJavascriptで制御する
import React from 'react';
import ReactDOM from 'react-dom/client';
import {
createBrowserRouter,
RouterProvider
} from "react-router-dom";
import ListExample from "./pages/ListExample";
import IfExample from "./pages/IfExample.js";
const router = createBrowserRouter([
{
path: "/",
element: <ListExample/>,
},
{
path: "/if",
element: <IfExample/>,
}
])
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RouterProvider router={router}/>
</React.StrictMode>
);
import { useState } from "react";
import NumberHolder from "../ui/NumberHolder";
import ListContainer from "../ui/ListContainer";
const IfExample = () => {
const items = [
"aaa",
"aaabbb",
"aaabbbccc"
]
const [number, setNumber] = useState(10);
const decrement = () => {
//number -= 1
setNumber(number-1)
console.log(number)
return(number)
};
const increment = () => {
//number += 1
setNumber(number+1)
console.log(number)
return(number)
};
return (
<div id="list">
<button onClick={decrement}>-</button>
<NumberHolder number={number}/>
<button onClick={increment}>+</button>
文字以上の項目を非表示にする
<ListContainer items={items} maxChars={number}/>
</div>
)
}
export default IfExample;
import ListItem from "./ListItem";
const ListContainer = ({items, maxChars}) => {
console.log("number of items:", items.length, "max characters", maxChars)
const listItems = items.map(item => {
return (
<ListItem listKey={item} listValue={item} maxChars={maxChars}/>
)
})
return (
<ul>
{listItems}
</ul>
)
}
export default ListContainer;
const FilteredItem = ({listKey, listValue, maxChars}) => {
if (listValue.length < maxChars) {
console.log("smaller", listValue, maxChars)
return (
<li key={listKey}>{listValue}</li>
)
} else {
console.log("bigger", listValue, maxChars)
return null
}
}
const ListItem = ({listKey, listValue, maxChars}) => {
console.log(listKey, listValue, maxChars)
return (
<FilteredItem listKey={listKey} listValue={listValue} maxChars={maxChars}/>
)
}
export default ListItem;
上で使われている <FilteredItem listKey={listKey} listValue={listValue} maxChars={maxChars}/>
の listKey={listKey}
とか listValue={listValue}
をReact用語でprop(s)と呼ぶ。
じゃあ変化する変数は最上位コンポーネントに持たせて下位にはpropsで渡せばいいじゃんと考えるかもしれない(私も最初はそうした)が、変化したpropsを受け取った下位コンポーネントは再レンダリングが走るというのが曲者。うっかり最上位に頻繁に変更されるような変数をもってpropsで派生させていくと、なにかするたびに画面が再レンダリングの嵐になってまともに使えなくなる。
再レンダリングを防ぐ方法はいろんな人が書いてるので別途参照
- React の再レンダリングを防ぐ3つのパターン(https://zenn.dev/azukiazusa/articles/react-rerender-patterns)
- 【React】再レンダリングの仕組みと最適化(https://zenn.dev/b1essk/articles/react-re-rendering)
- How to stop re-rendering lists in React?(https://alexsidorenko.com/blog/react-list-rerender/)
Step.6 UIコンポーネントを使う
- Material UI : https://mui.com/
- Ant Design : https://ant.design/
Step.7 パッケージング、ホスティング
Vue.js と同じように npm run build
するといろいろ難読化されパッケージングされて、./build
以下に静的ファイルが作成される。あとはこれを NGINX などでホスティングすればいい。
Step.8 やってはいけないこと
useEffectの中でstateの参照と更新をやってはいけない
例えば以下のようなコードがあったとする
const [myState, setMyState] = useState(0)
useEffect(() => {
DoSomething()
.then((ret) => setMyState(ret))
.then(() => {
if(myState>10)
console.log("myState is larger than 10")
else
console.log("myState is up to 10")
})
},[myState])
実際に動かすと、myStateが更新されるたびにuseEffectの中が再実行されるので、useEffect内が無限ループする。
これを避けるには、別のところにクロージャとして定義して、それをいじるようにすればいい。
export const MyState = (() => {
let mystate = 0
const set = (val) => {
mystate = val
}
const get = () => {
return mystate
}
return {
set: (val) => set(val),
get: () => get()
}
})()
import { MyState } from './func.js'
const myState = MyState
useEffect(() => {
DoSomething()
.then((ret) => {
myState.set(ret)
}).then(() => {
if(myState.get()>10)
console.log("myState is larger than 10")
else
console.log("myState is up to 10")
})
}, [myState])
やっておいたほうがいいこと
Reactのコードを読んだり、なにかトラブルを解決するために、以下のことを理解しておくとなんとかなることがままある。参考- Reactの前提条件(https://kinsta.com/jp/blog/react-best-practices/#react-1)
上記のサイトだと前提条件って書かれているが、自分の感覚だとそこまでstrictなものじゃないけどサンプルコードとかにこういう書き方が多いので知っておくと何をやっているか理解できたり、前述したprops,useEffectまわりでいい感じに動かすために知っておくと便利、という感じ。
- 残余引数
- スプレッド演算子
- 分割代入
- Promise
- いわゆるES2015のもろもろ。var, let, constの使い分け方, 変数の有効範囲に関する知識とか、アロー関数とか。参考 - ES2015(ES6) 入門
- メッセージング
- クロージャ
Reactは結果的にわりとモダンな書き方が求められることがままあるので、Vue.jsと比較すると相対的に難易度が高いようには感じる。逆に言うとVue.jsはそこまで知らなくてもなんとなくそれなりのアプリが作れてしまうという敷居の低さ的なものはあるんじゃないかな、と。お気持ちですけど・・・。
さいごに
特にありません