Panda Noir

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

プログラミング題材集

  • みんな知ってる
  • そこそこ実装のやりがいがある
  • 仕様の把握がいらない(既に知ってる)

「あの言語を学びたいけど題材がない!」って時に活用ください。

アプリ編

  • TODO リスト
  • Twitter
  • カレンダー、予定表
  • Pixiv(画像ギャラリー)
  • ブログ(CMS)

ゲーム編

TUIでできるゲームばかりなので、どの言語でも実装できます。対戦用AIを考えるのも楽しいです。

  • テトリス
  • 2048
  • リバーシ
  • トランプゲーム

CLIツール編

有名なコマンドで、ちょっと面白いのを選んでみました。

  • ls
  • git
  • less
  • cal
  • jq
  • エディタ(nano や vim、Emacsなど)

シェルの実装をするのも面白いでしょう。

言語編

仕様が比較的カンタンな言語を選定しました。

  • Lisp
  • brainf**k
  • Lua

パズル系

パズルソルバーは競プロ的な要素もあります。解けると楽しいパズルを選んでみました(難易度が高いものもあります)

  • 数独ソルバー
  • チョコレートパズル(ペントミノ)ソルバー
  • ルービックキューブ
  • 15パズル

React の key をちゃんと使えないと起こる問題

「React の key に配列の添え字を使ってはいけない」理由を説明できますか?

リスト以外で key を使うべき場面をご存じですか?

今回は React の key を適切に扱えないと起こる問題を紹介します。

ダメな例のデモ

まず、key を適切に設定していない、keyをつけるべき場面を実際にご覧ください。

問題1: key をつけるべきなのにつけていない codesandbox.io

問題2: key を適切に設定していない(index で設定してしまっている) codesandbox.io

問題1: key をつけるべきなのにつけていない

recursing-fermi-6bm9c - CodeSandbox

確認手順:

  1. 「change」ボタンをクリックする
  2. 色名がすぐに Green へ変化する
  3. 画像のほうは以前のものがしばらく(新しい画像の読み込みが終わるまで)表示され続ける

このように色名と画像がズレるのは意図していません。

どうして起きるのか?

change ボタンを押したとき React からすると「img の src が変わっただけ」に見えます。 そのため、React は同じ img 要素を使って src だけ変えます。同じ img 要素なので、以前の画像がキャッシュされ、新しい画像を読み込むまで古い画像を表示し続けます。

解決方法: key を設定する

解決するには、React へ img 要素を新しい img 要素で置き換えるよう指示 すれば OK です。そして、ここで使うのが key prop です。key を使えば React に「img 要素が新しくなった」と伝えられます。新しい画像 URL がセットされた新しい img が追加されれば、古い画像が表示されることはありません。

問題2: key を適切に設定していない

zealous-lehmann-luycv - CodeSandbox

こちらは key に配列の添え字を渡してしまっているパターンです。要素を削除したときに問題が起こっています。

確認手順:

  1. 1つ目の delete ボタンを押す
  2. input の値が Blue のままになっている

どうして起きるのか?

これも先ほどの例と同じで、要素が削除されても React からすると「最後の要素が削除された」ように見えるため、1つ目の DOM を使いまわそうとするためです。

解決方法: 要素ごとにユニークな値を key にする

これを解決するには、要素ごとにユニークな値を使えば OK です。key は「ある要素と DOM の対応付けをしている」と考えれば腑に落ちるでしょう(間違えた例では index が同じなのだから input の値が変わらないのは当然)。

まとめ: コンポーネントが内部に状態を持っているときは key に気を付ける

見てきた例は2つとも、コンポーネントが内部に状態を持っていた(img と input)のが原因でした。内部の状態を引き継がせたくない場合は、しっかりと key を設定しましょう。

(もちろん各人が作った React コンポーネントも内部に状態を持っていれば同じような問題が起こります)

useLayoutEffectとは?何ができるの?

対象読者: 「useEffectより早く発火するやつでしょ?」くらいのふんわりとした理解をしている方

useLayoutEffect とは何か?

まずズバリ結論から言うと、「ブラウザが要素をレイアウトしたあと、画面に描画する前に同期的にJSを実行するためのフック」です。

さっぱりわからないと思うので順を追って説明します。

ブラウザのレンダリングの流れ

useLayoutEffect の動作を知るには、ブラウザのレンダリング処理について知らなければなりません。

ブラウザのレンダリングはおおむね次のような流れです。

  1. DOM が変更される
  2. レンダリングツリーを構築
  3. レンダリングツリーをもとにそれぞれの要素の位置と大きさを計算(Layout)
  4. 描画内容を計算(Paint)
  5. Paint した内容を合成(Composite)
  6. ユーザーの画面に実際に反映

このように、ユーザーに見えるようになるまでに複数の工程があります。

useLayoutEffect はいつ実行されるか?

useLayoutEffect は React がコンポーネントをレンダリングして、それを元にDOM を変更して、ブラウザが Layout したあとに実行されます。つまり要素の大きさと位置の情報は取れますがユーザーには見えていない段階です。

(引用: hook-flow)

useLayoutEffect がチラつき抑制になるのはなぜ?

よく useEffect だとチラつくから useLayoutEffect を使うという説明があります。ここまでの説明でなぜチラつきを抑えられるかも説明ができます。

もし、useLayoutEffect をした結果 DOM が変更になったらどうなるでしょうか?答えは Paint が行われず、Layout から再実行される です。ユーザーには useLayoutEffect が実行されたあとの画面しか見えません。そのため変更前の画面はユーザーに見えません。

useEffect は画面に反映された後に実行され、再度 Layout → Paint が行われるためチラつくのです。

useLayoutEffect が同期的とはどういうことか?

Paint は Layout のあと、JS の実行が止まり次第走ります。つまり同期的処理は終わるまで Paint をブロッキングします。逆に言えば非同期的処理は Paint をブロッキングしません。そのため同期処的理でなければなりません。

参考

https://leap-in.com/ja/lets-learn-how-to-browser-works/ https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/#render-behavior-edge-cases https://ja.reactjs.org/docs/hooks-reference.html#uselayouteffect

2021年を振り返る

今年は入社2年目ということで色々あったので書きたい。

  • チーム内のフロントリードを任された
  • リーダブルコードを意識してコーディングしてた
  • 開発環境の整備とか開発フローの改善の提案をしたり、実際に改善したりした
  • 外部登壇はほぼしなかった
  • 趣味でプログラミングする機会が減ってきた
  • テキストベースコミュニケーションをひたすら意識してた

チームにフロントエンド、サーバー、QAといて、フロントエンドが2人(俺と新卒)なのでフロントの部分は任される形になった。けど、スケジューリング能力の欠如やらでなかなか上手くできない…来年こそはしっかりやりたいな。

けどそもそも2年目でリード任されるのが早すぎる気がする。フロントエンド自体が平均年齢低めだから仕方ないとはいえ、早くない…?

あとはリーダブルコードをひたすら意識して書いてた。難しい問題をなるべく簡単に解く、変数の命名をしっかり意識する、意味あるテストを書くとか。だから俺の書いた部分はかなり読みやすい自信がある…と言い切りたいがそうでもない箇所もあるな。来年も引き続きがんばりたい部分。

業務と直接関係のない改善とかもたくさんやった。CircleCIのワークフローを見直したり、設計フローを見直したり、チームで使ってるslack botの改善したり。こういう自由を許されてるのはありがたい(下手したらサボりと見做されかねないからね)。

外部登壇は今年はほぼやらなかった。理由は単純で、自発的でない登壇は向いてないと思ったから。来年は自発的に登壇してみたいな。なんかテーマあれば。

趣味でプログラミングする時間はやっぱ減ってきた。けど、最近はスモールチーム内のフロントリードしてる関係でコミュニケーション系の仕事ばっかで仕事でプログラミングする時間も減ってきてる。来年は趣味の方である程度まかないたい。

来年の目標とかやりたいこと

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

この辺かな。転職は考えてないので大きくは変わらない気がする。

型安全に React の Provider をマージしてネストを浅くしたい

苦節半年くらいしてようやく実現できてメッチャうれしいので記事も書くぞ!!!

Context のネストがつらい

React の Context、いくつも書くとなるとネストがどんどん深くなっていってつらいですよね。

<ContextA.Provider>
  <ContextB.Provider>
    <ContextC.Provider>
      <ContextD.Provider>
        <ContextE.Provider>
          ...
        </ContextE.Provider>
      </ContextD.Provider>
    </ContextC.Provider>
  </ContextB.Provider>
</ContextA.Provider>

これ、フラットにしたくありませんか?

<MergedProvider>
  ...
</MergedProvider>

今回はこれが実現できたので紹介します!!!!

コード

import { Context, createContext, PropsWithChildren } from 'react';

type CheckContexts<T> =
  T extends (infer R)[] ?
    R extends [Context<infer _>, infer _] ?
      true
    : false
  : false;
const MergeProvider = <T extends (readonly [] | readonly any[])[]>({
  contexts,
  children,
}: PropsWithChildren<{ contexts: T } & (false extends CheckContexts<T> ? void : {})>) =>
  contexts.reduceRight(
    (acc, [Context, value]) => <Context.Provider value={value}>{acc}</Context.Provider>,
    children,
  );

const NumContext = createContext(0);
const StringContext = createContext('string');

const App = () => (
  <MergeProvider
    contexts={[
      [NumContext, 0],
      [StringContext, 'string'],
    ]}
  >
    <div />
  </MergeProvider>
);

playground で動かす

invalid な contexts が入ってきた場合、MegeProvider の props の型は void になり、何も受け付けなくなります。これにより型検査が通りません。やったね。

実際、以下のようにコンテキストと値の型がズレていると型検査が通りません。

const App = () => (
  <MergeProvider
    contexts={[
      [NumContext, '0'], // 型が合ってないので型検査が通らない
      [StringContext, 'string'],
    ]}
  >
    <div />
  </MergeProvider>
);

検証用の playground コードも置いておきます。 TypeScript playground

(上のコードは 2024/01/06 に追記したものです。以前のコードは以下に折りたたんでおいておきます)

以前のコード

コード

(こちらは改善前のコードになります)

TypeScript: TS Playground - An online editor for exploring TypeScript and JavaScript

import { FC, Context, ReactNode, createContext } from 'react';
type IsProvider<T> = T extends (infer R)[] ? R extends [Context<infer x>, infer y] ? x extends y ? JSX.Element : void : void : void;

const mergeProvider = <T extends (readonly [] | readonly any[])[]>(contexts: T, children: ReactNode): IsProvider<T> => {
    const res = contexts.reduce((acc, [Context, value]) => {
        return <Context.Provider value={value}>{acc}</Context.Provider>;
    }, children);
    return res as IsProvider<T>;
}

const NumContext = createContext(0);
const StringContext = createContext('string');

const App: FC = () => {
  return mergeProvider([
    [NumContext, 0],
    [StringContext, 'string']
  ], <div />);
}

こんな感じです。

invalid な contexts が入ってきた場合、megeProvider は void を返してくれるため、型検査が通りません。やったね。

const App: FC = () => {
  return mergeProvider([
    [NumContext, '0'], // invalid なので型検査が通らない
    [StringContext, 'string']
  ], <div />);
}

仕組み

仕組みとしてはごく単純で、contexts はなんでも受け入れておき、処理も context と value のタプルが渡されてきたと想定して書きました。当然このままだと実行時エラーがおきます(なんでも受け入れてるので)。しかし、正常な組み合わせ以外が渡された場合、型の返り値が void となるため、結果として整合性が取れます。

全く正しくない引数を受け入れるという発想ができるまですごく時間がかかりました…