ReactでUIコンポーネント作る粒度の温度差について
Checkboxのコンポーネントを作るになんでこれだけのコード量が必要になるのだろうか... Hooksもふんだんに使われている。
import React, { useCallback, useEffect, useRef, useState } from "react"; import styled from "styled-components"; import { CheckBoxIcon, CheckBoxOutlineBlankIcon, } from "components/atoms/IconButton"; import Text from "components/atoms/Text"; import Flex from "components/layout/Flex"; export interface CheckBoxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "defaultValue"> { /** * 表示ラベル */ label?: string; } const CheckBoxElement = styled.input` display: none; `; const Label = styled.label` cursor: pointer; margin-left: 6px; user-select: none; `; /** * チェックボックス */ const CheckBox = (props: CheckBoxProps) => { const { id, label, onChange, checked, ...rest } = props; const [isChecked, setIsChecked] = useState(checked); const ref = useRef<HTMLInputElement>(null); const onClick = useCallback( (e: React.MouseEvent) => { e.preventDefault(); // チェックボックスを強制的にクリック ref.current?.click(); setIsChecked((isChecked) => !isChecked); }, [ref, setIsChecked] ); useEffect(() => { // パラメータからの変更を受け付ける setIsChecked(checked ?? false); }, [checked]); return ( <> <CheckBoxElement {...rest} ref={ref} type="checkbox" checked={isChecked} readOnly={!onChange} onChange={onChange} /> <Flex alignItems="center"> {/* チェックボックスのON/OFFの描画 */} {checked ?? isChecked ? ( <CheckBoxIcon size={20} onClick={onClick} /> ) : ( <CheckBoxOutlineBlankIcon size={20} onClick={onClick} /> )} {/* チェックボックスのラベル */} {label && label.length > 0 && ( <Label htmlFor={id} onClick={onClick}> <Text>{label}</Text> </Label> )} </Flex> </> ); }; export default CheckBox;
個人的にはこれぐらいでよいのではと感じる。propでUIコンポーネントが受け取るパラメータをtypeで定義して代入する。場合によっては、useEffectやuseStateをonChangeに仕込むなりしたらいい。
type Props = { //idを追加 id: string; value: boolean; text: string; onChange: () => void; }; export const Checkbox = ({ id, value, text, onChange }: Props) => { return ( <div> <label htmlFor={id}> <div> <input type="checkbox" id={id} checked={value} onChange={() => { onChange(); }} /> </div> <div> {text} </div> </label> </div> ); };
CloudSQL にローカルから接続するメモ
GCP SDKのインストール(任意)
$ brew install --cask google-cloud-sdk $ source /opt/homebrew/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/path.zsh.inc $ source /opt/homebrew/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/completion.zsh.inc $ source ~/.zshrc $ gcloud --version Google Cloud SDK 404.0.0 bq 2.0.78 core 2022.09.23 gcloud-crc32c 1.0.0 gsutil 5.14
Cloud SQLに接続可能なユーザーを作成
$ gcloud iam service-accounts create sample-user --display-name "sample-user" --project <PROJECT_ID> $ gcloud projects add-iam-policy-binding <PROJECT_ID> --member serviceAccount:sample-user@<PROJECT_ID>.iam.gserviceaccount.com --role roles/cloudsql.client $ gcloud iam service-accounts keys create key.json --iam-account sample-user@<PROJECT_ID>.iam.gserviceaccount.com
Cloud SQL Proxyを入れて、立ち上げる。SSHトンネルのようなものか。
# M1 はこれ $ curl -o cloud_sql_proxy https://dl.google.com/cloudsql/cloud_sql_proxy.darwin.arm64 $ chmod +x cloud_sql_proxy $ ./cloud_sql_proxy -instances=<インスタンス接続名>=tcp:13306 -credential_file=key.json
あとはSQLクライアントソフトでつなぐだけ。ローカルホストで13306番に。お疲れさまでした。
SQLのINとEXISTSの違い
ユーザーテーブルと、そのユーザーが好きなプログラミング言語というテーブルがあるとします。
ユーザーテーブル(UserTable)
id | name |
---|---|
1 | 山田 |
2 | 村上 |
3 | 中村 |
好きなプログラミング言語テーブル(FavLangTable)
id | user_id | lang |
---|---|---|
1 | 1 | Python |
2 | 1 | PHP |
3 | 2 | TypeScript |
4 | 2 | C# |
5 | 2 | Java |
6 | 3 | TypeScript |
INとEXISTSの使い分け
結合先のレコードを全て欲しい場合は、EXISTS。そうでない場合は、INを使う。
TypeScriptが好きなユーザーを引いてくるSQLをINで書くとこうなる。
SELECT ut.name, f.lang FROM user_table AS ut JOIN fav_lang_table AS f ON f.user_id = ut.id WHERE f.lang IN("TypeScript")
結合先をINで絞っているので、そのレコードのみが返却されるため、こうなる。
name | lang |
---|---|
村上 | TypeScript |
中村 | TypeScript |
TypeScriptが好きなユーザーを抽出するならこれでいい。だが、TypeScriptが好きなユーザーの「他の全てのプログラミング言語」を同時に取得したい場合、うまくいかない。結合先をINで絞り込んでしまうと、結合先のレコードしか取得しないので。
こーゆー時は、EXISTSの出番。EXISTSはそのレコードの存在有無だけを見てくれるので、JOINした結合先のレコードを丸々取ってくる。
SELECT ut.username, f.lang FROM user_table AS ut JOIN fav_lang_table AS f ON f.user_id = ut.id WHERE EXISTS ( SELECT * FROM fav_lang_table AS f1 WHERE f1.lang IN("TypeScript") AND f1.user_id = ut.id )
こうなる。
name | lang |
---|---|
村上 | TypeScript |
村上 | C# |
村上 | Java |
中村 | TypeScript |
EXISTSはINに比べると可読性が下がる(ぱっと見て内容がわかりにくい)が、使いこなせると楽しいよ。
SQLAlchemyのEager Loadingをいい感じにやる
SQLAlchemy==1.4.37
N+1のlazy fetch
をしたくない場合、eager load
というJOIN時に予めデータを持ってくる方式があります。SQLAlchemyでも、Railsでも、この辺は一緒。Alchemyの場合、孫のテーブルをJOINした時もそれらが出来る。
数時間苦戦したので、Eager Loadingの設定をメモる。
filterする対象のテーブルが自分だけ
この場合は、joinedload
でいけた。
session.query(UserTable) .options( joinedload(UserTable.attention), joinedload(UserTable.friend_type), joinedload(UserTable.bookmark), ) .filter(UserTable.user_id == user_id)
UserTableを軸に、Where句で引いてくるのがUserTableのカラムのみ。
filterする対象のテーブルが複数ある
この場合は、以下のようなjoinedload
を使うと、えらいことになった。
session.query(UserTable) .options( joinedload(UserTable.attention), joinedload(UserTable.friend_type), joinedload(UserTable.bookmark), ) .filter(UserTable.user_id != user_id) .filter(UserFriendTypeTable.friend_type.in_(["AAA","BBB"])
えらいことになるというのは、filterで指定したテーブルが OUTER JOINではなくCROSS JOINされていたため。FROM user_table, user_friend_type_table というSQLになるので、激重になった。
複雑な検索条件に基づいてフェッチする場合は、join
とcontains_eager
の合わせ技が良いみたい。
session.query(UserTable) .outerjoin(UserBookmarkTable, UserBookmarkTable.user_id == UserTable.id) .join(UserFriendTypeTable, UserFriendTypeTable.user_id == UserTable.id) .options( contains_eager(UserTable.bookmark), contains_eager(UserTable.friend_type), ) .filter(UserTable.user_id != user_id) .filter(UserFriendTypeTable.friend_type.in_(["AAA","BBB"])
自分はいつもwith_entities
でカラムを指定してSELECT文を投げることが多かった。一覧画面に表示するためのSQLが多いので、JOIN先がN件でも親に合わせるためにカラムを指定してた。ただ、今回は、要件が特殊でカラムを指定するのではなく、N+1せずに結合先のレコードをリストで保持して置く必要があったので、苦戦したのだった。
これでだいぶRelationshipには慣れてきたな。ドキュメント、じっくり読もう。どこかで。
Asynchronous Gap on BuildContext
Flutter2.x系からこの問題がフィーチャーされるようになった気がする。1.xの時はそこまで。
どんな問題?
- 非同期でサーバーにデータを送った後、ダイアログを閉じるようなケース。
- 非同期で
await
を入れた所で、Flutterのウィジェットツリーの再構築が同期されたかは、全くもって保証されない。 - そのため、非同期処理後に
Navigator.of(context).pop()
とかやると例外が発生する。
TextButton( child: const Text("いいえ(エラーになる)"), onPressed: () async { await doSomething(); Navigator.pop(context); //このcontextが同期されない } )
At this point the state of the widget's element tree is no longer stable. To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method
どーするか
公式の推奨は、StatefulWidget
でWidgetのライフサイクルを管理する方式。
void onButtonTapped() async { await Future.delayed(const Duration(seconds: 1)); if (!mounted) return; Navigator.of(context).pop(); }
StatefulWidgetを使わない場合、await
する前の Context
をキャッシュすると良いみたいなStackOverflowがあったけど、そもそもキャッシュするもんじゃないだろこれっていう思いが強い。
onPressed: () async { final navigator = Navigator.of(context); // store the Navigator await showDialog( context: context, builder: (_) => AlertDialog( title: Text('Dialog Title'), ), ); navigator.pop(); // use the Navigator, not the BuildContext },
WPFでコンボボックスの選択値が共有できない
印刷プリンタのコンボボックスを作っていて、その選択値を様々な画面で使い回したいのだが、非常にハマった。今も解決できない。
ViewModelBase
というクラスを作り、そのプロパティを継承したら行けるかと思ったが、ダメだった。ViewModelBaseのコンストラクタのログを見ると、親クラスの値がそのままセットされているけど、画面が表示されるタイミングでSetProperty
を叩かないと無駄っぽい。
まぁそれはいいやで、A画面でプリンタを選び直して、B画面を表示しても、A画面で選択されたプリンタの選択値の通り画面が切り替わらない。アプリケーション全体にまたがってデータをバインドするっていうことが、WPF/Prismでは出来ないのだろうか。解決の方向性が全く見えない。
・・・これ、どうしたらいいんだろうなぁ。