こんにちは!okeです!
つい昨日、Riverpodの罠(?)に引っかかっているコードを発見したので、共有します🚀
罠のコード
早速ですが昨日、下記のようなコードを見つけました。
なにが問題かパッと分かるでしょうか?
/// 未予約のホテル情報
@riverpod
Future<List<Hotel>> unbookedHotels(UnbookedHotelsRef ref) async {
// ホテル情報を取得
final availableHotels = ref.watch(availableHotelRepositoryProvider).asData?.value ?? [];
// 予約済みのホテル情報を取得
final reservedHotels = ref.watch(reservedHotelRepositoryProvider).asData?.value ?? [];
// 未予約のホテル情報を取得
return availableHotels.where((hotel) => !reservedHotels.contains(hotel)).toList();
}
ごく自然なコードで特にエラーは出ません。
ただ、このコードは意図しない結果を招く可能性があります。
今回の場合、下記のフローで取得しないと正しい結果が得られません。
- 「ホテル情報」を取得・「予約済みのホテル情報」を取得
- 「ホテル情報」と「予約済みのホテル情報」から「未予約のホテル情報」を取得
しかし、上記のコードは順番がそのときの状況によって変化してしまい、不整合な結果が出力される可能性があります。
原因
RiverpodにてFutureProvider
を扱う際、
ref.watch(provider).when
でdata/error/loadingの値を別々にハンドリングしたりしますが、
ref.watch(provider).asData?.value
でerror/loading時はnullにしたりもしますよね。
今回は.asData?.value
を使用しているのが問題となっています。
具体的には、下記のような流れで問題が出てきます。
- 初期状態で
availableHotelRepositoryProvider
やreservedHotelRepositoryProvider
はローディング状態で空の配列が渡される。 availableHotelRepositoryProvider
が先に返ってきた場合に、予約済みのホテル情報は空なので「すべて未予約」としてユーザーに表示されてしまう!
上記の後にreservedHotelRepositoryProvider
が更新されて適切なデータに切り替わるかもしれませんが、下記のような問題は出てくるかなと思います。
- 一瞬画面がチラつく(この指摘を受けて今回調査をしました)
- どちらかエラー時はずっと正しくない結果が表示される
対策
asData?.value
が有効な場合もあると思いますが、
ちゃんと順を追わないと結果が取得できないフローに関してはawait ref.watch(provider.future)
で結果を待ってあげることが重要かと思います。
(今回の場合、Future.wait
で2つ一気に取得しても良さそうですね)
@riverpod
Future<List<Hotel>> unbookedHotels(UnbookedHotelsRef ref) async {
// ホテル情報を取得(処理が終わるまでawaitで待機)
final availableHotels = await ref.watch(availableHotelRepositoryProvider.future);
// 予約済みホテル情報を取得(処理が終わるまでawaitで待機)
final reservedHotels = await ref.watch(reservedHotelRepositoryProvider.future);
// 未予約のホテル情報を取得(変更なし)
return availableHotels.where((hotel) => !reservedHotels.contains(hotel)).toList();
}
このようにすることで、データの取得が完了するまで処理を待機し、
エラーが発生した場合には適切にハンドリングできるようになります。
まとめ
なるべくこの罠(?)に引っかかる人が少なくなれば良いなと思い、記事を書きました。
もし「ここが間違っているかも」や「もっと良いやり方あるよ」などあればコメントをください🙇♂️