Building Web Apps with Familiar Patterns
You already know the patterns. Today we translate them.
| Layer | React Stack | Flutter Equivalent |
|---|---|---|
| Routing + Data | React Router 7 | GoRouter + BloC |
| Components | React 19 | Flutter Widgets |
| Styling | Tailwind CSS | Inline styles / Theme |
| UI Kit | Shadcn/UI | Material / Cupertino |
| Runtime | Cloudflare Workers | Firebase / Cloud Run |
Bundlers, TypeScript, Tailwind
Loaders, Actions, Components
Hopefully you've already set this up.
If not, here's your chance while I keep yapping.
Flutter is "Widgets all the way down."
React is "Components all the way down."
Widget tree → Component tree
BloC events → Loaders & Actions
bloc.add() → <Form>
ShellRoute child → <Outlet />
build(BuildContext) needed — just return JSX
useState hook lives inside the function
Loaders & Actions — DB data, API calls
useState — UI toggles, form inputs, modals
In Flutter, you trigger a BloC event in initState to fetch data.
In React Router, the loader fetches data before the component renders.
BlocBuilder<State> → useLoaderData()
Client-side. Streams are long-lived.
Request-scoped. Per-navigation.
Context, Zustand, or HTTP caching.
Back button? Loader runs again.
Users share URLs. State must restore.
Every page load starts fresh.
Loaders ARE your API
DB credentials never reach browser
HTML rendered on server, hydrated on client
From imperative onPressed: () => bloc.add(Event) to declarative <Form>.
The router handles submission like a BloC handles an added event.
<Form method="post"> = bloc.add(Event())
navigation.state: "idle" | "submitting" | "loading"
child = React Router's <Outlet />
Renders an <a> tag, works with browser
After form submit, programmatic redirects
<Link> — it's accessible and works without JavaScript
Real architecture. Tiny scope. Let's build it.
| /bookmarks | loader | action |
| /bookmarks/:id | loader | — |
Fetch all bookmarks
Fetch single by ID
Create new bookmark
Remove bookmark
Toggle favorite status
| I want to... | Flutter / BloC | React Router |
|---|---|---|
| Fetch data on load | on<LoadEvent> | loader function |
| Submit a form | bloc.add(Event()) | <Form> -> action |
| Show loading state | BlocBuilder + Loading | useNavigation() |
| Access route params | state.pathParameters | params.id |
| Navigate | context.go('/path') | useNavigate() |
| Render child route | child in ShellRoute | <Outlet /> |
| Handle errors | try/catch + Error state | ErrorBoundary |
Caught automatically
Caught automatically
Caught automatically
ErrorBoundary from any route — errors bubble up to nearest boundary
Styling, routing, API client
Loaders, actions, components
"favorite" intent to the action<Form> with star buttonCheckout a checkpoint branch
Same patterns. Different syntax.
You've got this.