Skip to main content

Flutter Hooks

Flutter Hooks Fundamentals

What are Hooks?

Hooks are functions that allow you to use state and other React-like features in functional widgets. They provide a cleaner alternative to StatefulWidget.

Basic Setup

import 'package:flutter_hooks/flutter_hooks.dart';

// Convert StatelessWidget to HookWidget
class MyHookWidget extends HookWidget {

Widget build(BuildContext context) {
// Use hooks here
return Container();
}
}

State Management Hooks

useState

class CounterWidget extends HookWidget {

Widget build(BuildContext context) {
final counter = useState(0);

return Column(
children: [
Text('Count: ${counter.value}'),
ElevatedButton(
onPressed: () => counter.value++,
child: Text('Increment'),
),
],
);
}
}

// With custom type
class UserForm extends HookWidget {

Widget build(BuildContext context) {
final name = useState<String>('');
final email = useState<String>('');
final age = useState<int>(0);

return Column(
children: [
TextField(
onChanged: (value) => name.value = value,
decoration: InputDecoration(labelText: 'Name'),
),
TextField(
onChanged: (value) => email.value = value,
decoration: InputDecoration(labelText: 'Email'),
),
Text('Name: ${name.value}, Email: ${email.value}'),
],
);
}
}

useReducer

// Define actions
enum CounterAction { increment, decrement, reset }

// Define reducer
int counterReducer(int state, CounterAction action) {
switch (action) {
case CounterAction.increment:
return state + 1;
case CounterAction.decrement:
return state - 1;
case CounterAction.reset:
return 0;
}
}

class CounterWithReducer extends HookWidget {

Widget build(BuildContext context) {
final counter = useReducer(counterReducer, initialState: 0);

return Column(
children: [
Text('Count: ${counter.state}'),
Row(
children: [
ElevatedButton(
onPressed: () => counter.dispatch(CounterAction.increment),
child: Text('+'),
),
ElevatedButton(
onPressed: () => counter.dispatch(CounterAction.decrement),
child: Text('-'),
),
ElevatedButton(
onPressed: () => counter.dispatch(CounterAction.reset),
child: Text('Reset'),
),
],
),
],
);
}
}

Lifecycle Hooks

useEffect

class DataFetcher extends HookWidget {

Widget build(BuildContext context) {
final data = useState<List<String>>([]);
final loading = useState(true);

useEffect(() {
// This runs after the widget is built
Future.delayed(Duration(seconds: 2), () {
data.value = ['Item 1', 'Item 2', 'Item 3'];
loading.value = false;
});

// Cleanup function (optional)
return () {
print('Widget disposed');
};
}, []); // Empty dependency array = run only once

if (loading.value) {
return CircularProgressIndicator();
}

return ListView.builder(
itemCount: data.value.length,
itemBuilder: (context, index) {
return ListTile(title: Text(data.value[index]));
},
);
}
}

// With dependencies
class UserProfile extends HookWidget {
final int userId;

UserProfile({required this.userId});


Widget build(BuildContext context) {
final user = useState<User?>(null);

useEffect(() {
// Fetch user data when userId changes
fetchUser(userId).then((userData) {
user.value = userData;
});
}, [userId]); // Re-run when userId changes

return user.value == null
? CircularProgressIndicator()
: Text('Hello, ${user.value!.name}');
}
}

useMemoized

class ExpensiveCalculation extends HookWidget {
final List<int> numbers;

ExpensiveCalculation({required this.numbers});


Widget build(BuildContext context) {
// Memoize expensive calculation
final sum = useMemoized(() {
return numbers.reduce((a, b) => a + b);
}, [numbers]); // Recalculate only when numbers change

return Text('Sum: $sum');
}
}

// Complex object memoization
class UserList extends HookWidget {
final List<User> users;

UserList({required this.users});


Widget build(BuildContext context) {
final sortedUsers = useMemoized(() {
return List<User>.from(users)..sort((a, b) => a.name.compareTo(b.name));
}, [users]);

return ListView.builder(
itemCount: sortedUsers.length,
itemBuilder: (context, index) {
return ListTile(title: Text(sortedUsers[index].name));
},
);
}
}

Animation Hooks

useAnimationController

class AnimatedButton extends HookWidget {

Widget build(BuildContext context) {
final controller = useAnimationController(
duration: Duration(milliseconds: 300),
);

final scale = useAnimation(
Tween<double>(begin: 1.0, end: 0.95).animate(controller),
);

return GestureDetector(
onTapDown: (_) => controller.forward(),
onTapUp: (_) => controller.reverse(),
onTapCancel: () => controller.reverse(),
child: Transform.scale(
scale: scale,
child: ElevatedButton(
onPressed: () => print('Button pressed'),
child: Text('Animated Button'),
),
),
);
}
}

useAnimation

class FadeInWidget extends HookWidget {

Widget build(BuildContext context) {
final controller = useAnimationController(
duration: Duration(milliseconds: 500),
);

final opacity = useAnimation(
Tween<double>(begin: 0.0, end: 1.0).animate(controller),
);

useEffect(() {
controller.forward();
return null;
}, []);

return Opacity(
opacity: opacity,
child: Text('Fade in text'),
);
}
}

useTweenAnimation

class SlideInWidget extends HookWidget {

Widget build(BuildContext context) {
final animation = useTweenAnimation<double>(
tween: Tween(begin: -100.0, end: 0.0),
duration: Duration(milliseconds: 500),
);

return Transform.translate(
offset: Offset(animation.value, 0),
child: Container(
color: Colors.blue,
child: Text('Slide in from left'),
),
);
}
}

Timer & Stream Hooks

useTimer

class CountdownTimer extends HookWidget {
final int seconds;

CountdownTimer({required this.seconds});


Widget build(BuildContext context) {
final timeLeft = useState(seconds);

useTimer(
Duration(seconds: 1),
() {
if (timeLeft.value > 0) {
timeLeft.value--;
}
},
);

return Text('Time left: ${timeLeft.value}');
}
}

// One-time timer
class DelayedWidget extends HookWidget {

Widget build(BuildContext context) {
final show = useState(false);

useTimer(
Duration(seconds: 2),
() => show.value = true,
once: true, // Run only once
);

return show.value
? Text('Shown after 2 seconds')
: CircularProgressIndicator();
}
}

useStream

class StreamListener extends HookWidget {

Widget build(BuildContext context) {
final stream = useMemoized(() {
return Stream.periodic(Duration(seconds: 1), (i) => i);
}, []);

final snapshot = useStream(stream);

return snapshot.when(
data: (data) => Text('Count: $data'),
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
);
}
}

// With custom stream
class UserStream extends HookWidget {
final Stream<User> userStream;

UserStream({required this.userStream});


Widget build(BuildContext context) {
final snapshot = useStream(userStream);

return snapshot.when(
data: (user) => Text('User: ${user.name}'),
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Error loading user'),
);
}
}

Form & Validation Hooks

useTextEditingController

class FormWithHooks extends HookWidget {

Widget build(BuildContext context) {
final nameController = useTextEditingController();
final emailController = useTextEditingController();

// Listen to text changes
useEffect(() {
void listener() {
print('Name changed: ${nameController.text}');
}

nameController.addListener(listener);
return () => nameController.removeListener(listener);
}, [nameController]);

return Column(
children: [
TextField(
controller: nameController,
decoration: InputDecoration(labelText: 'Name'),
),
TextField(
controller: emailController,
decoration: InputDecoration(labelText: 'Email'),
),
ElevatedButton(
onPressed: () {
print('Name: ${nameController.text}');
print('Email: ${emailController.text}');
},
child: Text('Submit'),
),
],
);
}
}

useFormField

class CustomFormField extends HookWidget {

Widget build(BuildContext context) {
final field = useFormField<String>(
initialValue: '',
validator: (value) {
if (value?.isEmpty ?? true) {
return 'This field is required';
}
return null;
},
);

return Column(
children: [
TextFormField(
onChanged: field.didChange,
decoration: InputDecoration(
labelText: 'Required Field',
errorText: field.errorText,
),
),
ElevatedButton(
onPressed: () {
if (field.validate()) {
print('Valid: ${field.value}');
}
},
child: Text('Validate'),
),
],
);
}
}

Custom Hooks

Creating Custom Hooks

// Custom hook for API calls
AsyncValue<T> useApiCall<T>(Future<T> Function() apiCall) {
final state = useState<AsyncValue<T>>(const AsyncValue.loading());

useEffect(() {
apiCall().then(
(data) => state.value = AsyncValue.data(data),
onError: (error, stack) => state.value = AsyncValue.error(error, stack),
);
return null;
}, []);

return state.value;
}

// Usage
class UserList extends HookWidget {

Widget build(BuildContext context) {
final users = useApiCall(() => fetchUsers());

return users.when(
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) => ListTile(
title: Text(users[index].name),
),
),
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
);
}
}

Custom Hook with Parameters

// Custom hook for pagination
AsyncValue<List<T>> usePaginatedData<T>(
Future<List<T>> Function(int page) fetchData,
int page,
) {
final state = useState<AsyncValue<List<T>>>(const AsyncValue.loading());

useEffect(() {
fetchData(page).then(
(data) => state.value = AsyncValue.data(data),
onError: (error, stack) => state.value = AsyncValue.error(error, stack),
);
return null;
}, [page]);

return state.value;
}

// Custom hook for debounced search
String useDebouncedSearch(String query, Duration delay) {
final debouncedQuery = useState(query);

useEffect(() {
final timer = Timer(delay, () {
debouncedQuery.value = query;
});

return () => timer.cancel();
}, [query, delay]);

return debouncedQuery.value;
}

Advanced Patterns

useCallback

class OptimizedList extends HookWidget {

Widget build(BuildContext context) {
final items = useState<List<String>>([]);

// Memoize callback to prevent unnecessary rebuilds
final addItem = useCallback(() {
items.value = [...items.value, 'Item ${items.value.length + 1}'];
}, [items]);

final removeItem = useCallback((int index) {
items.value = items.value.where((_, i) => i != index).toList();
}, [items]);

return Column(
children: [
ElevatedButton(onPressed: addItem, child: Text('Add Item')),
...items.value.asMap().entries.map((entry) {
return ListTile(
title: Text(entry.value),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => removeItem(entry.key),
),
);
}),
],
);
}
}

useValueChanged

class ValueTracker extends HookWidget {

Widget build(BuildContext context) {
final counter = useState(0);

// Track value changes
useValueChanged(counter.value, (oldValue, newValue) {
print('Counter changed from $oldValue to $newValue');
});

return Column(
children: [
Text('Counter: ${counter.value}'),
ElevatedButton(
onPressed: () => counter.value++,
child: Text('Increment'),
),
],
);
}
}

usePrevious

class PreviousValueTracker extends HookWidget {

Widget build(BuildContext context) {
final currentValue = useState(0);
final previousValue = usePrevious(currentValue.value);

return Column(
children: [
Text('Current: ${currentValue.value}'),
Text('Previous: ${previousValue ?? 'None'}'),
ElevatedButton(
onPressed: () => currentValue.value++,
child: Text('Increment'),
),
],
);
}
}

Performance Tips

Optimizing Hook Usage

class OptimizedWidget extends HookWidget {

Widget build(BuildContext context) {
// Use const for static values
const duration = Duration(milliseconds: 300);

// Memoize expensive calculations
final expensiveValue = useMemoized(() {
return performExpensiveCalculation();
}, []);

// Use useCallback for functions passed to children
final onTap = useCallback(() {
print('Button tapped');
}, []);

return ElevatedButton(
onPressed: onTap,
child: Text('Optimized Button'),
);
}
}

Avoiding Common Pitfalls

class CorrectHookUsage extends HookWidget {

Widget build(BuildContext context) {
// ✅ Correct: Hooks at the top level
final counter = useState(0);
final controller = useAnimationController();

// ✅ Correct: useEffect with proper dependencies
useEffect(() {
controller.forward();
return () => controller.dispose();
}, [controller]);

// ❌ Wrong: Don't call hooks conditionally
// if (someCondition) {
// final state = useState(0); // This will cause issues
// }

return Container();
}
}

References