prprompts-flutter-generator
Version:
AI-powered Flutter development with full automation + official extension support - Generate 32 security-audited guides & auto-implement in 2-3 hours. NEW v5.1: Official Claude Code plugin with hooks, Gemini TOML commands, Qwen MCP settings. Features: Comp
924 lines (757 loc) • 19.5 kB
Markdown
# Hooks Conversion Examples
This document demonstrates React Hooks to Flutter pattern conversion.
## Overview
The hooks conversion system automatically converts:
- ✅ **useState** → StatefulWidget state fields
- ✅ **useEffect** → Lifecycle methods (initState, didUpdateWidget, dispose)
- ✅ **useContext** → Provider pattern
- ✅ **useReducer** → BLoC/Cubit pattern
- ✅ **useCallback** → Cached callbacks
- ✅ **useMemo** → Memoized values
- ✅ **useRef** → FocusNode/TextEditingController/ScrollController/GlobalKey
- ✅ **Custom hooks** → Mixins
---
## useState Conversion
### Simple State
**React:**
```jsx
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
};
```
**Flutter:**
```dart
class CounterWidget extends StatefulWidget {
const CounterWidget({Key? key}) : super(key: key);
State<CounterWidget> creates => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
num count = 0;
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () {
setState(() {
count = count + 1;
});
},
child: Text('Increment'),
),
],
),
);
}
}
```
### Multiple State Variables
**React:**
```jsx
const LoginForm = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
setLoading(true);
await login(email, password);
setLoading(false);
};
return (
<form onSubmit={handleSubmit}>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button disabled={loading}>
{loading ? 'Loading...' : 'Submit'}
</button>
</form>
);
};
```
**Flutter:**
```dart
class LoginFormWidget extends StatefulWidget {
const LoginFormWidget({Key? key}) : super(key: key);
State<LoginFormWidget> createState() => _LoginFormWidgetState();
}
class _LoginFormWidgetState extends State<LoginFormWidget> {
String email = '';
String password = '';
bool loading = false;
Future<void> _handleSubmit() async {
setState(() {
loading = true;
});
await login(email, password);
setState(() {
loading = false;
});
}
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: (value) {
setState(() {
email = value;
});
},
),
TextField(
obscureText: true,
onChanged: (value) {
setState(() {
password = value;
});
},
),
ElevatedButton(
onPressed: loading ? null : _handleSubmit,
child: Text(loading ? 'Loading...' : 'Submit'),
),
],
);
}
}
```
---
## useEffect Conversion
### Mount Only (Empty Dependency Array)
**React:**
```jsx
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // Empty deps = componentDidMount
return <div>{user?.name}</div>;
};
```
**Flutter:**
```dart
class UserProfileWidget extends StatefulWidget {
final String userId;
const UserProfileWidget({Key? key, required this.userId}) : super(key: key);
State<UserProfileWidget> createState() => _UserProfileWidgetState();
}
class _UserProfileWidgetState extends State<UserProfileWidget> {
Map? user;
void initState() {
super.initState();
fetchUser(widget.userId).then((value) {
setState(() {
user = value;
});
});
}
Widget build(BuildContext context) {
return Container(
child: Text(user?['name'] ?? ''),
);
}
}
```
### Dependency-Based Effects
**React:**
```jsx
const SearchResults = ({ query }) => {
const [results, setResults] = useState([]);
useEffect(() => {
search(query).then(setResults);
}, [query]); // Re-run when query changes
return <List data={results} />;
};
```
**Flutter:**
```dart
class SearchResultsWidget extends StatefulWidget {
final String query;
const SearchResultsWidget({Key? key, required this.query}) : super(key: key);
State<SearchResultsWidget> createState() => _SearchResultsWidgetState();
}
class _SearchResultsWidgetState extends State<SearchResultsWidget> {
List results = [];
void initState() {
super.initState();
_performSearch();
}
void didUpdateWidget(covariant SearchResultsWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.query != widget.query) {
_performSearch();
}
}
void _performSearch() {
search(widget.query).then((value) {
setState(() {
results = value;
});
});
}
Widget build(BuildContext context) {
return ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) => ListTile(title: Text(results[index])),
);
}
}
```
### Cleanup Functions
**React:**
```jsx
const WebSocketComponent = () => {
useEffect(() => {
const ws = new WebSocket('ws://example.com');
ws.onmessage = (event) => {
console.log(event.data);
};
// Cleanup function
return () => {
ws.close();
};
}, []);
return <div>WebSocket Active</div>;
};
```
**Flutter:**
```dart
class WebSocketWidget extends StatefulWidget {
const WebSocketWidget({Key? key}) : super(key: key);
State<WebSocketWidget> createState() => _WebSocketWidgetState();
}
class _WebSocketWidgetState extends State<WebSocketWidget> {
late WebSocket _ws;
void initState() {
super.initState();
_ws = WebSocket.connect('ws://example.com');
_ws.listen((message) {
print(message);
});
}
void dispose() {
_ws.close();
super.dispose();
}
Widget build(BuildContext context) {
return Container(
child: Text('WebSocket Active'),
);
}
}
```
---
## useContext Conversion
### Basic Context
**React:**
```jsx
const ThemeContext = React.createContext();
const ThemedButton = () => {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.primary }}>
Click me
</button>
);
};
```
**Flutter:**
```dart
class ThemeProvider extends ChangeNotifier {
Color primary = Colors.blue;
void updateTheme(Color color) {
primary = color;
notifyListeners();
}
}
class ThemedButton extends StatelessWidget {
const ThemedButton({Key? key}) : super(key: key);
Widget build(BuildContext context) {
final theme = context.watch<ThemeProvider>();
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.primary,
),
onPressed: () {},
child: Text('Click me'),
);
}
}
// Usage
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ThemeProvider()),
],
child: ThemedButton(),
)
```
### Multiple Contexts
**React:**
```jsx
const App = () => {
const theme = useContext(ThemeContext);
const auth = useContext(AuthContext);
const lang = useContext(LanguageContext);
return <div>...</div>;
};
```
**Flutter:**
```dart
class AppWidget extends StatelessWidget {
const AppWidget({Key? key}) : super(key: key);
Widget build(BuildContext context) {
final theme = context.watch<ThemeProvider>();
final auth = context.watch<AuthProvider>();
final lang = context.watch<LanguageProvider>();
return Container(
// Use providers
);
}
}
// Setup
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ThemeProvider()),
ChangeNotifierProvider(create: (_) => AuthProvider()),
ChangeNotifierProvider(create: (_) => LanguageProvider()),
],
child: AppWidget(),
)
```
---
## useReducer Conversion (to Cubit)
**React:**
```jsx
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
};
const Counter = () => {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</div>
);
};
```
**Flutter (using Cubit):**
```dart
// State
class CounterState {
final int count;
CounterState(this.count);
}
// Cubit
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(CounterState(0));
void increment() => emit(CounterState(state.count + 1));
void decrement() => emit(CounterState(state.count - 1));
}
// Widget
class CounterWidget extends StatelessWidget {
const CounterWidget({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return BlocBuilder<CounterCubit, CounterState>(
builder: (context, state) {
return Column(
children: [
Text('Count: ${state.count}'),
ElevatedButton(
onPressed: () => context.read<CounterCubit>().increment(),
child: Text('+'),
),
ElevatedButton(
onPressed: () => context.read<CounterCubit>().decrement(),
child: Text('-'),
),
],
);
},
);
}
}
// Usage
BlocProvider(
create: (_) => CounterCubit(),
child: CounterWidget(),
)
```
---
## useRef Conversion
### Input Focus
**React:**
```jsx
const FocusInput = () => {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
};
```
**Flutter:**
```dart
class FocusInputWidget extends StatefulWidget {
const FocusInputWidget({Key? key}) : super(key: key);
State<FocusInputWidget> createState() => _FocusInputWidgetState();
}
class _FocusInputWidgetState extends State<FocusInputWidget> {
late FocusNode inputRef;
void initState() {
super.initState();
inputRef = FocusNode();
// Request focus after build
WidgetsBinding.instance.addPostFrameCallback((_) {
inputRef.requestFocus();
});
}
void dispose() {
inputRef.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return TextField(
focusNode: inputRef,
);
}
}
```
### Scroll Controller
**React:**
```jsx
const ScrollableList = () => {
const scrollRef = useRef(null);
const scrollToTop = () => {
scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
};
return (
<div>
<button onClick={scrollToTop}>Scroll to Top</button>
<div ref={scrollRef} style={{ overflow: 'auto' }}>
{/* content */}
</div>
</div>
);
};
```
**Flutter:**
```dart
class ScrollableListWidget extends StatefulWidget {
const ScrollableListWidget({Key? key}) : super(key: key);
State<ScrollableListWidget> createState() => _ScrollableListWidgetState();
}
class _ScrollableListWidgetState extends State<ScrollableListWidget> {
late ScrollController scrollRef;
void initState() {
super.initState();
scrollRef = ScrollController();
}
void dispose() {
scrollRef.dispose();
super.dispose();
}
void _scrollToTop() {
scrollRef.animateTo(
0,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: _scrollToTop,
child: Text('Scroll to Top'),
),
Expanded(
child: ListView(
controller: scrollRef,
children: [
// content
],
),
),
],
);
}
}
```
---
## useCallback & useMemo
### useCallback
**React:**
```jsx
const List = ({ items }) => {
const [filter, setFilter] = useState('');
const handleItemClick = useCallback((item) => {
console.log('Clicked', item);
}, []); // Memoized callback
return (
<div>
{items.map(item => (
<Item key={item.id} onClick={() => handleItemClick(item)} />
))}
</div>
);
};
```
**Flutter:**
```dart
// In Flutter, methods are already memoized by the class instance
class ListWidget extends StatefulWidget {
final List items;
const ListWidget({Key? key, required this.items}) : super(key: key);
State<ListWidget> createState() => _ListWidgetState();
}
class _ListWidgetState extends State<ListWidget> {
String filter = '';
void _handleItemClick(dynamic item) {
print('Clicked $item');
}
Widget build(BuildContext context) {
return ListView.builder(
itemCount: widget.items.length,
itemBuilder: (context, index) {
final item = widget.items[index];
return ItemWidget(
item: item,
onTap: () => _handleItemClick(item),
);
},
);
}
}
```
### useMemo
**React:**
```jsx
const ExpensiveComponent = ({ a, b }) => {
const expensiveValue = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]); // Only recompute if a or b changes
return <div>{expensiveValue}</div>;
};
```
**Flutter:**
```dart
class ExpensiveWidget extends StatefulWidget {
final num a;
final num b;
const ExpensiveWidget({Key? key, required this.a, required this.b}) : super(key: key);
State<ExpensiveWidget> createState() => _ExpensiveWidgetState();
}
class _ExpensiveWidgetState extends State<ExpensiveWidget> {
late num _cachedValue;
late num _lastA;
late num _lastB;
void initState() {
super.initState();
_lastA = widget.a;
_lastB = widget.b;
_cachedValue = _computeExpensiveValue();
}
void didUpdateWidget(covariant ExpensiveWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.a != widget.a || oldWidget.b != widget.b) {
_lastA = widget.a;
_lastB = widget.b;
_cachedValue = _computeExpensiveValue();
}
}
num _computeExpensiveValue() {
return computeExpensiveValue(widget.a, widget.b);
}
Widget build(BuildContext context) {
return Container(
child: Text('$_cachedValue'),
);
}
}
```
---
## Custom Hooks to Mixins
**React:**
```jsx
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// Usage
const MyComponent = () => {
const { data, loading, error } = useFetch('/api/data');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error!</div>;
return <div>{data}</div>;
};
```
**Flutter (using Mixin):**
```dart
mixin FetchMixin<T extends StatefulWidget> on State<T> {
dynamic data;
bool loading = false;
dynamic error;
Future<void> fetch(String url) async {
setState(() {
loading = true;
});
try {
final response = await http.get(Uri.parse(url));
setState(() {
data = jsonDecode(response.body);
loading = false;
});
} catch (e) {
setState(() {
error = e;
loading = false;
});
}
}
}
// Usage
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with FetchMixin {
void initState() {
super.initState();
fetch('/api/data');
}
Widget build(BuildContext context) {
if (loading) return CircularProgressIndicator();
if (error != null) return Text('Error!');
return Text('$data');
}
}
```
---
## Testing
The hooks conversion system includes comprehensive tests covering:
- ✅ 38 test cases
- ✅ useState with all types (number, string, bool, array, object)
- ✅ useEffect with different dependency patterns
- ✅ useContext with Provider pattern
- ✅ useReducer to Cubit conversion
- ✅ useCallback and useMemo
- ✅ useRef to Flutter controllers
- ✅ Custom hooks to mixins
- ✅ Code generation for lifecycle methods
- ✅ Edge cases and error handling
Run tests:
```bash
npm test -- tests/refactoring/generators/hooks-conversion.test.js
```
---
## API Usage
```javascript
const hooksConverter = require('./lib/refactoring/generators/HooksConverter');
const hooks = [
{
name: 'useState',
variables: ['count', 'setCount'],
initialValue: 0,
type: 'number',
},
{
name: 'useEffect',
callback: 'fetchData()',
dependencies: [],
hasCleanup: false,
},
];
const result = hooksConverter.convertHooks(hooks);
const code = hooksConverter.generateFlutterCode(result);
console.log(code.stateClass);
// " num count = 0;"
console.log(code.initState);
// "@override\n void initState() {\n super.initState();\n fetchData()\n }"
```
---
## Limitations & Future Work
### Current Limitations
1. **useLayoutEffect**: Not directly supported (use useEffect with immediate DOM access patterns)
2. **useImperativeHandle**: Requires manual conversion
3. **Complex custom hooks**: May need manual review
4. **Hooks rules**: Conditional hooks not automatically detected
### Future Enhancements
- [ ] Automatic hooks rules validation
- [ ] Advanced custom hooks patterns
- [ ] Performance optimization hints
- [ ] React 18 concurrent features
---
## See Also
- **HooksConverter.js** - Main hooks conversion engine
- **WidgetGenerator.js** - Widget generation with hooks support
- **ReactParser.js** - React code parsing with hook extraction
- **hooks-conversion.test.js** - Comprehensive test suite