Skip to main content

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, whose valueOrNull returns to null during 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 null gaps - 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 to null.

  • For a ComputedState rendered through one of the wrapper widgets, the View-side keepInProgress flag does the same job (keeping the last ready value during a reload). Reach for usePreviousIfNull when you are working with the raw value in the State, and keepInProgress when the branching lives in the View.

  • Resetting via keys brings the retained value back to null. 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