🥑

Flutterでフックの一貫性の原則に反したらエラーが出た!

2024/03/16に公開
3

はじめに

Flutterでエラーになった箇所について、どうしてそのエラーが出たのかを自分なりに調べてみました🙌

エラー

下記のコードはエラーになった時のコードです。

class ExampleScreen extends HookConsumerWidget {
  const ExampleScreen({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final isButtonActive = useState(false);
    final exampleState = ref.watch(exampleControllerProvider);

    // exampleStateの中のitemsリストの長さを取得
    final itemsLength = exampleState.value?.items.length ?? 0;
    
    // itemsの長さに応じて、ButtonStateを持つ状態変数のリストを生成
    // itemsが空の場合は長さ1のリストを作成
    final buttonStates = List.generate(
        itemsLength > 0 ? itemsLength : 1,
        (_) => useState(ButtonState.Default));

    // ボタンのアクティブ状態を更新する関数
    callback() {
    // buttonStatesの中で、ButtonState.Selectedでないものがあれば、 
    // isButtonActive.valueをfalseに設定、そうでなければtrueに設定
      isButtonActive.value = !buttonStates
          .any((state) => state.value != ButtonState.Selected);
    }

    useEffect(() {
      //なんらかの処理
    }, []);

    return Scaffold(
      // Scaffoldの内容
    );
  }
}

エラーは(_) => useState(ButtonState.Default));に現れました。

下記エラー内容です

例外が発生しました
StateError (Bad state: Type mismatch between hooks:
- previous hook: _EffectHook
- new hook: _StateHook<ButtonState>
)

このエラーについて調べていると、フックの一貫性の原則に違反していることによって起きているということが分かりました。

フックの一貫性の原則とは??

フックの一貫性の原則とは、Flutter(およびReact)において、フックの数と順序がコンポーネントの各レンダリングで一定である必要があるという公式の原則のことです。

例えば、コンポーネントが初めてレンダリングされるときにuseStateが2回、useEffectが1回呼ばれた場合、次のレンダリングでも同じ順序と数でこれらのフックが呼び出される必要があります。途中でuseStateやuseEffectの呼び出しを条件分岐の中に置くなどして変更すると、フックの管理システムが正しく動作しなくなる可能性があるからです。

このエラーでは、フックの呼び出しの順序が変わったことにより、Type mismatch between hooks(フック間の型の不一致)というエラーが出た可能性があります。

previous hook: _EffectHook
new hook: _StateHook<ButtonState>

つまりこのエラーは、フックを呼び出した時に前の型は_EffectHookだったのに、次は新しい型の_StateHook<ButtonState>になっているから、型が違うよと言っています。

Flutter Hooksのドキュメントにフックの一貫性の原則について書いてあるか調べてみました。

https://pub.dev/documentation/flutter_hooks/latest/

ドキュメントには、’For more explanation of how hooks are implemented, here's a great article about how it was done in React(フックがどのように実装されているかについては、Reactでどのように実装されているかについての素晴らしい記事がある。)’と書いてありました。

その素晴らしい記事がこちらです。
https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e

私には内容が難しかったのでこちらでの説明は省きますが、ご興味があればぜひ読んでみてください。

対処法

とりあえず、エラーの対応としてクラスを分けてみました!
クラス内のフックを分ければ型の不一致が起こらないと思ったからです。

class ExampleScreen extends HookConsumerWidget {
  const ExampleScreen({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    useEffect(() {
      //なんらかの処理
    });

    return const ExampleScreenContent();
  }
}

class ExampleScreenContent extends HookConsumerWidget {
  const ExampleScreenContent({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final isButtonActive = useState(false);
    final exampleState = ref.watch(exampleControllerProvider);
    final itemsLength = exampleState.value?.items.length ?? 0;
    final buttonStates = List.generate(
      itemsLength > 0 ? itemsLength : 1,
      (_) => useState(ButtonState.Default));

    callback() {
       isButtonActive.value = !buttonStates
          .any((state) => state.value != ButtonState.Selected);
    }

    // 他のUIコンポーネント
    return Container();
  }
}

これで、クラス内でのフックの一貫性の原則は守られました🙌

しかし、ドキュメントのルールを見てみると、

  • 無条件にフックを呼び出すこと
  • use条件にラップしないでください

と書いてありました!

なので、List.generateに入っているuseStateを、書き換える必要があると思いました!

他のなんらかの処理で回して書くのが一番いいと思います!
今回の記事はエラー内容を調べることでしたので、ここまでにします!

まとめ

今回は、FlutterでBad state: Type mismatch between hooksエラーが発生した原因について調べてみました!

同じエラーに直面した方の参考になると嬉しいです!

また、フックの一貫性の原則について知れたので勉強になりました!時間のある時に、List.generate内のuseStateを別の方法で実装する方法を考えたいと思います!

Discussion

JboyHashimotoJboyHashimoto

buildメソッドの中にvoidなんとかって、メソッド書いていいもんですかね。
デバッグしたときに、何度も呼ばれてたから僕は最近怖くてやめましたね。

僕は、昔、StatefuleWidget使うと嫌がられたので、flutter_hooks使ってましたが、これ大丈夫か気になりますね。パフォーマンスに影響なかったりします?

みぃみぃ

こちら、 ChatGPTに変数名をExampleに修正して欲しいと変換してもらった際に書き換わってしまっていたみたいです。失礼いたしました。実際の実装にはcallback(){}を使用しています。

JboyHashimotoJboyHashimoto

なるほど!
callbackですね。ふむふむ参考になりました。フレームの描画が終わってから使っているのかな。面白そうだから試してみようと思います。