usePreviousIfNull
Returns the most recent non-null value it has seen. While the incoming value is non-null it passes it straight through and remembers it; when the value goes null, it keeps returning the last non-null value instead. This is the packaged fix for refresh blink, where content flashes away while a computed state reloads.
class ProductView extends HookWidget {
final String productId;
const ProductView({super.key, required this.productId});
Widget build(BuildContext context) {
final productState = useAutoComputedState(
() async => _loadProduct(productId),
keys: [productId],
);
// valueOrNull goes back to null during a reload; usePreviousIfNull keeps
// the last loaded product on screen until the new one arrives.
final product = usePreviousIfNull(productState.valueOrNull); // <- no blank flash on refresh
if (product == null) return const Center(child: CircularProgressIndicator());
return Text(product.name);
}
}
Future<Product> _loadProduct(String id) async => Product(name: id);
Signature
T? usePreviousIfNull<T>(T? value, {HookKeys keys = hookKeysEmpty});
It takes a nullable value and returns a nullable result - the result is only null before the first non-null value arrives. Once any non-null value has been seen, that value is retained until a newer non-null one replaces it. Passing keys resets the retained value when the keys change.
Use cases
- Smoothing over the reload gap of
useAutoComputedState/useComputedState, whosevalueOrNullreturns tonullduring a refresh. Wrapping it keeps the old content on screen until the new value lands:final dataState = useAutoComputedState(() => service.load(id), keys: [id]);final display = usePreviousIfNull(dataState.valueOrNull); // <- old content stays visible during refresh
Caveats
-
It only fills
nullgaps - it cannot smooth over a value that changes to a different non-null value. The transition between two non-null values is instantaneous; this hook only matters when the source dips tonull. -
For a
ComputedStaterendered through one of the wrapper widgets, the View-sidekeepInProgressflag does the same job (keeping the last ready value during a reload). Reach forusePreviousIfNullwhen you are working with the raw value in the State, andkeepInProgresswhen the branching lives in the View. -
Resetting via
keysbrings the retained value back tonull. If the keys track the same input that drives the source, an intentional reset and a transient reload look the same to this hook.
See also
- useAutoComputedState - the usual source whose
valueOrNullthis hook smooths - useMemoizedIf - another small conditional helper in this category
- usePreviousValue - track the previous distinct value rather than the last non-null one