Panda Noir

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

Next.js + Chakra UI で初期描画からダークモードを反映させたい

問題: なにも設定しないと初期描画のときにカラーモードが反映されない

demo

このように、Chakra UI でなにも設定しないとリロード直後に一瞬ライトモードで表示されます。この記事ではこの問題を解消する方法を紹介します。

tl;dr

ColorModeScript を追加する + initialColorMode をセットする

'use client';
export const ChakraProviderClient = ({
  children,
  initialColorMode,
}: PropsWithChildren<{
  initialColorMode: 'light' | 'dark';
}>) => (
  <ChakraProvider
    colorModeManager={cookieStorageManager}
    theme={extendTheme({ config: { initialColorMode } })}
  >
    <ColorModeScript type="cookie" />
    {children}
  </ChakraProvider>
);

対処1: ColorModeScript を追加する

demo

ColorModeScript をセットすると、最初から背景色や文字色が設定されるようになります。 (コンポーネントの色は一瞬ライトモードで表示されていますが、これは後述の initialColorMode で直ります)

ColorModeScript はこのように使います。

<ColorModeScript type="cookie" />

<ColorModeScript type="cookie" /> はページ読み込み時に cookie を見て body に light/dark クラスを追加してくれます(ColorModeScript の実装)。これによって背景色などがうまく機能するようになります。

対処2: initialColorMode をセットする

initialColorMode をセットすると、リロード時にもボタンの色が設定したカラーモードで表示されるようになります。(ColorModeScript との効果を比較するために、動画ではColorModeScript は抜いています)

demo

initialColorMode をセットしないと、サーバーレンダリング時点では設定で決まった色でレンダリングしてしまいます。そのため、設定したカラーモードで描画するために intialColorMode をセットするようにします。

<ChakraProvider
  colorModeManager={cookieStorageManager}
  theme={extendTheme({ config: { initialColorMode } })}
>
  {children}
</ChakraProvider>

intialColorMode はサーバーサイドで cookies().get('chakra-ui-color-mode')?.value で取得できます。あとはこれを ChakraProvider に渡せば完了…なのですが、ここで一つ注意すべき点があります。それは、Client component 内でないと theme={extendTheme({ config: { initialColorMode } })} を直接渡せないということです。これは、extendTheme で返されるオブジェクトのプロパティに関数が混ざっていて「クライアントコンポーネントに直接関数を渡せない」という制限に引っかかるのが原因です。

これに引っかからないように、クライアントコンポーネントの中で ChakraProvider に theme を渡すようにしてください。

完成形

以上をまとめるとこのようになります。

// ChakraProviderClient.tsx
'use client';
export const ChakraProviderClient = ({
  children,
  initialColorMode,
}: PropsWithChildren<{
  initialColorMode: 'light' | 'dark';
}>) => (
  <ChakraProvider
    colorModeManager={cookieStorageManager}
    theme={extendTheme({ config: { initialColorMode } })}
  >
    <ColorModeScript type="cookie" />
    {children}
  </ChakraProvider>
);
// layout.tsx
import { cookies } from 'next/headers';
import { ChakraProviderClient } from './ChakraProviderClient';

export default function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='en'>
      <body>
        <ChakraProviderClient initialColorMode={cookies().get('chakra-ui-color-mode')?.value}>
          {children}
        </ChakraProviderClient>
      </body>
    </html>
  );
}

demo

一応動画も撮りましたが、表示が一切変わらないのでわかりづらい…

逆FizzBuzzを解いてみた2

全然書いた記憶はなかったんですが、昔の僕は逆FizzBuzz という問題に挑戦してました。 逆FizzBuzzを解いてみた - Panda Noir

この解答コードがもうちょいシンプルに書けたので載せておきます

おさらい: 逆FizzBuzzとは?

  • [1,2,3,4,5,…] のように並んだ数列がある
  • これを FizzBuzz で変換する([1,2,Fizz,4,Buzz,…])
  • そこから数字を取り除く([Fizz,Buzz,…])
  • そうして出来た文字列が入力として与えられる
  • このとき元の数列を求めよ
  • ただし、元の数列は複数ありうるため、数列の最初の数字が一番小さい数列を正解とする

以上が逆FizzBuzzです。

サンプルテストケース

  • 入力が Fizz であれば [3] が正解
  • 入力が Buzz Fizz Fizz Buzz Fizz なら [5, 6, 7, 8, 9, 10, 11, 12] が正解

Fizz に対して [6] と出力するのは、[3] のほうが数字が小さいため不正解。

コード

だいたいは元記事の方針に沿ってリファクタリングしました。

const range = (start, stop) =>
  stop < start ? [] : [...Array(stop - start).keys()].map((x) => x + start);

const solve = (arr) => {
  arr = arr.map((s) => s.replace(/[a-z]/g, ''));
  const index = arr.indexOf('FB');
  if (index === -1) {
    // FB が含まれていない
    switch (arr.join('')) {
      case 'F': return range(3, 4);
      case 'B': return range(5, 6);
      case 'FB': return range(9, 11);
      case 'BF': return range(5, 7);
      case 'FF': return range(6, 10);
      case 'FBF': return range(3, 7);
      case 'BFF': return range(5, 10);
      case 'FFB': return range(6, 11);
      case 'FBFF': return range(3, 10);
      case 'BFFB': return range(5, 11);
      case 'FFBF': return range(6, 13);
      case 'FBFFB': return range(3, 11);
      case 'BFFBF': return range(5, 13);
      case 'FBFFBF': return range(3, 13);
      default: return null;
    }
  }

  const beforeFB = arr.slice(0, index);
  const afterFB = arr.slice(index + 1);
  // arr == [...beforeFB, 'FB', ...afterFB]

  // FB 以降が規則通りに並んでいるかチェックする
  if (!afterFB.every((s, i) => s === 'F B F F B F FB'.split(' ')[i % 7])) {
    return null;
  }

  const start = { '': 15, F: 12, BF: 10, FBF: 9, FFBF: 6, BFFBF: 5, FBFFBF: 3 }[
    beforeFB.join('')
  ];
  if (!start) {
    return null;
  }
  return range(
    start,
    (Math.floor(afterFB.length / 7) + 1) * 15 +
      [1, 4, 6, 7, 10, 11, 13][afterFB.length % 7]
  );
};

ちゃんとテストケースを確認したわけじゃないので間違ってるかも。一応以下のケースは満たしていることを確認済み。

検証に使ったテストケース

for (const [input, expect] of [
  ['Fizz', [3]],
  ['Buzz', [5]],
  ['FizzBuzz', [15]],
  ['Fizz Fizz', [6, 7, 8, 9]],
  ['Fizz Buzz', [9, 10]],
  ['Buzz Fizz', [5, 6]],
  ['Fizz FizzBuzz', [12, 13, 14, 15]],
  ['FizzBuzz Fizz', [15, 16, 17, 18]],
  ['Fizz Fizz Buzz', [6, 7, 8, 9, 10]],
  ['Fizz Buzz Fizz', [3, 4, 5, 6]],
  ['Buzz Fizz Fizz', [5, 6, 7, 8, 9]],
  ['Buzz Fizz FizzBuzz', [10, 11, 12, 13, 14, 15]],
  ['Fizz Fizz Buzz Fizz', [6, 7, 8, 9, 10, 11, 12]],
  ['Fizz Buzz Fizz Fizz', [3, 4, 5, 6, 7, 8, 9]],
  ['Buzz Fizz Fizz Buzz', [5, 6, 7, 8, 9, 10]],
  ['Fizz Buzz Fizz FizzBuzz', [9, 10, 11, 12, 13, 14, 15]],
  ['Fizz Buzz Fizz Fizz Buzz', [3, 4, 5, 6, 7, 8, 9, 10]],
  ['Buzz Fizz Fizz Buzz Fizz', [5, 6, 7, 8, 9, 10, 11, 12]],
  ['Fizz Fizz Buzz Fizz FizzBuzz', [6, 7, 8, 9, 10, 11, 12, 13, 14, 15]],
  ['Fizz Buzz Fizz Fizz Buzz Fizz', [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]],
  [
    'Buzz Fizz Fizz Buzz Fizz FizzBuzz',
    [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
  ],
  [
    'Fizz Buzz Fizz Fizz Buzz Fizz FizzBuzz',
    [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
  ],
]) {
  console.log(
    JSON.stringify(solve(input.split(' '))) === JSON.stringify(expect)
  );
}

善は急げ 悪は延べよ

「善は急げ」はよく知られています。しかし、実は続きがあるそうです。それが「悪は延べよ」です。

悪は延べよの意味は、「悪いと思う事は、一事延期せよ、そうすれば事情がやがて変化して、行なわないで済むようになることもある。」とのことです。

プログラミングでもメチャクチャ出てくる概念ですよね。いわゆる「早すぎる最適化」はまさに悪を延べていればなぁという後悔の話ですし。

決定はなるべく遅延させるべきだし、課題は一旦引いて考えてみると良い案が出てくる、仕事を終えてシャワーを浴びてる時にふと妙案が出てくる。これらもそうですよね。…いや後半のは悪は延べよとはちょっと違うかもですが。

ともかく、事を急いてしまって後々の負債になってしまうくらいなら慌てずに対応しよう。そもそも実装を遅らせて対応できるかもしれない。そういう話でした

pubsub パターンの現時点でのベストプラクティス

以前pubsub パターンのベタープラクティスを考えたんですが、よく考えたらイベント名を設定する必要なかったです。

これでよさそう↓

class PubSub<Payload extends unknown[]> {
  private listeners: ((...payload: Payload) => void)[] = [];
  subscribe(callback: (...payload: Payload) => void) {
    this.listeners.push(callback);
  }
  unsubscribe(callback: (...payload: Payload) => void) {
    this.listeners = this.listeners.filter((item) => item !== callback);
  }
  publish(...payload: Payload) {
    for (const listener of this.listeners) {
      listener(...payload);
    }
  }
}

export const buttonPubsub = new PubSub<[]>();
export const messagePubsub = new PubSub<[string]>();

イベント名を指定したかったらオーバーロードを使えば実装可能

const subscribe: {
  (eventName: 'click', callback: () => void): void;
  (eventName: 'message', callback: (message: string) => void): void;
} = (eventName: string, callback: (...event: any[]) => void) => {
  switch (eventName) {
    case 'click':
      buttonPubsub.subscribe(callback);
      break;
    case 'message':
      messagePubsub.subscribe(callback);
      break;
  }
};
subscribe('message', (mes: string) => console.log(mes)); // OK
subscribe('click', (mes: string) => console.log(mes)); // 型エラー

vue の SFC 内でうまくコメントアウトプラグインを動かしたい vim + mini.comment 編

(vim + mini.comment 編と書いたが、別に他パターンを書く予定はない)

割と手間取ったのでメモとして残しておきます。他の人の参考にもなりそうなので

結構無理やりな方法を取ったので、もっとスマートなやり方がありそうですが…

ポイント: hooks.pre で filetype を切り替える + vue の commentstring を設定する

こんな感じの基本方針のもと実装しました。

  1. コメントアウト処理の直前に filetype をカーソル行のものに変更
  2. コメントアウト処理が走る
  3. 処理が終わったらもとに戻す

以下がコードです (パッケージマネージャーは packer.nvim)

  use 'Shougo/context_filetype.vim'
  use {
    'echasnovski/mini.comment',
    config = function()
      require 'mini.comment'.setup {
        hooks = {
          pre = function()
            -- context_filetype プラグインを使って、カーソル行の filetype を取得している。
            vim.cmd('noa setfiletype ' .. vim.fn['context_filetype#get_filetypes']()[1])
          end,
          post = function()
            -- コメントアウト処理が終わったので元に戻す
            vim.cmd 'filetype detect'
          end
        }
      }
    end
  }

ポイントは noa setfiletype です。noa なしで setfiletype としてしまうと、LSP が起動してしまいます。LSP が動くと余計なシンタックスエラーが表示されてウザったいので noa をつけて不要な autocmd が動かないようにしてます。

ただ、これでは不十分です。実は mini.comment はそのままだと vue テンプレートのコメントアウトができません(多分。あまりよく調べてない)。そこで、自力で commentstring を設定する必要があります

  vim.api.nvim_create_autocmd({ 'BufRead', 'BufNewFile' }, {
    pattern = '*.vue',
    command = 'setlocal commentstring=<!--%s-->'
  })

これでコメントアウトできるようになります