Skip to main content

LeanCode Hooks

A curated collection of Flutter hooks that we commonly use at LeanCode, all gathered in one place for better discoverability and consistent versioning.

Installation

Add the package to your pubspec.yaml:
dependencies:
  leancode_hooks: ^0.1.2
This package exports package:flutter_hooks, so you don’t need to add it as a separate dependency.

Import

import 'package:leancode_hooks/leancode_hooks.dart';

Available Hooks

BLoC Integration

useBloc

Provides a Cubit or Bloc that is automatically disposed without having to use BlocProvider. Signature:
B useBloc<B extends BlocBase<Object?>>(
  B Function() create, {
  List<Object?> keys = const [],
})
Example:
class MyWidget extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final counterCubit = useBloc(() => CounterCubit());
    final count = useBlocState(counterCubit);
    
    return Column(
      children: [
        Text('Count: $count'),
        ElevatedButton(
          onPressed: () => counterCubit.increment(),
          child: Text('Increment'),
        ),
      ],
    );
  }
}

useBlocState

Provides the current state of a Cubit or Bloc and forces the widget to rebuild on state changes. Signature:
S useBlocState<S>(BlocBase<S> bloc)
Example:
final authBloc = context.read<AuthBloc>();
final authState = useBlocState(authBloc);

if (authState is Authenticated) {
  return HomeScreen();
}
return LoginScreen();

useBlocListener

Takes a Bloc or Cubit and invokes a listener in response to state changes. Signature:
void useBlocListener<S>({
  required BlocBase<S> bloc,
  required void Function(S state) listener,
})
Example:
final authBloc = context.read<AuthBloc>();

useBlocListener<AuthState>(
  bloc: authBloc,
  listener: (state) {
    if (state is AuthError) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(state.message)),
      );
    }
  },
);

Debouncing & Throttling

useDebounce

Returns a function whose invocation will be delayed by the specified duration. When called again, the previous invocation is canceled. Signature:
DebouncedCallback useDebounce(Duration duration)
Example:
final debounce = useDebounce(Duration(milliseconds: 500));
final searchController = useTextEditingController();

useEffect(() {
  void listener() {
    debounce(() {
      // This will only run 500ms after the user stops typing
      performSearch(searchController.text);
    });
  }
  
  searchController.addListener(listener);
  return () => searchController.removeListener(listener);
}, [searchController]);

useThrottle

Returns a function that is throttled for the specified duration after being called. Signature:
ThrottledCallback useThrottle(Duration duration)
Example:
final throttle = useThrottle(Duration(seconds: 1));

return ElevatedButton(
  onPressed: () {
    throttle(() {
      // This will run at most once per second
      submitForm();
    });
  },
  child: Text('Submit'),
);

Text Editing

useDeclarativeTextEditingController

A wrapper around useTextEditingController that updates the controller’s text when the value changes declaratively. Signature:
TextEditingController useDeclarativeTextEditingController({
  required String text,
})
Example:
final searchQuery = useState('');
final controller = useDeclarativeTextEditingController(
  text: searchQuery.value,
);

return Column(
  children: [
    TextField(controller: controller),
    ElevatedButton(
      onPressed: () => searchQuery.value = 'Flutter',
      child: Text('Set to "Flutter"'),
    ),
  ],
);
This hook handles updating the controller’s text when the text parameter changes, which is useful for controlled text inputs.

useSyncedTextEditingController

A wrapper around useTextEditingController that makes it easier to add an onChanged callback. Signature:
TextEditingController useSyncedTextEditingController(
  void Function(TextEditingValue value) onChanged, {
  String? initialText,
})
Example:
final searchQuery = useState('');
final controller = useSyncedTextEditingController(
  (value) => searchQuery.value = value.text,
  initialText: '',
);

return TextField(
  controller: controller,
  decoration: InputDecoration(
    labelText: 'Search',
    suffixText: 'Query: ${searchQuery.value}',
  ),
);

Utilities

useDisposable

Creates and memoizes an instance, then executes a dispose function on unmount to cleanup resources. Signature:
T useDisposable<T>({
  required ValueGetter<T> builder,
  required void Function(T) dispose,
  List<Object?> keys = const [],
})
Example:
final videoController = useDisposable(
  builder: () => VideoPlayerController.network('https://example.com/video.mp4'),
  dispose: (controller) => controller.dispose(),
);

return VideoPlayer(videoController);

useFocused

Subscribes to FocusNode changes and returns whether the node has focus. Signature:
bool useFocused(FocusNode focusNode)
Example:
final focusNode = useFocusNode();
final isFocused = useFocused(focusNode);

return TextField(
  focusNode: focusNode,
  decoration: InputDecoration(
    labelText: 'Email',
    border: OutlineInputBorder(
      borderSide: BorderSide(
        color: isFocused ? Colors.blue : Colors.grey,
        width: isFocused ? 2 : 1,
      ),
    ),
  ),
);

usePostFrameEffect

Registers an effect to be run in WidgetsBinding.addPostFrameCallback. Signature:
void usePostFrameEffect(
  VoidCallback effect, {
  List<Object?> keys = const [],
})
Example:
final scrollController = useScrollController();

usePostFrameEffect(
  () {
    // Scroll to bottom after the frame is rendered
    scrollController.jumpTo(
      scrollController.position.maxScrollExtent,
    );
  },
  [messages.length], // Re-run when messages change
);

useStreamListener

Listens to a stream and calls onData on new data. Signature:
void useStreamListener<T>(
  Stream<T> stream,
  ValueChanged<T> onData, {
  Function? onError,
  VoidCallback? onDone,
  bool? cancelOnError,
  List<Object?> keys = const [],
})
Example:
final locationStream = locationService.getLocationStream();

useStreamListener<Location>(
  locationStream,
  (location) {
    print('New location: ${location.latitude}, ${location.longitude}');
  },
  onError: (error) => print('Location error: $error'),
);

useTapGestureRecognizer

Creates a TapGestureRecognizer that will be disposed automatically. Signature:
TapGestureRecognizer useTapGestureRecognizer(
  TapGestureRecognizer Function() builder, {
  List<Object?> keys = const [],
})
Example:
final tapRecognizer = useTapGestureRecognizer(
  () => TapGestureRecognizer()
    ..onTap = () => print('Tapped!'),
);

return Text.rich(
  TextSpan(
    text: 'Click ',
    children: [
      TextSpan(
        text: 'here',
        style: TextStyle(color: Colors.blue),
        recognizer: tapRecognizer,
      ),
      TextSpan(text: ' to continue'),
    ],
  ),
);

Best Practices

1

Use hooks only in HookWidget

All hooks must be called inside a HookWidget or HookConsumer (from flutter_hooks).
2

Maintain hook call order

Always call hooks in the same order on every build. Don’t call hooks conditionally or in loops.
3

Specify keys for dependencies

When using hooks with memoization (like useBloc, useDisposable), pass the keys parameter to control when the value is recreated.
4

Clean up resources

Use useDisposable for resources that need cleanup, or useEffect with a cleanup function.

Source Code

View the source code on GitHub.

Build docs developers (and LLMs) love