Skip to main content

React hooks in Flutter

If you write React, utopia_hooks is the Flutter state-management model that will feel most familiar. State is declared where logic runs, effects are keyed by dependencies, derived values are memoized, and custom hooks compose smaller hooks into a reusable unit.

The full migration guide is React to Flutter with hooks. This page is the short map.

React to Flutter mapping

Reactutopia_hooksNotes
useState(initial)useState(initial)Returns a mutable object; read and write through .value
useEffect(fn, deps)useEffect(fn, keys)Return a cleanup function when needed
useEffect(fn, [])useEffect(fn) or useEffect(fn, [])No keys means run once in utopia_hooks
useMemo(fn, deps)useMemoized(fn, keys)Prefer this for derived state
useCallback(fn, deps)useMemoized(() => fn, keys)A callback is a memoized function
useContext(Context)useProvided<T>()Reads a provided value by type
Custom hookCustom hookSame idea: a function that calls hooks
Async data in an effectuseAutoComputedStateAvoid hand-rolled loading/error flags
List paginationusePaginatedComputedStateCursor/page/token pagination built in
Hook testingSimpleHookContextUnit-test hook state without pumping widgets

The biggest differences

State is .value, not a tuple. React returns [value, setValue]; utopia_hooks returns one mutable state object.

final count = useState(0);
count.value++;

No keys means run once. In React, omitting the dependency array runs the effect after every render. In utopia_hooks, omitting keys is the empty-dependency case.

useEffect(() {
analytics.trackScreen('profile');
return null;
}); // runs once

Global state is part of the hook model. React's useContext is a primitive, but app state usually becomes Redux, Zustand, React Query, or a context stack. In utopia_hooks, shared state is a hook registered at the app root and read with useProvided<T>().

The View is separate. React components usually hold logic and JSX in the same function. In the recommended Flutter architecture, the state hook owns the logic and the View is a plain StatelessWidget.

What a React render function becomes

In React, a component often has state, effects, derived values, and returned UI in one function. In utopia_hooks, that logic becomes a state hook:

class ProfileScreenState {
final int count;
final String label;
final void Function() onIncrementPressed;

const ProfileScreenState({
required this.count,
required this.label,
required this.onIncrementPressed,
});
}

ProfileScreenState useProfileScreenState() {
final count = useState(0);
final label = useMemoized(() => 'count: ${count.value}', [count.value]);

useEffect(() {
analytics.trackScreen('profile');
return null;
});

return ProfileScreenState(
count: count.value,
label: label,
onIncrementPressed: () => count.value++,
);
}

The widget tree that React would return becomes a View:

class ProfileScreenView extends StatelessWidget {
final ProfileScreenState state;

const ProfileScreenView({required this.state});


Widget build(BuildContext context) {
return Column(
children: [
Text(state.label),
ElevatedButton(
onPressed: state.onIncrementPressed,
child: const Text('+'),
),
],
);
}
}

The coordinator Screen calls the hook once and returns the View. That split is the Screen / State / View pattern.

Where to go next