Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

ブロックを使うと prevValue を使うフックが書きやすい

小技。

「一時的な変数を useRef で定義したい」といった時に便利なテクニックです。

{
  const prevValue = useRef(value);
  useEffect(() => {
    if (prevValue.current === value) return;
    // 変化したタイミングで行う処理
    prevValue.current = value;
  }, [value]);
}

やり方は簡単です。↑こうやってブロック({})で囲むだけです。コレで prevValue のスコープを狭められます。フックのルールにも抵触しません。

値の変化を検知して副作用を実行したいときに僕はよくこの書き方をします。

conclusion

カスタムフックに抜き出すほどでもないときに便利なテクニックですが、慣れるまではすごく違和感ある書き方だと思います。チームに導入する時などはお気をつけください。

デスク環境変遷まとめ (2020/5 ~ 2023/1)

最新と最古を比較

結構長くなったので最初にどーんと比較。

…いうほど変わってないか?

2020年

5月 入社直後

一番最初。HHKB を買った、デスクを買った以外はほぼ大学のときと同じ構成だった気がする。 https://twitter.com/le_panda_noir/status/1265958137686704129

ちなみに(写真はないが)これ以前はコタツテーブルに座椅子で仕事してた。今思うと腰を痛めた原因はこれだった気がする…

7月 配線を改善

https://twitter.com/le_panda_noir/status/1286622266277556226

10月

この辺でまあまあ整った。 https://twitter.com/le_panda_noir/status/1312018560412049413

2021年

1月 デスクを新調

このデスクは IKEA の BEKANT。かなり大きくて使い勝手が良かった。1年以上メインデスクだった。 https://twitter.com/le_panda_noir/status/1355755124325449728

あと、デスクトップPCを購入した。

6月 Moonlander(分割キーボード)を購入 + モニターを2枚に増設

https://twitter.com/le_panda_noir/status/1400079140645085189

10月 引っ越し

今の家に引っ越してきた。 https://twitter.com/le_panda_noir/status/1448662856950448132

2022年

1月 机をきれいにした(?)

この時期は初代デスクを横に配置していた

https://twitter.com/le_panda_noir/status/1479098720281329673

確か4月ごろに妹にモニターを1枚あげたので、これ以降はモニターが1枚に減っている。

4月 昇降式デスク購入

腰痛に耐えかねてついにFlexiSpot(昇降式デスク)を導入。今現在のメインデスク。

https://twitter.com/le_panda_noir/status/1519296576669503488

ちなみに昇降式は腰が壊れた日に急いで注文した。仕事を早退するレベルで壊したので、次の日から届くまでの1週間はスタンディングデスク(↓)を作って仕事してた。見ての通り不安定で、ディスプレイの重みでグラグラする。現場猫案件。 https://twitter.com/le_panda_noir/status/1516738200949379080

9月 ほぼ完成

今の環境はほぼコレ。キレイめに撮った https://twitter.com/le_panda_noir/status/1574373998380863488

2023年

1月 デスク断捨離

実はずっと昇降式の後ろに IKEA の BEKANT を置いていた。が、さすがに狭いなと思ったので捨てた。 ↓こんな感じで置かれていた。チェアを下げられないくらい狭い。

↓捨てた後。だいぶスッキリ。 https://twitter.com/le_panda_noir/status/1613894369207283712/photo/1

まとめ

時系列にするとこんな感じか。

  • 2020/5 HHKB、初代デスクを購入
  • 2021/1 デスクを新調、デスクトップPCを購入
  • 2021/6 Moonlander(分割キーボード)を購入、モニターを2枚に増設
  • 2021/10 引っ越し
  • 2022/4 モニターを1枚ゆずった
  • 2022/4 昇降式デスク、ウェブカメラ、マイクを購入
  • 2023/1 2代目デスクを捨てる

ちなみに表を見て気づく人もいると思うが、実は初代デスクはまだ捨ててない。押し入れのなかで棚として使ってる。

番外編: もう1部屋について

実は仕事部屋と別にもう一部屋ある。今 1DK に住んでいて、ダイニングキッチンにデスクを置いてるのだ。もう片方の部屋はこんな感じ。 https://twitter.com/le_panda_noir/status/1537819343400345600

本棚と筋トレ器具がある。今はさらにソファとホームシアター用のプロジェクターが置かれている。映画観るときとかコッチの部屋を使ってる。冬と夏以外はコッチの部屋に布団を敷いて寝てる(エアコンがダイニングキッチンにしかないので夏と冬は寝られない)

インターフェイスってなんぞ

インターフェイスは何かと何かの境界。

UI はユーザーとソフトウェアの間のインターフェイス。ソフトウェアは UI を通じて表示したりユーザーからアクションを受け取ったりする。

キーボードはユーザーと PC のインターフェイス。ユーザーはキーボードを叩くことで PC を操作している。

USB 端子や LAN 端子、コンセントとかもインターフェイス。というか接続する部分は大体インターフェイス。

API もインターフェイス。たとえば DOM API を使うとブラウザの表示を変えることができる。つまりブラウザとやり取りするためのインターフェイスなのだ。

関数やメソッドの引数、返り値もインターフェイス。関数とのやり取りは(基本的には)引数を通してしかできないから。

LSP のファイル名変更機能を使った mv コマンドを実装した

タイトルのとおりです。

作ったもの

tsserver には、ファイルを移動させると相対パスを自動で修正する機能があります。

例えば A.tsx を B.tsx にリネームすると、A.tsx を import していた箇所が自動で修正されます。

import {Component} from './components/A'; // これが './components/B' に修正される

この機能は language server のものなので、エディタ上でしか使えません。なので、language server を利用して相対パスの自動修正をしてくれる mv コマンドを作りました。

動作イメージ

成果物

動かし方:

  1. 以下のコードを mv.mjs という名前でプロジェクトのルート(node_modules があるフォルダ)に置く
  2. chmod +x mv.mjs する
  3. ./mv.mjs components/A.tsx components/B.tsx で動く!

colors と diff に依存しているので、まだインストールしてない場合はインストールしてください。

#!/usr/bin/env node
import 'colors';
import Diff from 'diff';
import { spawn } from 'node:child_process';
import { promisify } from 'node:util';
import {
  readFile as fsReadFile,
  writeFile as fsWriteFile,
  rename,
} from 'node:fs';
import { resolve, extname } from 'node:path';
import readline from 'node:readline';
const readFile = promisify(fsReadFile),
  writeFile = promisify(fsWriteFile);

// tsserver に getEditsForFileName request を送信して、結果を取得する
const getEditsForFileRename = async (from, to) => {
  const tsserver = spawn('./node_modules/typescript/bin/tsserver', []);

  tsserver.stdin.write(`${JSON.stringify({
    type: 'request',
    seq: 0,
    command: 'open',
    arguments: {
      file: from,
    },
  })}
    ${JSON.stringify({
      type: 'request',
      seq: 1,
      command: 'getEditsForFileRename',
      arguments: {
        oldFilePath: from,
        newFilePath: to,
      },
    })}
`);

  for await (const message of (function* f() {
    while (true) {
      yield new Promise((resolve) =>
        tsserver.stdout.on('data', (chunk) => {
          resolve(chunk.toString());
        })
      );
    }
  })()) {
    // getEditsForFileRename のレスポンスが出るまでループする
    for (const data of JSON.stringify(message).split('\\n')) {
      // JSON じゃないものが入りうるので、try で囲んでいる
      try {
        const json = JSON.parse(data.replace(/\\"/g, '"'));
        if (
          json.type !== 'response' ||
          json.command !== 'getEditsForFileRename'
        ) {
          continue;
        }
        tsserver.kill();
        return json;
      } catch {}
    }
  }
};

const applyTextChanges = (file, textChanges) =>
  textChanges
    .sort((a, b) =>
      a.start.line === b.start.line
        ? a.start.offset - b.start.offset
        : a.start.line - b.start.line
    )
    .reduce((res, { start, end, newText }) => {
      const lines = res.split('\n');
      const textToChange =
        start.line === end.line
          ? lines[start.line - 1].slice(start.offset - 1, end.offset - 1)
          : [
              lines[start.line - 1].slice(start.offset - 1),
              ...lines.slice(start.line, end.line - 1),
              lines[end.line - 1].slice(0, end.offset - 1),
            ].join('\n');

      return `${res.slice(
        0,
        lines.slice(0, start.line - 1).join('\n').length + start.offset
      )}${newText}${res.slice(
        lines.slice(0, start.line - 1).join('\n').length +
          start.offset +
          textToChange.length
      )}`;
    }, file);

const main = async (from, to) => {
  // js, ts, tsx 以外は普通に rename する
  if (!['.js', '.ts', '.tsx'].includes(extname(from))) {
    rename(from, to, (err) => {
      if (err) {
        console.error(err);
      }
    });
    return;
  }

  const { body } = await getEditsForFileRename(from, to);

  // 変更点を列挙して、変更して問題ないか確認する
  for (const { fileName, textChanges } of body) {
    const file = await readFile(fileName, 'utf8');
    const newContent = applyTextChanges(file, textChanges);

    const diff = Diff.diffChars(file, newContent);

    console.log(`${fileName}:`);
    for (const part of diff) {
      const color = part.added ? 'green' : part.removed ? 'red' : 'grey';
      process.stderr.write(part.value[color]);
    }
  }

  const confirmInterface = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });
  const answer = await new Promise((resolve) =>
    confirmInterface.question('これらの変更をしますか? (y/n) ', (ans) => {
      confirmInterface.pause();
      resolve(ans);
    })
  );
  if (answer.toLowerCase() !== 'y' && answer.toLowerCase !== 'yes') {
    console.log('キャンセルしました');
    return;
  }

  // 変更を反映する
  for (const { fileName, textChanges } of body) {
    await writeFile(
      fileName,
      applyTextChanges(await readFile(fileName, 'utf8'), textChanges),
      'utf8'
    );
  }
  rename(from, to, (err) => {
    if (err) {
      console.error(err);
    }
  });
  console.log('変更しました');
};

const [from, to] = process.argv.slice(2);
if (typeof from !== 'string') {
  console.log('missing file operand');
  process.exit(1);
}
if (typeof to !== 'string') {
  console.log(`missing destination file operand after '${from}'`);
  process.exit(1);
}
const absolutePathFrom = resolve(process.cwd(), from);
const absolutePathTo = resolve(process.cwd(), to);

main(absolutePathFrom, absolutePathTo);

(ほんとはグローバルインストールできるコマンドとして配布したかったですが、ここまでで丸一日かかったので後日時間があればやります…)

2022年を振り返る

去年の → 2021年を振り返る - Panda Noir

仕事、趣味、プライベートについて書くぞい。

仕事

  • テックリードという肩書きを思う存分振りかざして仕事した
  • ずっと JS ファイルを TS に置き換える件を進めてた
  • テストもたくさん書いた。スナップショットテストもバシバシ書いたし、testing-libray や msw も使ったし、storybook と puppeteer 組み合わせてブラウザテストを書いたりもした
  • 「転職したい!」って思ってから準備するようじゃ遅いと思ってカジュアル面談をいくつか受けた

2年目からテックリードをしていて、今年も継続してやってる。交代制でリードをやる想定らしいので来年度のどこかで交代かも?

JS → TS とかテストカバレッジを上げたりとか、技術負債の返却をたくさんできた年だった。今年前半にテストカバレッジを上げたのが返済を推し進める上でうまく機能してくれた。

カジュアル面談も受けてみたりした。「失職しても拾ってくれそうなところがある」と思えたのでよかった。ドワンゴが良さそうと思ったし、向こうからも是非!みたいな反応をもらった(言っていいのかコレ?)。でも多分しばらくは転職しない。

ひとまずはもっと頑張って強くなるぞ!

趣味

  • 映画ばっかり見てた
  • 料理は可処分時間を持っていかれるから意図的にペースを落とした
  • 趣味グラミングはあんまやらなかった。一時期 sumamrize me の開発に熱中してたけどすぐ飽きちゃった
  • 合気道は感染が怖くて今年の前半はほとんど行かなかった。後半は徐々に行くようにしたし自転車で行ける道場にも通い始めた。
  • 12月に唐突にエレキギターを買った。かなり楽しい。音楽を聴く意識も変わったのでよき。

特に9月10月11月は映画を1日1本ペースで見ていた。9月以降だけで100本観た。当然可処分時間がかなり持っていかれたけど、「1日2時間何かに集中する」みたいな耐性がついた。来年はコレを英語学習に当てたい。映画をちゃんと英語で見れるようになりたいから…

ギターは ぼっち・ざ・ろっくの影響ではない。時期的にそう思われそうでなんか嫌だ。ぼざろ観てないのでマジでたまたま。音楽の鑑賞をもっと楽しむには弾く側の視点がいるなと思って始めた感じなので。まだ1か月だけどこの狙いは結構達成できてる。相対性理論の『気になるあの娘』の各フレーズを弾けるようになってきた(まだ通しては弾けない)

うむ、ギターと映画が趣味に追加されて、料理とルービックキューブが薄れてきた1年だった。

その他プライベートとか

  • 付き合い始めて別れた。別れた直後はだいぶ荒れたけどもう落ち着いた。
  • 同期と何回か物理的にあって飲み会とかした。

今年はマッチングアプリで会ってデートするのを結構やった。多分10回くらいデートした気がする。GW 前後はかなり予定を詰め込んでた。

けっこう外出 OK な雰囲気にもなってきたよね。デートとか飲み会とかそこそこ行った気がする。やっぱ楽しい。一人で家にこもっていても別にいいけど、そればっかりはきつい。

体感は一瞬だけど思い返すと今年えげつないくらい色々あったんだな。

去年たてた目標について

去年たてた目標たち

  • とりあえず現職でもっとしっかりリードできるようなりたい
  • web vitals 意識して開発したい
  • 開発確認をもっとしっかりやってQAさんの負担減らしたい(てか今の俺があまりに雑)
  • 仕事に慣れてきたけど、どうしてもやることがワンパターン(JSON色つけ係)だから、データベースとかedge workerとか他のことやりたい

しっかりリードは…まあできていたのかな。技術的負債を積極的に返却したりアーキテクチャを考えたりとかたくさんしたし、QA コストを下げるために色々画策したりしたし、できていたと言えなくもない。わからないや。年収は上がったので評価はされていそう。

web vitals はそこそこ意識してたかも。でも実際の行動には移せてない。てか組織内にそれ専用のチームがあるから俺がやる隙があまりなかった。

開発確認はけっこう頑張った。テストカバレッジをほぼ0%から60%まで引き上げた。これは結構偉い。テストの勘どころもかなり掴めた。

データベースじゃないけど今年は趣味で Prisma 使ってゴリゴリ開発したりとかしてたな。edge worker は全然やってない。来年こそ…てかそろそろ Rust ちゃんとやりたいっすなぁ。作りたいものなくてやる気が起きてない。

総評

趣味を増やしたりマッチングアプリをやったり、仕事以外の部分をメチャクチャ充実させた1年になった。体感は結構あっという間だったけど、かなり濃い1年だったな。

仕事はなんかデカい課題にぶち当たってそれをあーだこーだと考えるフェーズになってきた。自分以外の人に動いてもらったりする必要が出てきて結構むずかしくなってきた。

来年の目標とか

  • 引っ越したい。今の家は来年で2年目だけど、更新はしないつもり。駅から遠いんじゃあ…人を呼ぶにしても不便。
  • 転職はまだしない。
  • 仕事関係はどうだろう…来年は何がしたいかな。最近は新しい技術とかは趣味の方でまかなえばいいかと思い始めているので仕事はこのままでいいかって思ってる。
  • 結婚圧がかかり始める年齢なのでそっちも急ぎたい。
  • 英語のリスニングを、そこそこのレベルまで上げたい