ReactでdetailsをoutSideClickで閉じるやつ

自作のアニメ視聴管理webサービス watch-duty-manager にこのようなメニューUIがある。ハンバーガーを押すとメニューが開く。これはHTMLのdetails/summaryタグで実現している。

こういうメニューはメニューの外側をクリックすると閉じるのが一般的だ。しかしdetailsタグは標準でそのような挙動をしない(まあメニューにだけ使われるものではないからね)。summary(ハンバーガー部分)をもう一度押すことで閉じることができる。外側クリックで閉じる方法を適当にググってみると

  • detailsが開いているときはcssでsummaryを画面全体を覆うように巨大化させる
  • JavaScriptのクリックイベントを全て拾ってなんとかする

という2つの方針が見つかった。1はなかなか賢いと思うものの、真面目に作り込もうと思うとなかなかバギーな部分もありそうで(特にレイヤー順とか)やめて2をやることにした。

みんな大好き react-use に useClickAway というカスタムフックがある。refで参照されたHTML要素の外側がクリックされたときに指定されたコールバックを実行する。実装としては愚直に2をやっているだけ。

// https://github.com/streamich/react-use/blob/ad33f76dfff7ddb041a9ef74b80656a94affaa80/src/useClickAway.ts#L16-L22

const handler = (event) => {
  const { current: el } = ref;
  el && !el.contains(event.target) && savedCallback.current(event);
};
for (const eventName of events) {
  on(document, eventName, handler);
}

これを使って

function Component() {
  const ref = useRef<HTMLDetailsElement>(null);
  useClickAway(ref, () => {
    if (ref.current && ref.current.open) {
      ref.current.removeAttribute("open");
    }
  })
  return (
    <details ref={ref}>
      <summary>summary</summary>
      details
    </details>
  )
}

てな感じでやれば一応動くものは作れる。作れるのだが、この作りだとdocument内のどこをクリックしても、画面内のこのコンポーネントの数だけ ref.current && ref.current.open の評価が走る。実際のユースケースでは100個くらいの表示はあり得る。まあ実際のところ100個あろうと大したコストではなく、使用感に影響を与える16msには届かないのだが、個人開発だからそういう現実的な損得はおいといて、もっと効率的な作りを追求することにした。

この作りの何が無駄かと言えば同時に開くdetailsは1個しかないのに画面内のdetails全てに対して同じ判定関数を与えていることだ。そこでデフォルトではなにもしない関数を渡しておいて、detailsが開いたときだけuseClickAwayに渡す関数を差し替えることにした。

const noOpCallback = () => {};

const useCloseDetailsOnClickAway = () => {
  const ref = useRef<HTMLDetailsElement>(null);
  const closeCallback = useCallback(() => {
    if (ref.current) {
      ref.current.removeAttribute("open");
    }
  }, [ref.current]);
  const [onClickAway, setOnClickAway] = useState({ f: noOpCallback });
  const onToggle = useCallback<React.ReactEventHandler<HTMLDetailsElement>>(
    (_) => {
      if (ref.current === null) {
        return;
      }
      if (ref.current.open) {
        setOnClickAway({ f: closeCallback });
      } else {
        setOnClickAway({ f: noOpCallback });
      }
    },
    [],
  );
  useClickAway(ref, onClickAway.f);
  return { ref, onToggle };
};

function Component() {
  const { ref, onToggle } = useCloseDetailsOnClickAway();
  return (
    <details ref={ref} onToggle={onToggle}>
      <summary>summary</summary>
      details
    </details>
  )
}

その結果できたカスタムフックがこれだ。detailsのtoggleイベントを監視して、開いたものだけuseClickAwayのコールバック関数をcloseCallbackに差し替えている。

ちなみに数字上の速度差はなかったです。たぶんReactやV8がなんとかしてるんでしょ。

 

20250110 おしぼり/タイミングを逃す

微妙な睡眠からの出勤。なかなか良い労働、退勤、ジム(有酸素のみ)。

金曜だしお腹が空いていたのでジム帰りにコンビニで肉まんとモンブランを買って、いつもの通り袋スプーン要らないって高速詠唱したら、「じゃあ(肉まん用の)お手拭きはもらってください」と言われてつけられてしまった。もらってくださいってなんだ、よくわからない。食事をする前に手を拭くべき人間だと思われたのだろうか?呆気に取られたのと「クレジットで」の詠唱とタイミングが被ってしまったので断れなかった。関西のおばちゃん的なサービス精神だったんだろうか。

遊戯王にはタイミングを逃すという概念がある。ざっくり言うと「Xした時Yできる」という任意の誘発効果は、チェーンの逆順効果解決等でXの直後に他の処理が入ってしまった場合、発動できないというルールだ。たぶんもとから知ってる人以外には意味不明な説明だろう。さらに面白いのは「Xした場合Yできる」という書き方だったり「Xした時Yする」という強制効果だったりする場合はタイミングを逃さず発動できる。そんなのオフィシャルに問い合わせないとカードの文章からは読み取れないだろ…

しかしまあ、この辺りの煩雑さも度重なる電子ゲーム化(カードゲームがコンピューターゲームになることをなんと言えばいいんだ?)によって否応なしにプログラム化され整理されたことだろう。遊戯王の膨大なカードプールを一通りゲームで使えるようにプログラムで記述するの、どれだけ大変だったんだろう。

これなんすよね

20250109 集合/絶叫

大学の後輩の卒論提出を祝って飲酒が催されているとのことで参加してきた。僕が着いたときは既に店を出て路上に移っており、通りすがりの知らない人(????)まで参加して盛り上がっていた。彼を祝い、他の後輩たちにHOW Y’ALL DOING?し、先輩に最近どすか仕事…してきた。短くも楽しい時間だったが、それはそれとしてとにかく寒い。二重ズボンとモコモコダウンで行くべきだった。普段は近い範囲で短時間しか外出しないので油断していた。

帰りの電車で、降りる人が席にスマホを落としていった。急いで拾って「お兄さんスマホ落としました」と絶叫したけどなかなか気づいてもらえない。さりとて僕は降りるわけにはいかない。ホームにいる人たちが一緒に叫んでくれて、なんとかスマホを渡すことができた。普段は大きい声を出さないけれど今日はちょうど出した帰りだったので良かった。

駅から自宅までの帰り道は風が強くなって一層寒く、とても辛かった。暖かい部屋に勝る幸せというのはそうそうない。

20250109 暗い

3時頃に中途覚醒し、再入眠まで60分を要した。その後の睡眠の質も悪く、11時に労働開始。内容は上々。

昼食は松屋の普通の牛丼(これは意外にも栄養バランスが良い、味噌汁抜くとなお良い)。

退勤、ジム、Dota2(敗)。勉強会は時間が合わずサボった。

ジムではNHKラジオニュースを聞いていたが、今日は気分が上がらず、どうして世界の裏側の悲しいニュースをどうにもできない俺が聞かなきゃいけないんだと嫌な気持ちになったのでやめた。

それでも気分が良くない日というのは自分からも嫌な情報を集めてしまいがちで、redditでは外国人たちが海賊版アニメサイトの閉鎖に対して「使いやすくて収録作品が多くて安いただ1つの配信プラットフォームに集めてくれればお金を払って見るのに…」みたいなくっだらねえ言い訳をしていて、作ってくれる人届けてくれる人への尊重がない、というよりは嘘でもあるかのように振る舞うという社会常識が存在しないんだなあとがっかりした。二度と日本のアニメーターの待遇の話するなよ。

架空のキャラクターに夢中になるということについて考えていた。これまでの人生で何度かそういう経験はある。キャラクターのstaticな属性そのものが好きというよりは、キャラクターが描かれていたり描かれていなかったりする様々な出来事に触れてどう反応していくのかという、ストーリーとの絡みまで含めて好きだ。一度夢中になると描かれていない生活の一部分まで想像して、他の人がどう考えているかについても一生懸命調べ回る。そして本編の供給が終了することを恐れる。しかし実際にはストーリーの最後の最後まで夢中であり続けることは少なく、まあそんなもんかという気持ちで終わりを迎えることが多い。大抵の面白いコンテンツは面白くなくなるまで続けられてその後終わるのでそうなるよね。

そろそろ寝るべき時間だが寝る気分にならない。困ったなあ。

20250107 七草粥

9時起床。散歩、朝食、労働(まあまあ)。

昼食、七草粥(概念)

退勤。ジムは昨日ガッツリやって若干関節に不穏を感じたので休み。Dota2(勝)、夕食、勉強会、個人開発(eslintのバージョン上げ)。

寝。

20250105 作らされたいもの/RimWorld終盤の資産管理と人生訓

作らされたいもの

作りたいものねぇ〜とか言っていたら昔作ったwebアプリケーションをまだ使ってくれている友人から機能の要望が来て、若干の修正と膨大なリファクタリングが発生していて楽しい。しかし連休の終盤に楽しくて夜ふかししているのは頭が悪い。

昔作ったものだから結局コンフォートゾーンに逆戻りしているということではあるんだけど…まあ何もしないよりはマシかな。TypeScript+prismaでデータベースを複雑にいじくるところで(mysqlだとcreateManyAndReturnが使えません)、fp-tsで型をガチガチに固めているので絶対間違ったプログラムを書けないのだが、だからと言って正しいプログラムが書けるというわけでもなく苦労している。fp-tsの型エラーは原因箇所と遠く離れた場所に出るので何を間違ったのか探すのにコツがいる。好きな思想のライブラリではあるんだけどやっぱり言語機能の支援なしにこういうことをやろうとするとどこかに無理が生じるのよね…。

本日

今日からジムが開いていたので行って全身シバいてきた。少し衰えていたが想定内だ。NHKラジオニュースではUSスチールの買収差し止めが大きく扱われていた。大変だ。昼は久々に餃子の王将に行ってみたが、日高屋よりは美味しいだろうと期待しすぎた結果思ったほどじゃないな…となった。かぼちゃを買って帰宅。

RimWorld終盤の資産管理と人生訓

RimWorldは佳境である。いよいよ宇宙船の研究が一通り完了し、後は地道にウランやらプラスチールやら先進コンポーネントやらをかき集める段階に入った。ここの進め方がなかなか難しい。コロニー経営は軌道に乗っていて金も食料も有り余っているのだが、希少資源はそうそう見つからないので金余りの状態になりやすく、そのせいでゲームシステムによって敵の襲撃が強力になってしまう。かと言って金を捨てるといざ希少資源が見つかったときに買えない。つまりゲームシステムにバレない場所に資金を移動させる必要があるのだ。

一つの方法としてはキャラバンに乗せて適当な人員にコロニー外を徘徊させるというのがある。物理的な資産隠しだ。しかしこれはあまりにもゲームを騙してる感が強くポリシーに反するのでダメ。他の方法としては近隣の他勢力に配っちゃうというのがある。もちろんその資金は消滅するのだが、その分友好度が上昇し、いざというときに助けに来てくれたりキャラバンを派遣してくれたりする。

人を働かせて金を集めるシステムを構築したら次はその金を隠したり配ったりというのは嫌なリアリティがあるが、溜め込むくらいなら人に配ってしまえというのはなかなかポジティブな人生訓ではないだろうか。資産を持つことはそれ自体がリスクである。価値は損耗・変動するし、奪われる可能性もある(現代日本の銀行だって1000万円までしか保証してくれないぞ)。それに対して、ロマンティックに言えば友情、システマティックに言えば互恵性の規範に投資するという道もあるぞということをこのゲームは提示しているのだ(厳密に考えると金銭投げつけは友情なのか?という疑問は新たに浮かんでくるが…)。

ちなみに住人の一人であるElk(39歳独身男性・社交能力1)は性欲が強すぎて人妻を含む周囲の女性に声をかけまくり、当然のごとく振られ続けた結果メンタルを悪化させ暴れだしたので逮捕・収監と相成りました。そのうち復帰はさせようと思うけどもう女性と顔を合わせないように深夜シフト固定かなあ。これはネガティブな人生訓。

20250102 RimWorld Mod紹介

今年は帰省はなし。血縁共同体への参加を諦め、自宅で孤独で穏やかな1月1日を過ごした(それっぽいことを何もしていないのでもはや正月ではない)。

休日は好きだが、あまりに長いと人生の意味について考えはじめて非常に精神に悪い。特に正月は30歳独身男性としての何の希望もない未来のことを。せっかく薬で意味のない思考をしないように止めているのに。

30歳になるとあらゆる挑戦心が枯れる。趣味も誰にも褒めてもらえないなら意味ないなとなるし、なにか技術的な勉強をしようかと思ったけど作ってみたいものが思い浮かばず結局大したことをしていない。「やりたいけど時間がない/他の欲求に負けた」のではなく、何事も別にそこまでやりたくないのだ。悲しいことだ。やりたいという欲求が薄まるので、欲求が満たされない苦痛すらそれほど感じない。だから平穏だ。平穏な死。

これまでの人生に満ちていた無意味な不安に比べれば、平穏な死であろうと平穏であることには価値があり、僕は今平穏を自ら望んで生きている。帰省していたら自分の人生にまだ何からの期待を持っている人と会わねばならないので、今年は避けて正解だったろう。

RimWorld Mod紹介

ポリシーとしてゲーム性を変化させるmodは入れない。UIと操作性の改善だけ。

  • Interaction Bubbles
    • 定番。にぎやかい。楽しい。
  • Colony Groups
    • pawnが10人を超えた辺りから前衛と後衛の衣類ポリシーの手動変更ポチポチが面倒になってきたので導入。一括変更が可能。
  • Numbers
    • 自分で好きなカラムを指定した表が見れる。各pawnの空腹度や睡眠を縦覧するときに役立つ
  • Moody
    • コロニー全体のmood上昇/下降要因を集約された形で見れる。やはり人数が増えてくると個人ではなく全体として確認し対応せねばならないので役に立つ
  • RimHUD
    • 定番。でも意外と必須でもないかも?
  • RPG Style Inventory
    • どうしても各装備のカバー範囲とレイヤーの概念を覚えられないので
  • Relations Tab
    • pawnの人間関係を一覧するために最も適した形はグラフであって、当然こういうmodが存在してしかるべきと考え、探したら案の定あった
  • More Graphs
    • 各種リソースの時系列グラフが確認できるようになる。コロニーが発展してくるとリソース管理も全体として捉える必要が生じる。

大規模集約型農業の苦悩

1人のpawnが1日に消費する栄養は20。年間(60日)だと1200。

米とトウモロコシの生産効率はほぼ同じなので米(およそ6栄養/6日)で考える。1年間のうち米が栽培できる期間は、気候によるが、温帯森林だと40日間なので6サイクル栽培できるとすると、諸々の計算の結果34タイル/pawnの栽培面積が必要になる(収穫失敗・火災・病害のリスクを含めるとさらにマージンが必要)。

10人いると340タイル。これはなかなかの規模だ。これだけの規模になると農作業・運搬作業のリソース軽減を考える必要が出てくる。

農作業のリソース軽減は、やはりトウモロコシ栽培だ。トウモロコシは米の約3倍の生育期間で約3倍の収穫が得られる。農作業が1/3で済むというメリットがあるが、収穫の頻度が落ちるので十分な保管庫が必要になる。火災・病害でロットがまるごとダメになると次の収穫が得られるのが20日後になり、非常にリスクが高い。

一般的なサイズの冷蔵庫はフル棚化して容量3倍にしても不十分になるだろう。その場合は冷蔵しない第2保管庫を立てるのか良い。常温でも1年位保つし、分散保管は火災へのリスクヘッジになる。常温保管庫を優先して非常食化するのも良い。

20241221 ガスコンロカバー 指と服

ガスコンロカバー

炊飯器を買い替えたら上面ディスプレイになったせいで電子レンジの上に置くと身長を超えてしまって操作できないという問題があった。しばらくは台所の調理スペースに置いてしのいでいたが、noshを含めて食品の加熱は電子レンジしか使っていないので、ガスコンロは不要であり収納空間にできることに気づいた。そういうときに使うガスコンロカバーという既成品があるのだが、合うサイズを見つけるのが難しいし、炊飯器(6kg)を載せられる材質のものは高くなりがちなので自分で作ることにした。

構造は単純で、ガスコンロの手前と奥に角材を置いてその上に天板を載せればいい。簡単に図面を書いて近くのホームセンターへ行き、店員と相談しながら材木を選んでカットしてもらった。それでも大きいので持ち帰るのが大変だったが…技術料含めて3500円程度でいい具合のものが作れた。

ちなみに新炊飯器はそこそこ良いやつで冷凍ごはん用の炊飯モードがあるのだが、これがなかなかちゃんと解凍後も弾力があり、本当に効果あるんだなあと感心している。QOLチャリンチャリン。

指と服

犬の動画を見ていると、冬は服を着せている人が多い。しかし犬は服を自分の意思で着脱できるのだろうか。やはり犬や猫の手足の器用さでは文明は発展しなかっただろうなと思う。

同じように宇宙の彼方の高度生命体は、ふっとい指が10本しかない、そもそも3次元しか認識できない人類のことを哀れんでいるのだろうか。

攻殻機動隊の、各指が更に枝分かれして超高速でキーボードを打鍵できる(セキュリティが厳しい公安警察のオペレーターは電子的干渉を避けるために物理デバイスであるキーボードを使う←そうだったよな…?と思って確認したら別にそういう設定ではないらしい?)アンドロイドたちのことを考えている。

20241220 日記

0:30入眠。4:30起床(怒)。7:30再入眠に成功。10時ごろ起きて労働。典型的な早寝(当社比)からの早期覚醒のパターン。

徐々に周囲の人が年末休みに入っていくことで進めにくくなる作業が増えたので、年末大掃除的なやつをしよう…と思っていたら一人で黙々と進められてしまう作業が発生し、そこそこ進めて退勤。

ジム。iPadを忘れたので『ゲーム・オブ・スローンズ』が見れなかったが、代わりにテレビで放送していた全日本フィギュアスケート選手権男子ショートを見た。37歳織田信成が4T-3Tをクリーンに決めたのはぶったまげた(まあ出る大会を減らせばパフォーマンスは出せるというのは相撲もそう)。それにしても踊ったり跳んだり滑ったり、相変わらず変なスポーツだ。せっかく氷の上を滑るという独自性があるのだから滑りの上手さをもっとよく点数化すればいいと思うのだが、どうにもジャンプが重視されていてもやっとする。

帰宅、夕食、勉強会、Dota2(勝)、ゲースロ、日記、睡眠薬。

明日は何をしようかな。読書でも昼寝でも、何もしなくてもいい。スケートを見ていたせいでいつもより長くエリプティカルしてしまったので明日は膝を休めたい。常備菜は小松菜が切れたのでまたなにか作らないとな。冬の野菜ということでカブでも使ってみるか?

20241219 日記

起床、散歩、掃除、労働。ジム、夕食、勉強会、Dota2(敗)。日記、睡眠薬。

最近は睡眠薬の効きが悪くなってきた。まあ耐性が付く薬なので仕方ない。昨夜は入眠まで2時間かかってしまった。しかし遅寝遅起きになると早期覚醒は起きなくなる。不思議だ。

寝れない時間はしょうもないので会社で勧められた『【新版】日本語の作文技術』(本多勝一)を読んでいた。あまりにも思想性の強い序章に衝撃を受けたが、その後の各論は具体的かつ実用的であり、納得感がある。

最近は『ゲーム・オブ・スローンズ』を見ている。血族と名誉で殺し合いしてるのでまあ大体『仁義なき戦い』で、その前に見ていた『グッド・ワイフ』では全員ゼニ!出世!裏切り!仁義なんかクソ喰らえ!という世界観だったので落差が大きい。時代性と神秘性を感じさせる舞台の作り込みは本当にすごい。でも登場人物があまりにも多くて、それらの関係が断片的なセリフから浮かび上がってくるので把握するのが難しい。完結済みの作品なので迂闊にググるとネタバレを踏むし自分でNotionに整理しようとしてもAI補完でネタバレされそうになって笑ってしまった。