君たちはuseEffectとどう生きるか
はじめに
状態管理や副作用など少ないコードで多機能な挙動を実現できて、便利なflutter_hooksですが、しっかり挙動を理解できていないと思わぬバグに繋がる恐れがあります。
今回はflutter_hooksの中でもuseEffectの挙動にフォーカスします。
実際のコードから結果がどうなるか予想しながら読むと自分の理解を確認でき、少しでも参考になれば幸いです。
それでは参ります。
君たちはuseEffectとどう生きるか
内容
サンプルコードの実行環境
名称 | バージョン |
---|---|
Flutter | 3.10.4 |
Dart | 3.0.3 |
flutter_hooks | 0.19.0 |
useRefとObjectRef.valueをKeyに
useRefで count
というObjectRefを定義してそのvalueをuseEffectのKeyに指定した場合です。
useEffectのラムダ式ではただ print()
しているだけです。
FilledButton
の onPressed()
が呼ばれたときに count
の値を更新しています。
このとき、結果はどうなるでしょうか?
サンプルコード
class HomePage extends HookWidget {
const HomePage({
Key? key,
}) : super(key: key);
Widget build(BuildContext context) {
final count = useRef(0);
useEffect(
// ignore: body_might_complete_normally_nullable
() {
print('count = ${count.value}');
},
[count.value],
);
return Scaffold(
appBar: AppBar(
title: const Text('useEffect Sample'),
),
body: Center(
child: FilledButton(
onPressed: () => count.value++,
child: const Text('count++'),
),
),
);
}
}
結果
結果は以下のようになります。
結果
I/flutter (20300): count = 0
初回のビルド時に一度だけ呼ばれるだけで、何回ボタンを押下してもuseEffect内の print()
が呼ばれることはありません。
useRefとObjectRefをKeyに
useRefで count
というObjectRefを定義してそのObjectRefをそのままuseEffectのKeyに指定した場合です。
useEffectのラムダ式ではただ print()
しているだけです。
FilledButton
の onPressed()
が呼ばれたときに count
の値を更新しています。
このとき、結果はどうなるでしょうか?
サンプルコード
class HomePage extends HookWidget {
const HomePage({
Key? key,
}) : super(key: key);
Widget build(BuildContext context) {
final count = useRef(0);
useEffect(
// ignore: body_might_complete_normally_nullable
() {
print('count = ${count.value}');
},
[count],
);
return Scaffold(
appBar: AppBar(
title: const Text('useEffect Sample'),
),
body: Center(
child: FilledButton(
onPressed: () => count.value++,
child: const Text('count++'),
),
),
);
}
}
結果
結果は以下のようになります。
結果
I/flutter (20300): count = 0
初回のビルド時に一度だけ呼ばれるだけで、何回ボタンを押下してもuseEffect内の print()
が呼ばれることはありません。
useStateとValueNotifier.valueをKeyに
useStateで count
というValueNotifierを定義してそのvalueをuseEffectのKeyに指定した場合です。
useEffectのラムダ式ではただ print()
しているだけです。
FilledButton
の onPressed()
が呼ばれたときに count
の値を更新しています。
このとき、結果はどうなるでしょうか?
サンプルコード
class HomePage extends HookWidget {
const HomePage({
Key? key,
}) : super(key: key);
Widget build(BuildContext context) {
final count = useState(0);
useEffect(
// ignore: body_might_complete_normally_nullable
() {
print('count = ${count.value}');
},
[count.value],
);
return Scaffold(
appBar: AppBar(
title: const Text('useEffect Sample'),
),
body: Center(
child: FilledButton(
onPressed: () => count.value++,
child: const Text('count++'),
),
),
);
}
}
結果
結果は以下のようになります。
結果
I/flutter (20300): count = 0
I/flutter (20300): count = 1
I/flutter (20300): count = 2
I/flutter (20300): count = 3
I/flutter (20300): count = 4
I/flutter (20300): count = 5
初回のビルド時に I/flutter (20300): count = 0
が出力されます。
それ以降は、ボタンを押下するたびにプラス1された値が I/flutter (20300): count = 1
、 I/flutter (20300): count = 2
と出力されます。
useStateとValueNotifierをKeyに
useStateで count
というValueNotifierを定義してそのValueNotifierをそのままuseEffectのKeyに指定した場合です。
useEffectのラムダ式ではただ print()
しているだけです。
FilledButton
の onPressed()
が呼ばれたときに count
の値を更新しています。
このとき、結果はどうなるでしょうか?
サンプルコード
class HomePage extends HookWidget {
const HomePage({
Key? key,
}) : super(key: key);
Widget build(BuildContext context) {
final count = useState(0);
useEffect(
// ignore: body_might_complete_normally_nullable
() {
print('count = ${count.value}');
},
[count],
);
return Scaffold(
appBar: AppBar(
title: const Text('useEffect Sample'),
),
body: Center(
child: FilledButton(
onPressed: () => count.value++,
child: const Text('count++'),
),
),
);
}
}
結果
結果は以下のようになります。
結果
I/flutter (20300): count = 0
初回のビルド時に一度だけ呼ばれるだけで、何回ボタンを押下してもuseEffect内の print()
が呼ばれることはありません。
ValueNotifierのvalueをKeyに指定した場合と少し違うだけで、大きな挙動の違いなので注意が必要です。
ValueNotifierをKeyにすると、ボタン押下時にValueNotifierのvalueは更新されてその度にリビルドが発生しますが、ValueNotifierのインスタンス自体に変化はないため、このような挙動になります。
なにもしないvoid FunctionをKeyに
build()
内で doSomething()
というメソッドを定義して、そのvoid Function
をuseEffectのKeyに指定して、useEffectのラムダ式で実行している場合です。
doSomething()
では、ただ print()
しているだけです。
このとき、結果はどうなるでしょうか?
サンプルコード
class HomePage extends HookWidget {
const HomePage({
Key? key,
}) : super(key: key);
Widget build(BuildContext context) {
void doSomething() {
print('doSomething');
}
useEffect(
// ignore: body_might_complete_normally_nullable
() {
doSomething();
},
[doSomething],
);
return Scaffold(
appBar: AppBar(
title: const Text('useEffect Sample'),
),
body: const Text('Hello world'),
);
}
}
結果
結果は以下のようになります。
結果
I/flutter (23665): doSomething
初回のビルド時に一度だけ出力されます。
Page全体リビルドをかけてなにもしないvoid FunctionをKeyに
build()
内で doSomething()
というメソッドを定義して、そのvoid Function
をuseEffectのKeyに指定して、useEffectのラムダ式で実行している場合です。
doSomething()
では、ただ print()
しているだけです。
また、useStateで count
というValueNotifierを定義して FilledButton
の onPressed()
が呼ばれたときに count
の値を更新しています。
このとき、結果はどうなるでしょうか?
サンプルコード
class HomePage extends HookWidget {
const HomePage({
Key? key,
}) : super(key: key);
Widget build(BuildContext context) {
final count = useState(0);
void doSomething() {
print('doSomething');
}
useEffect(
// ignore: body_might_complete_normally_nullable
() {
doSomething();
},
[doSomething],
);
return Scaffold(
appBar: AppBar(
title: const Text('useEffect Sample'),
),
body: Column(
children: [
Text(count.value.toString()),
FilledButton(
onPressed: () => count.value++,
child: const Text('count++'),
),
],
),
);
}
}
結果
結果は以下のようになります。
結果
I/flutter (25559): doSomething
I/flutter (25559): doSomething
I/flutter (25559): doSomething
I/flutter (25559): doSomething
ボタンを押下してuseStateのValueNotifierを更新してPage全体リビルドを書けるたびにuseEffectのラムダが実行され、 その中で実行している doSomething()
内の print()
が出力されます。
useEffectのラムダ式内で print('doSomething.hashCode = ${doSomething.hashCode}');
のようにすると確認できますが、毎回異なるハッシュ値が確認できます。
useEffect(
// ignore: body_might_complete_normally_nullable
() {
doSomething();
print('doSomething.hashCode = ${doSomething.hashCode}');
},
[doSomething],
);
I/flutter (27021): doSomething
I/flutter (27021): doSomething.hashCode = 462925064
I/flutter (27021): doSomething
I/flutter (27021): doSomething.hashCode = 606372275
I/flutter (27021): doSomething
I/flutter (27021): doSomething.hashCode = 638055
ボタン押下時に、ValueNotifierのvalueが更新されてリビルド発生時に doSomething()
が再生成されて、その変化を検知してuseEffectのラムダ式が再実行されています。
コンストラクタ引数に渡されたvoid FunctionをKeyにするとどうなるか
HomePage
のbuild()
内で doSomething()
というメソッドを定義して、そのvoid Function
を子の _Component
に渡しています。
渡された void Function
は _Component
の build()
内の useEffectのラムダ式で実行されています。Keyにも onPressed
を指定しています。
また、 _Component
内ではuseStateで count
というValueNotifierを定義して FilledButton
の onPressed()
が呼ばれたときに count
の値を更新しています。
このとき、結果はどうなるでしょうか?
サンプルコード
class HomePage extends HookWidget {
const HomePage({
Key? key,
}) : super(key: key);
Widget build(BuildContext context) {
void doSomething() {
print('doSomething');
}
return Scaffold(
appBar: AppBar(
title: const Text('useEffect Sample'),
),
body: _Component(onPressed: doSomething),
);
}
}
class _Component extends HookWidget {
const _Component({
Key? key,
required this.onPressed,
}) : super(key: key);
final VoidCallback onPressed;
Widget build(BuildContext context) {
final count = useState(0);
// ignore: body_might_complete_normally_nullable
useEffect(() {
onPressed();
}, [onPressed]);
return Column(
children: [
Text(count.value.toString()),
FilledButton(
onPressed: () => count.value++,
child: const Text('count++'),
),
],
);
}
}
結果
結果は以下のようになります。
結果
I/flutter (27344): doSomething
初回のビルド時に一度だけ出力されます。それ以降は何度 _Component
内の FilledButton
の onPressed
を実行してもなにも出力されません。
ValueNotifiet.valueをuseEffectのKeyに指定して、ラムダ式で更新してるケース
useStateで count
というValueNotifierを定義してそのvalueをuseEffectのKeyに指定した場合です。また、useEffectのラムダ式では count
の値を更新しています。
さらに、FilledButton
の onPressed()
が呼ばれたときに count
の値を更新しています。
このとき、結果はどうなるでしょうか?
サンプルコード
class HomePage extends HookWidget {
const HomePage({
Key? key,
}) : super(key: key);
Widget build(BuildContext context) {
final count = useState(0);
useEffect(
// ignore: body_might_complete_normally_nullable
() {
print('update before count.value = ${count.value}');
count.value++;
print('update after count.value = ${count.value}');
},
[count.value],
);
return Scaffold(
appBar: AppBar(
title: const Text('useEffect Sample'),
),
body: Column(
children: [
Text(count.value.toString()),
FilledButton(
onPressed: () => count.value++,
child: const Text('count++'),
),
],
),
);
}
}
結果
結果は以下のようになります。
結果
I/flutter (11856): update before count.value = 0
I/flutter (11856): update after count.value = 1
// ボタン押下
I/flutter (11856): update before count.value = 2
I/flutter (11856): update after count.value = 3
// ボタン押下
I/flutter (11856): update before count.value = 4
I/flutter (11856): update after count.value = 5
初回ビルド時に、 I/flutter (11856): update before count.value = 0
が出力されて、useEffect内の count.value++;
が実行されるため、そのあとに I/flutter (11856): update after count.value = 1
が出力されます。
ですが、ここでリビルドは発生せず無限ループのような挙動にはなりません。
ボタン押下時に count.value++;
が実行されてリビルドが発生し、useEffectのラムダ式がまた実行される流れになっています。
おわりに
さて、いくつ正解できたでしょうか。
詳細まで理解していないと、useEffectのラムダ式で重要なロジックを実行させる場合意図しない挙動になる場合があります。
これは恐ろしいことです。
これを期にこれまで書いたuseEffectのコードを見返すなどすると、もしかしたらまだ気づいてはいなかったバグに気づくことができるかもしれません。
Discussion