Skip to content

feat: add useThrottled hooks. #471

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

ostk0069
Copy link

@ostk0069 ostk0069 commented May 25, 2025

close. #472

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced a new hook that allows throttling the execution of callbacks, helping to limit how frequently a callback can be triggered within a specified duration.
  • Tests

    • Added tests to verify that the new throttling hook correctly limits callback execution and behaves as expected during rapid user interactions.

Copy link

coderabbitai bot commented May 25, 2025

Walkthrough

A new throttling hook, useThrottled, has been added to the Flutter Hooks package. The hook is implemented in a new file, integrated into the main hooks library, and a dedicated test suite has been introduced to verify its throttling behavior and correctness through widget tests.

Changes

File(s) Change Summary
.../lib/src/hooks.dart Added part 'throttled.dart'; directive to include the new throttling hook source file.
.../lib/src/throttled.dart Introduced useThrottled hook and internal _Throttler class to provide throttled callback execution.
.../test/use_throttle_test.dart Added new widget tests for useThrottled, including a custom test widget and two test cases for throttling.

Sequence Diagram(s)

sequenceDiagram
  participant Widget
  participant useThrottled
  participant _Throttler

  Widget->>useThrottled: Initialize with duration
  useThrottled->>_Throttler: Create throttler instance

  loop On User Tap
    Widget->>useThrottled: Call throttled callback
    useThrottled->>_Throttler: run(callback)
    alt Timer not active
      _Throttler->>Widget: Execute callback
      _Throttler->>_Throttler: Start timer for duration
    else Timer active
      _Throttler-->>Widget: Ignore callback
    end
    Note over _Throttler: After duration, timer resets
  end
Loading

Poem

In the land of hooks, a throttler appears,
To slow down the taps and quiet our fears.
With timers and tests, it keeps things in line,
Only once per half-second—now isn’t that fine?
Fluttering softly, the state hops anew,
Throttle on, dear bunny, this one’s for you! 🐇⏱️


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@ostk0069 ostk0069 mentioned this pull request May 25, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
packages/flutter_hooks/lib/src/throttled.dart (2)

3-3: Fix grammatical error in documentation.

The documentation comment contains a grammatical error.

-/// widget ignore updates accordingly after a specified [duration] duration.
+/// Throttles widget updates to occur at most once per specified [duration].

7-12: Improve documentation example clarity.

The current example could be more clear about the throttling behavior and use a more typical throttling scenario.

-/// String userInput = ''; // Your input value
-///
-/// // Create a throttle callback
-/// final throttle = useThrottle(duration: const Duration(milliseconds: 500));
-/// // Assume a fetch method fetchData(String query) exists
-/// Button(onPressed: () => throttle(() => fetchData(userInput)));
+/// final searchQuery = useState('');
+/// final throttle = useThrottle(duration: const Duration(milliseconds: 500));
+/// 
+/// TextField(
+///   onChanged: (value) {
+///     searchQuery.value = value;
+///     // This will be throttled to run at most once every 500ms
+///     throttle(() => performSearch(value));
+///   },
+/// );
packages/flutter_hooks/test/use_throttle_test.dart (2)

40-41: Remove redundant delay operations.

Both pumpAndSettle(_duration) and Future.delayed(_duration) are unnecessary. The pumpAndSettle should be sufficient to advance the timer.

-await tester.pumpAndSettle(_duration);
-await Future<void>.delayed(_duration);
+await Future<void>.delayed(_duration);
+await tester.pump();

73-73: Consider adding test for custom duration.

The tests only cover the default duration. Consider adding a test case that verifies custom duration parameter works correctly.

Add a test case like:

+testWidgets('custom duration works correctly', (tester) async {
+  // Test with a shorter duration like 100ms
+  // to verify the duration parameter is respected
+});

Would you like me to generate a complete test case for custom duration validation?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e8215f0 and 07b1e42.

📒 Files selected for processing (3)
  • packages/flutter_hooks/lib/src/hooks.dart (1 hunks)
  • packages/flutter_hooks/lib/src/throttled.dart (1 hunks)
  • packages/flutter_hooks/test/use_throttle_test.dart (1 hunks)
🔇 Additional comments (6)
packages/flutter_hooks/lib/src/hooks.dart (1)

40-40: LGTM! Proper integration of the new throttle hook.

The part directive correctly follows the established pattern and alphabetical ordering of other hook inclusions in the library.

packages/flutter_hooks/lib/src/throttled.dart (3)

14-19: LGTM! Clean function implementation.

The useThrottle function is well-implemented:

  • Proper use of useMemoized to maintain the throttler instance
  • Correct dependency array with [duration]
  • Clean return of the throttler's run method

21-26: LGTM! Proper validation in constructor.

The constructor correctly validates that the duration is positive and provides a clear assertion message.


34-41: LGTM! Correct throttling implementation.

The throttling logic is correctly implemented:

  • Executes callback immediately if no timer is running (leading edge behavior)
  • Starts timer to prevent subsequent executions during the duration
  • Properly resets the timer when duration completes

This follows the standard throttling pattern effectively.

packages/flutter_hooks/test/use_throttle_test.dart (2)

7-31: LGTM! Comprehensive throttling validation.

This test correctly validates that multiple rapid taps are throttled, ensuring only the first tap within the duration period triggers a state update.


52-71: LGTM! Well-structured test widget.

The test widget properly demonstrates the useThrottle hook usage with state management and user interaction.

Comment on lines 33 to 48
testWidgets('update number after duration', (tester) async {
await tester.runAsync<void>(() async {
await tester.pumpWidget(const _UseThrottleTestWidget());

final text = find.byType(GestureDetector);
expect(find.text('1'), findsOneWidget);

await tester.pumpAndSettle(_duration);
await Future<void>.delayed(_duration);

await tester.tap(text);
await tester.pump();

expect(find.text('2'), findsOneWidget);
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve test to better validate throttle reset behavior.

The current test doesn't fully validate that the throttle properly resets. It only tests a single tap after waiting, not that subsequent taps can occur after the throttle period.

 testWidgets('update number after duration', (tester) async {
   await tester.runAsync<void>(() async {
     await tester.pumpWidget(const _UseThrottleTestWidget());

     final text = find.byType(GestureDetector);
     expect(find.text('1'), findsOneWidget);

+    // First tap should work immediately
+    await tester.tap(text);
+    await tester.pump();
+    expect(find.text('2'), findsOneWidget);
+
+    // Wait for throttle duration to pass
     await tester.pumpAndSettle(_duration);
     await Future<void>.delayed(_duration);

+    // Next tap should work after throttle resets
     await tester.tap(text);
     await tester.pump();
-    expect(find.text('2'), findsOneWidget);
+    expect(find.text('3'), findsOneWidget);
   });
 });
🤖 Prompt for AI Agents
In packages/flutter_hooks/test/use_throttle_test.dart around lines 33 to 48, the
test only verifies a single tap after the throttle duration but does not confirm
that the throttle resets to allow subsequent taps. Modify the test to include
multiple taps separated by the throttle duration, verifying that the widget
updates correctly each time, thereby ensuring the throttle reset behavior is
properly validated.

/// Button(onPressed: () => throttle(() => fetchData(userInput)));
/// ```
void Function(VoidCallback callback) useThrottle({
Duration duration = const Duration(milliseconds: 500),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value seems arbitrary. I'd rather not have a default value and make the parameter required

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated with 4ebb7f7

/// // Assume a fetch method fetchData(String query) exists
/// Button(onPressed: () => throttle(() => fetchData(userInput)));
/// ```
void Function(VoidCallback callback) useThrottle({
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be named useThrottled to match `useDebounced

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated with 4ebb7f7

/// // Assume a fetch method fetchData(String query) exists
/// Button(onPressed: () => throttle(() => fetchData(userInput)));
/// ```
void Function(VoidCallback callback) useThrottle({
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the API

I'd like something that matches the useDebounced API, for consistency

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think useDebounced has big difference of usage.
useThrottled should be callback method to use.

@ostk0069 ostk0069 requested a review from rrousselGit June 4, 2025 16:49
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 07b1e42 and 4ebb7f7.

📒 Files selected for processing (2)
  • packages/flutter_hooks/lib/src/throttled.dart (1 hunks)
  • packages/flutter_hooks/test/use_throttle_test.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/flutter_hooks/test/use_throttle_test.dart
🔇 Additional comments (1)
packages/flutter_hooks/lib/src/throttled.dart (1)

14-19: LGTM! Function signature and implementation are well-structured.

The function correctly:

  • Makes duration required (addressing past feedback)
  • Uses useMemoized with proper dependency tracking
  • Returns a clean callback interface

Comment on lines +3 to +13
/// widget ignore updates accordingly after a specified [duration] duration.
///
/// Example:
/// ```dart
/// String userInput = ''; // Your input value
///
/// // Create a throttle callback
/// final throttle = useThrottled(duration: const Duration(milliseconds: 500));
/// // Assume a fetch method fetchData(String query) exists
/// Button(onPressed: () => throttle(() => fetchData(userInput)));
/// ```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve documentation clarity and correctness.

The documentation has grammatical issues and could better explain the throttling behavior:

-/// widget ignore updates accordingly after a specified [duration] duration.
+/// Creates a throttled callback that executes at most once per [duration].
+/// 
+/// The returned function will execute the provided callback immediately on the 
+/// first call, then ignore subsequent calls until the duration has elapsed.

Also consider adding more context about the throttling behavior (leading edge vs trailing edge) to set proper expectations.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// widget ignore updates accordingly after a specified [duration] duration.
///
/// Example:
/// ```dart
/// String userInput = ''; // Your input value
///
/// // Create a throttle callback
/// final throttle = useThrottled(duration: const Duration(milliseconds: 500));
/// // Assume a fetch method fetchData(String query) exists
/// Button(onPressed: () => throttle(() => fetchData(userInput)));
/// ```
/// Creates a throttled callback that executes at most once per [duration].
///
/// The returned function will execute the provided callback immediately on the
/// first call, then ignore subsequent calls until the duration has elapsed.
///
/// Example:
///
🤖 Prompt for AI Agents
In packages/flutter_hooks/lib/src/throttled.dart around lines 3 to 13, improve
the documentation by correcting grammatical errors and clarifying the
explanation of the throttling behavior. Add details about whether the throttle
triggers on the leading edge, trailing edge, or both, to set proper expectations
for users. Ensure the example is clear and accurately demonstrates how to use
the throttle callback.

Comment on lines +30 to +37
void run(VoidCallback callback) {
if (!_isRunning) {
_timer = Timer(duration, () {
_timer = null;
});
callback();
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider edge case handling for invalid durations.

The current implementation doesn't handle edge cases like Duration.zero or negative durations, which could lead to unexpected behavior.

 void run(VoidCallback callback) {
+  if (duration <= Duration.zero) {
+    callback();
+    return;
+  }
+  
   if (!_isRunning) {
     _timer = Timer(duration, () {
       _timer = null;
     });
     callback();
   }
 }

For zero or negative durations, the callback should execute immediately without throttling.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void run(VoidCallback callback) {
if (!_isRunning) {
_timer = Timer(duration, () {
_timer = null;
});
callback();
}
}
void run(VoidCallback callback) {
if (duration <= Duration.zero) {
callback();
return;
}
if (!_isRunning) {
_timer = Timer(duration, () {
_timer = null;
});
callback();
}
}
🤖 Prompt for AI Agents
In packages/flutter_hooks/lib/src/throttled.dart around lines 30 to 37, the run
method does not handle zero or negative Duration values, which can cause
unexpected behavior. Modify the method to check if the duration is zero or
negative; if so, execute the callback immediately without setting a timer or
throttling. Otherwise, proceed with the existing throttling logic.

Comment on lines +21 to +38
class _Throttler {
_Throttler(this.duration);

final Duration duration;

Timer? _timer;

bool get _isRunning => _timer != null;

void run(VoidCallback callback) {
if (!_isRunning) {
_timer = Timer(duration, () {
_timer = null;
});
callback();
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify timer resource cleanup behavior.

The current implementation should handle timer cleanup correctly, but it's worth verifying that timers are properly disposed when the widget is unmounted.


🏁 Script executed:

#!/bin/bash
# Description: Check if there are examples of timer cleanup patterns in other hooks
# Expected: Find similar timer usage patterns and cleanup mechanisms

rg -A 10 -B 5 "Timer" --type dart packages/flutter_hooks/lib/src/

Length of output: 2978


🏁 Script executed:

#!/bin/bash
# Display _DebouncedHookState implementation and its dispose method if any
rg -A20 -B5 "class _DebouncedHookState" --type dart packages/flutter_hooks/lib/src/debounced.dart
echo "----Searching for dispose override in debounced.dart----"
rg "@override\s+void\s+dispose" --type dart packages/flutter_hooks/lib/src/debounced.dart

# Display _Throttler usage: look for HookState and dispose
rg -A20 -B5 "class _ThrottledHookState" --type dart packages/flutter_hooks/lib/src/throttled.dart || echo "No HookState class in throttled.dart"
echo "----Searching for dispose override in throttled.dart----"
rg "@override\s+void\s+dispose" --type dart packages/flutter_hooks/lib/src/throttled.dart

Length of output: 1224


🏁 Script executed:

#!/bin/bash
# Display full content of throttled.dart
sed -n '1,200p' packages/flutter_hooks/lib/src/throttled.dart

Length of output: 972


Cleanup pending timers on unmount

The _Throttler in packages/flutter_hooks/lib/src/throttled.dart schedules a Timer but never cancels it when the widget is disposed. If that timer fires after unmount, it may invoke callbacks on a disposed widget. To address this:

  • In _Throttler, add a cancel() method:
    void cancel() {
      _timer?.cancel();
      _timer = null;
    }
  • In useThrottled, register a cleanup effect:
    final throttler = useMemoized(() => _Throttler(duration), [duration]);
    useEffect(() {
      return () {
        throttler.cancel();
      };
    }, [throttler]);
    return throttler.run;
  • (Optional) Review packages/flutter_hooks/lib/src/debounced.dart—it likewise never cancels its timer on dispose and should follow a similar pattern.

These changes ensure no stray timers fire after the hook is unmounted.

🤖 Prompt for AI Agents
In packages/flutter_hooks/lib/src/throttled.dart around lines 21 to 38, the
_Throttler class creates a Timer but does not cancel it when the widget
unmounts, risking callbacks firing on disposed widgets. Fix this by adding a
cancel() method to _Throttler that cancels and nullifies the timer. Then, in the
useThrottled hook, register a cleanup effect that calls throttler.cancel() on
unmount. This ensures timers are properly cleaned up and no callbacks run after
disposal.

@itsMohammedAli
Copy link

Hi 👋,

Thanks for the useThrottled hook — it’s a great addition!

I’d like to suggest enhancing it with support for throttle modes (leading, trailing, and both) to give developers more control over when the throttled callback fires. This is especially useful for UI scenarios where you want either immediate response or delayed trailing updates (or both).

Additionally, adding .cancel() and .flush() methods to the returned controller would be very helpful:

  • .cancel() to abort any pending throttled calls and clean up timers (important on widget disposal or navigation).
  • .flush() to immediately execute any pending throttled call (useful for finalizing input or scroll events).

These features are common in popular throttle/debounce utilities like Lodash, and including them would make useThrottled more powerful and flexible for real-world Flutter apps.

I’m happy to help contribute these improvements if the maintainers are open to it. Thanks!

@ostk0069 ostk0069 changed the title feat: add useuseThrottle hooks. feat: add useThrottled hooks. Jul 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants