初めてReactを触って何となくSPAができるまでの軌跡
スクラップ、こんな使い方でいいのだろうか。
今のところぼんやり考えていること
・コンポーネントライブラリはMUI使う
・レイアウトはBOXを使えばいいぽい
・Atomic Designを意識していきたいけどMUIのコンポーネント使った時のイメージがつかない
・TypeScriptも使っていきたい
・Reduxは使わない(手を伸ばしすぎないように)
・CSSはわからない(けど頑張る)
今やってること
・良書探しに密林を探索中
・「React実践の教科書」を見ながらコンポーネント作り
・MUIのコンポーネントを物色、カスタマイズ
ともあれ何を作ろうかな。
早速TypeScriptを使ってみる
既存のプロジェクトに導入してみた
やり方はネットで落ちてるいつものコマンド使って新しいプロジェクトを作る
npx create-react-app my-app --template typescript
その後
npm start
でローカルサーバー立ち上げてコンパイルを動かす
次にsrc配下を乱暴に丸ごとコピー。
そしてjsxの拡張子をtsxに変更。
コンパイルが動き出す。
溢れ出る型起因のエラーたち。
一つずつ潰していく!
そういえば型定義ってやっぱり定義専用tsファイルでも作っときゃいい感じかな。
とりあえず何かを作り始めてみた
段階的に候補が絞り込まれるコンボボックスコンポーネントを作ってみた
Material-UIのAutocompleteを複数使って、組織階層を段階的に選択するコンポーネントを作ってみた。
動きとしては「コンボボックスA」で上位グループを選択すると「コンボボックスB」にはその配下グループが候補に設定されていく感じ。Aの選択を切り替えると、Bの候補も切り替えたい。
進捗
Aで選択した上位グループに合わせてBの候補が更新されるところまではできた。
問題点
一度Aで上位グループ(ex:海の生き物)を選択してBでグループ(ex:魚)を選択したあと、再びAで別の上位グループ(ex:空の生き物)を選択し直すと、直前に選択したグループ(魚)がBのテキスト部分に残ってしまっている。
ただ候補は問題なく切り替わった。
Aで上位のグループが切り替わる都度、Bのテキストをクリアしたいんだけどどうすればいいのだろう🤔
<Autocomplete
inputValue=""
(略)
/>
で空にできたんだが、今度は候補を選択してもテキストが空のままになってしまった。ぐぬぬ。
こんなコード書いて試してみた
import { useState, useEffect } from "react";
import { Autocomplete, TextField } from "@mui/material";
const Hoge = () => {
const [refresh, setRefresh] = useState(false);
return (
<div>
<Autocomplete
disablePortal
id="group"
options={["aa", "ii"]}
renderInput={(params) => <TextField {...params} label="Group" />}
onInputChange={()=>{}}
/>
<button onClick={() => { setRefresh(!refresh) }} >click</button>
</div>
);
}
clickボタンでstateを更新して強制的にレンダリングさせてみるも動作は変わらず、基本的に表示が残ってしまう。
コンポーネントが若干違うものの連動するコンボボックスで同じような挙動が起きた時の解決策が見つかった。
値はvalueで設定できるので、親コンボボックス、子コンボボックスそれぞれのonClickで常にvalueを最新の状態に更新し続ければ良いようです。(言葉での説明が難しい)
value使わなくても単体では全然問題ないので、こんな設計があるんだと目から鱗。
他人の綺麗なソースコードはめちゃくちゃ勉強になりますね。
いまいち混乱するのでイベント実装するときのメモ
(例)クリックで「hoge」と表示する
#1直接書く場合
const App = () =>{
return(
<button
onClick={()=>{alert("hoge")}}
>
click
</button>
);
}
#2関数化する場合
const App = () =>{
const sayHoge = () =>{
alert("hoge")
}
return(
<button
onClick={sayHoge}
>
click
</button>
);
}
これは以下の書き方でもOK
const App = () =>{
const sayHoge = () =>{
alert("hoge")
}
return(
<button
onClick={()=>{sayHoge()}}
>
click
</button>
);
}
#3親コンポーネントから受け取ったコールバック関数を渡す場合
const Parent = () =>{
const parentMethod = () =>{
alert("hoge");
}
return(
<App method={parentMethod} />
);
}
const App = (props) => {
const { method } = props;
console.log(props.method);
return (
<button
onClick={method}
>
click
</button>
);
};
これも以下の書き方がOK
const Parent = () =>{
const parentMethod = () =>{
alert("hoge");
}
return(
<App method={parentMethod} />
);
}
const App = (props) => {
const { method } = props;
console.log(props.method);
return (
<button
onClick={()=>{method()}}
>
click
</button>
);
};
だがまだよくわからん。。
カスタムフックと仲良くなりたい
React実践の教科書を参考に...
本書の最後に記載されているTypeScriptを使ったメモ帳アプリの実践編を試してみた。
ローカルで開発環境を新規作成しても良かったけど、せっかくなのでCodeSandBoxを使ってみる。
(ただ理由はよくわかっていないんだがstyled-componentsのインポートがうまくできず、その部分だけ実装していない。)
動作
テキストボックスに入力した後に追加ボタンを押すと、削除ボタンとともに下に1行のメモ書きが出る
リファクタリングまでの流れ
- App.tsxにざっと実装
- App.tsxからメモ一覧の表示部分をコンポーネント化
- App.tsxのメモ一覧に関する部分をカスタムフック化
という流れで段階的にリファクタリングしていった。
コンポーネント化
App.tsx
テキストのstate
メモのstate
メモ追加処理
メモ削除処理
テキストボックス表示
追加ボタン表示
メモと削除ボタン表示
を
App.tsx
テキストのstate
メモのstate ※
メモ追加処理
メモ削除処理(useCallback使用)※
テキストボックス表示
追加ボタン表示
メモと削除ボタン表示(コンポーネント化)
MemoList.tsx(※をpropsで受け取る)
メモと削除ボタン表示
に分解する。コンポーネント化は「表示するメモの配列」「削除関数」の2つをpropsとして受け取って実装すれば良いので比較的簡単にできた。
カスタムフック化
カスタムフック化は、メモの追加、メモの削除関数をApp.tsxに残しつつ、処理の中身をカスタムフックの方に移動した。(うまく口で説明できない)
App.tsx
テキストのstate
メモのstate ※
メモ追加処理
メモ削除処理(useCallback使用)※
テキストボックス表示
追加ボタン表示
メモと削除ボタン表示(コンポーネント化)
MemoList.tsx(※をpropsで受け取る)
メモと削除ボタン表示
を
App.tsx
テキストのstate
メモのstate
メモ追加処理 ※中でuseMemoListの追加関数を呼ぶ
メモ削除処理 ※中でuseMemoListの削除関数を呼ぶ
テキストボックス表示
追加ボタン表示
メモと削除ボタン表示(コンポーネント化)
useMemoList.ts
メモのstate
メモの追加処理詳細
メモの削除処理詳細
※上記3つを戻り値として返す
MemoList.tsx
メモと削除ボタン表示
こんな感じにした。伝わる気がしない。
あとコールバック関数にした関数はuseCallbackを漏れなくつけている。
毎回関数の更新をする必要がないはずなので第二引数でstateを設定しているのだけど、自分の開発ではuseCallback使うところまでは思いついても第二引数に若干迷いそう。
オライリージャパンのReactハンズオンラーニングに金言が。
要約すると「みんなmemoとかuseCallbackとか乱用するけど、Reactは頻繁に描画されてもパフォーマンス落ちないように設計されてるしコストに見合った成果は得られないから、どうしても気になったところにだけ使うようにするといいよ」とのこと。気にしすぎはよくないとさ。
Atomic Design風にコンポーネントを分けてみる
こちらを参考にしました。
Atoms:
・汎用的な機能を提供する。
・ドメインが入ってはいけない。
・Contextへのアクセスはしない。
・自分自身で状態はなるべく持たない。
・他のコンポーネントに依存していなければAtoms。
Molecules:
・汎用的な機能を提供する。
・ドメインが入ってはいけない。
・Contextへのアクセスはしない。
・自分自身で状態はなるべく持たない。
・他のAtomsやMoleculesのコンポーネントに依存している。
Organisms:
・ドメインが入ったらOrganisms。
・他に依存するコンポーネントがなかったとしても、ドメインが入った時点でOrganismsにする。
・useContextによるContext接続可。
・その機能のためのAPIを叩くのはここ。
Templates:
・部分導入した範囲内のレイアウトを決める。
・ロジックは持たない。
Pages:
・現在はただのラッパーに近い。
基本的に上記の考え方でコンポーネントを作り、既存のコンポーネントは必要に応じて分解してみる。
- AtomsとMoleculesの違いは「コンポーネントに依存しているかどうか」
- MoleculesとOrganismsの違いは「ドメインが入っているかどうか」、「コンテキストを使うかどうか」
で判断が簡単にできそう。すごくわかりやすい。
そして参考にしたサイトではTemplatesでレイアウトを管理しているようだったんだけど、ちょっとイメージが湧いていない。子や孫やひ孫コンポーネントまでのレイアウトをまとめてバケツリレーしていく感じだろうか。孫が複数いたらどうする、などちょっと実装に一工夫必要かもしれないけど、そんなに難しくはなさそう。
エラーの受け渡しについて考察(別にエラーじゃなくてもいいんだけど)
例えばこんなコンポーネントがあったとする
押すとエラーの状態になるボタン
const MyButton = () => {
const [isError, setIsError] = useState(false);
console.log(isError);
return (
<button onClick={()=>{setIsError(true)}} >beError</button>
);
};
このボタンのエラーを他のコンポーネントへどうやって渡すのか、思いつくパターンを考察してみる。
親コンポーネントにstateを持たせて更新する
親コンポーネントに渡したい場合は、親自身にエラーのstateを持たせて子に更新させる方法がある
const Parent = () =>{
const [isError, setIsError] = useState(false);
return(
<MyButton method={setIsError) />
);
};
const MyButton = (props) => {
const { method } = props;
return (
<button onClick={()=>{method(true)}} >beError</button>
);
};
こうすれば、子のコンポーネントが親コンポーネントのisErrorを更新し、親コンポーネントにエラー情報が渡せる。useEffectなんか使えば状態の変化をトリガーに処理できる。
ちなみにsetIsErrorを直ではなく、ラッパーで包んだっていい。
コンテキストを使う?
DOMから調べる?
外部スクリプトの使い方についてハマったのでまとめておく
どういうこと?
Reactのコンポーネントといえば、必要なコンポーネントや関数はimportで参照するのが当たり前。
ところが世の中にはimportさせてもらえないjsファイルも存在する。
そんな外部スクリプトをどうやって使うの?という話。
具体例
「直接htmlにJavaScriptを書いてね」という前提で提供されているAPIの場合、恐らくそのjsファイルはこんな内容になっている。
function hogehoge(){
//ホゲホゲする処理
}
function fugafuga(){
//フガフガする処理
}
importするために必要なexportが入っていない。
本当はこんな形で欲しかったはず。
export function hogehoge(){}
export function fugafuga(){}
となると、このjsファイルは必然的にindex.htmlのScriptタグを用いて読み込むことになる。
<script src="./api.js" type="text/javascript"></script>
create-react-appを使っている環境だったら、publicフォルダにこんな感じで置くことになる。
public/js/api.js
public/index.html ※scriptタグで./js/api.jsを参照
じゃあインポートしてみるか。
import * as api from "../js/api" //エラー(publicは参照不可/exportがないので無理)
あれ、これどうやってコンポーネントに読み込むの?
解決策
結論から言うと、コンポーネントに読み込む必要はない。
なぜならコンポーネントは最終的に普通のJavaScriptにコンパイルされ、scriptタグで読み込まれた関数を参照できるようになるから。
const App =() =>{
hogehoge();
fugafuga();
return(
//レンダリング
);
}
でもこれだけでは定義されていない変数名(関数名)でエラーが起きる。
色々調べたけどESLintだったらun-defエラーが出るし、TypeScriptだったらTS2304が出る。
私の環境では後者がでた。だったらこれ!アンビエント宣言をしておけば良い!
declare var hogehoge: any;
declare var fugafuga: any;
これでコンパイルが通るはず。この情報、調べて解決策まで見つけるのに苦労したので残しておく。
参考にしたサイトも敬意を表して載せておきます。
Vite環境で再始動します