UNPKG

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
# 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); @override State<CounterWidget> creates => _CounterWidgetState(); } class _CounterWidgetState extends State<CounterWidget> { num count = 0; @override 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); @override 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; }); } @override 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); @override State<UserProfileWidget> createState() => _UserProfileWidgetState(); } class _UserProfileWidgetState extends State<UserProfileWidget> { Map? user; @override void initState() { super.initState(); fetchUser(widget.userId).then((value) { setState(() { user = value; }); }); } @override 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); @override State<SearchResultsWidget> createState() => _SearchResultsWidgetState(); } class _SearchResultsWidgetState extends State<SearchResultsWidget> { List results = []; @override void initState() { super.initState(); _performSearch(); } @override void didUpdateWidget(covariant SearchResultsWidget oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.query != widget.query) { _performSearch(); } } void _performSearch() { search(widget.query).then((value) { setState(() { results = value; }); }); } @override 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); @override State<WebSocketWidget> createState() => _WebSocketWidgetState(); } class _WebSocketWidgetState extends State<WebSocketWidget> { late WebSocket _ws; @override void initState() { super.initState(); _ws = WebSocket.connect('ws://example.com'); _ws.listen((message) { print(message); }); } @override void dispose() { _ws.close(); super.dispose(); } @override 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); @override 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); @override 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); @override 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); @override State<FocusInputWidget> createState() => _FocusInputWidgetState(); } class _FocusInputWidgetState extends State<FocusInputWidget> { late FocusNode inputRef; @override void initState() { super.initState(); inputRef = FocusNode(); // Request focus after build WidgetsBinding.instance.addPostFrameCallback((_) { inputRef.requestFocus(); }); } @override void dispose() { inputRef.dispose(); super.dispose(); } @override 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); @override State<ScrollableListWidget> createState() => _ScrollableListWidgetState(); } class _ScrollableListWidgetState extends State<ScrollableListWidget> { late ScrollController scrollRef; @override void initState() { super.initState(); scrollRef = ScrollController(); } @override void dispose() { scrollRef.dispose(); super.dispose(); } void _scrollToTop() { scrollRef.animateTo( 0, duration: Duration(milliseconds: 300), curve: Curves.easeInOut, ); } @override 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); @override State<ListWidget> createState() => _ListWidgetState(); } class _ListWidgetState extends State<ListWidget> { String filter = ''; void _handleItemClick(dynamic item) { print('Clicked $item'); } @override 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); @override State<ExpensiveWidget> createState() => _ExpensiveWidgetState(); } class _ExpensiveWidgetState extends State<ExpensiveWidget> { late num _cachedValue; late num _lastA; late num _lastB; @override void initState() { super.initState(); _lastA = widget.a; _lastB = widget.b; _cachedValue = _computeExpensiveValue(); } @override 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); } @override 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); @override State<MyWidget> createState() => _MyWidgetState(); } class _MyWidgetState extends State<MyWidget> with FetchMixin { @override void initState() { super.initState(); fetch('/api/data'); } @override 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