direnvで親階層の.envrcも複数読み込む

これ、またやってしまった

tech-1natsu.hatenablog.com

コミッタ名がおかしくなるやつ

なんでやってしまったかというと、direnvでgitコミッタ名を管理しているからで、親階層にGitコミッタ名の変数を管理している.envrcを置いている。

その状況下でプロジェクトフォルダに別の.envrcを置いたからまたやってしまった。

親階層の.envrc読み込まれてない……

なんかどうもこの親階層にあるGitコミッタ名管理している.envrcが読み込まれていないような感じだった。

direnvのリポジトリ見に行ったら親階層の.emvrcを読み込む設定が必要らしかった(こっちがデフォルトの挙動じゃなかったのか…。)

Loading layered .envrcの項目

source_up

.envrcsource_up書くだけでよかった。

source_up

export HOGE="hogehoge"

こんな感じに。

これで親階層の.envrcも読んでくれて助かった。

React Hooksのメモ化のための第二引数の配列にオブジェクトを突っ込んだときの挙動について

タイトルが長い。

useEffectとかuseMemoとかの第一引数に渡した関数を発火させるかどうかの条件指定とも言える第二引数の配列にオブジェクトをまるごと入れたらどうなるのか

というはなし。

勘違いしそうなので最初に説明しておくと、

useMemo(()=> someFunction(), myObject)

ではなくて

useMemo(()=> someFunction(), [myObject])

です。配列の中にオブジェクトであって第二引数に直接オブジェクトではない。

なお当記事はReact16.8.1でのバージョン時点でのはなしです。


それで、タイトルのことをコードで書くとこう。当記事ではuseMemoを例にその実態を追う。

const myObj1 = {
  foo: "foo",
  bar: "bar",
  baz: {
    qux: 100,
    quux: {
      deepFoo: 'I am deepFoo'
    }
  }
};

const Foo = () => {
  // myObj1のfooプロパティが変わったらsomeFunctionが実行される
  const memo = useMemo(() => someFunction(), [myObj1.foo]);

  return <div>{memo}</div>;
};

const What = () => {
  // オブジェクトをごっそり渡した場合はsomeFunctionはどういうときに実行される?
  const memo = useMemo(() => someFunction(), [myObj1]);

  return <div>{memo}</div>;
};

コメント箇所で書いてあるように

  • FooコンポーネントではuseMemoの第二引数にmyObj1.fooを入れている
    • myObj1.fooが変わったら第一引数の関数が発火する
  • WhatコンポーネントではuseMemoの第二引数にmyObj1をまるごと入れている
    • しかもmyObj1はディープなオブジェクト
      • 第一引数の関数が発火する条件はどうなるの?
        • オブジェクトのどのプロパティが変わっても発火するの?

というはなし。

ここでReactちょっとやってる人だとなんとなく「shallow compareがされて浅いプロパティが変わったときは発火するが、深いプロパティが変わったときは発火しない」という予想を立てるかもしれない。

この予想はshallow copy/deep copyとかshallow compare(equal)/deep compare(equal)とかその辺の話題と経験からくると思う。

結論から

どういう条件下で第一引数の関数が発火するのかを結論からいうと、

第二引数の配列にオブジェクトを入れるとディーププロパティが変わっても大抵の場合は発火する

"大抵の場合"の意味を掘り下げると

オブジェクトが同値でなければ発火する

という意味。

さらに掘り下げると、

オブジェクトが更新される際に常に新しいオブジェクトが返されていれば発火する

ということになるかと思う。

どういうことなのか

詳しいことは後述するけれど、

配列に入れたオブジェクトのプロパティ値の比較は一切されていない = 第二引数の配列に入れたものは同一性の判定のみがされている

という処理になっている(Reactのコード見たらそうなっていた)

なんとshallow compare(equal)とかdeep compare(equal)とかの問題がありそうという予想は外れてしまって、そういった比較はなかった……。

DEMO

とりあえず実際に動く例を。

第二引数の配列に入れたオブジェクトのディーププロパティが変わってもshallow compareは関係がなく第一引数が発火する様子は以下。

JSONの下にあるボタンをポチポチするとオブジェクトのプロパティが変わって\ WOW /という文字の色が変わる。

ソースコード見てもらえばわかるとは思うけれど、\ WOW /のカラーコードはuseMemoを通していて表題の第二引数の配列にはuseStateから作ったオブジェクトを指定している。

浅いプロパティでも深いプロパティでも値が変わると\ WOW /がビカビカすると思う。

下側にあるカウンターはuseMemoの第二引数がちゃんと働いていることを裏付けるために一応置いたもの。カウンターのStateが変わっても\ WOW /の色は変わらないのでちゃんとメモ化されている。

v16.6で追加されたReact.memoではpropsをshallow compareしてpureかどうか判定しているはずで、なんだかHooksの第二引数にディープオブジェクトを突っ込んだ際のこの挙動はReactらしくないのではという気持ちになる。


以下からはReactのコードを見た詳細の様子がひたすら続きます。

第二引数の比較の真相に迫るべくReactコードの海へと潜った––––

ドキュメンタリーです。

useMemoの中身

中身はreact-reconciler/src/ReactFiberHooks.jsmountMemoupdateMemoっぽくて、

ちなみに第一引数はnextCreate、メモ条件の第二引数はdepsという名称になっているらしい

詳細なコードはGitHubで見てもらうとして、メモを返すか新しいものを返すかの判断はupdateMemo関数のここ(一部抜粋)

const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
  // Assume these are defined. If they're not, areHookInputsEqual will warn.
  if (nextDeps !== null) {
    const prevDeps: Array < mixed > | null = prevState[1];

    // 1. 初回時にメモしたdepsと現在のdepsを`areHookInputsEqual`で比較
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      // 2. イコールならメモしていたものを返す
      return prevState[0];
    }
  }
}
// 3. イコールじゃなかったら第一引数の関数を発火
const nextValue = nextCreate();
// 4. 新しくメモ
hook.memoizedState = [nextValue, nextDeps];
// 5. 新しいほうの値を返す
return nextValue;

元コードを改行してコメント済み

という流れ。

areHookInputsEqualをみてみる

同じくreact-reconciler/src/ReactFiberHooks.jsにあって

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
.
.
.
// forで配列分まわして全部isだったら前回と変わってないのでイコール(true)を返す
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
  if (is(nextDeps[i], prevDeps[i])) {
    continue;
  }
  return false;
}
return true;
}

なんかis関数をやってる

is関数をみてみる

shared/objectIs.jsがそれ

コメントアウト

/* * inlined Object.is polyfill to avoid requiring consumers ship their own * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is /

と書かれていて、Object.isのポリフィルらしい

developer.mozilla.org

Object.isした結果、イコールだったらメモしていたものを返す、そうでないなら関数を発火させ新しい値を得てそれを返すという流れということがわかった。

Object.isの比較って??

Hooks第二引数(deps)の比較はObject.isということがわかったところで、これってどういう比較判定なんだっけという疑問が浮かぶ。

developer.mozilla.org

便利なページと表があってなるほどだいたいなるほどという感じ。

同値チェックをしている……。

手元でObject.is()を試してみる

See the Pen Object.isの動作チェック by 1natsu (@1natsu172) on CodePen.

コメント見にくかったら別タブで開いてconsoleもみてください

いろいろやってみた結果Object.is()でオブジェクトの比較をすると見た目も値もすべて同じだったとしても比較対象がそれぞれ別物であればfalseが返るようだった。

やはりObject.is()はオブジェクトの中身は見ていないよう。

ふりかえり(結局どういうことだったのか)

CodeSandboxのデモではuseStateから取り出したディープオブジェクトを扱っていた。

useStateのアップデータ関数経由でstateを更新する際は、常に新しいオブジェクトを返すのがReactにおける鉄則なはずで、つまりstateが更新されるときはオブジェクトの同一性がfalseになる。

第二引数(deps)の比較はObject.is()ということで、falseが返る=オブジェクトが変更された=第一引数(nextCreate)を発火という流れ。だからディープオブジェクトのディーププロパティが変わっても第一引数が発火してuseMemoは更新されるという流れらしかった。

つまるところオブジェクトをFCの外側に持って、そのオブジェクトを直接書き換えたりしなければ大丈夫そうな気がしている。

let myObject = {
  foo: 'foo',
  bar: 'bar',
  baz: 'baz'
}

const hey = myObject

myObject.foo = '変わりました'

みたいなこと。

する人いない気がするしそもそもこういうのReactでどうやるのっていう感じではあるけれどこれだとObject.is()trueを返すので、もしやってると意図せぬ挙動をしそうという予感がする。


Hooksの第二引数の配列にオブジェクトを入れるとディーププロパティでも反応するよというただそれだけのことなのだけど、追ってみると当然奥が深かった。

『配列にオブジェクトまるごと入れたらどうなるんだろうな、shallow equalされてディーププロパティは反応しなさそうだけど……』という疑問からまさかReactのソースコード見ることになるとは思ってもみなかった。