Skip to main content

Command Palette

Search for a command to run...

What a Flutter Developer Learns When They Pick Up React

Updated
5 min readView as Markdown
J
I'm a senior frontend and mobile developer with a decade of experience building things that work - and occasionally fixing things that don't.

When I started learning React, I thought it would be easy. It was, mostly.

I've worked across enough languages and frameworks over the years that I don't dread the switch. The engineering fundamentals travel. It's often just a matter of learning the syntax, and IDEs do most of the heavy lifting now.

What does travel, and what doesn't — that's the more interesting question. Mental models and instincts built in one stack will carry you a long way in another. But they won't always translate neatly, and occasionally you'll hit something that isn't a translation problem at all. It's a new model, and the only way through it is to recognise it for what it is and build from scratch.

Component composition

In Flutter, UI is built by composing widgets. Every element on screen is a widget, and complex layouts are built by nesting simpler ones. The build() method returns the tree.

class ProductCard extends StatelessWidget {
  const ProductCard({required this.title, required this.price, super.key});

  final String title;
  final double price;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children: [
          Text(title),
          Text('£$price'),
        ],
      ),
    );
  }
}

React components map onto this cleanly. A function that accepts props and returns JSX, composed in exactly the same way. The mental model — break the screen into discrete pieces, pass data down, compose upward — transfers directly.

function ProductCard({ title, price }) {
  return (
    <div className="card">
      <p>{title}</p>
      <p>£{price}</p>
    </div>
  );
}

The shift from class-based to functional feels big at first, but the underlying thinking is the same. If you've built a widget tree in Flutter, you already understand component composition in React. You're not learning a new concept — you're learning where it lives in a different framework.

Stateless and stateful

React distinguishes between components that own state and components that don't. A component that just receives props and renders them has no state of its own — it's purely presentational. A component that manages its own data uses useState to do so.

// Presentational — no state, just renders what it receives
function ProductCard({ title, price }) {
  return (
    <div className="card">
      <p>{title}</p>
      <p>£{price}</p>
    </div>
  );
}

// Stateful — owns and manages its own data
function AddToCartButton({ productId }) {
  const [added, setAdded] = useState(false);
  return (
    <button onClick={() => setAdded(true)}>
      {added ? 'Added' : 'Add to cart'}
    </button>
  );
}

Flutter draws the same distinction explicitly in its type system. StatelessWidget for components that depend only on their configuration, StatefulWidget for components that need to manage change over time.

The pattern exists in both because it's not a framework decision — it's an architectural one. Keeping presentational components free of state makes them easier to reuse, easier to test, and easier to understand. If you've been applying this in Flutter, you already know how to apply it in React.

Server and client components

Flutter is client-side. Everything renders on the device. There's no server-rendering step to reason about, no boundary between what runs where.

Server components broke that assumption.

In Next.js App Router, components are server-rendered by default. They run on the server, have direct access to data sources, and send HTML to the client. Client components — anything that needs interactivity, browser APIs, or React hooks — are opted in explicitly with 'use client'.

// Server component — no directive needed, runs on the server
async function ProductList() {
  const products = await getProducts();
  return <ul>{products.map(p => <ProductCard key={p.id} {...p} />)}</ul>;
}
'use client';

// Client component — needs the directive, runs in the browser
function AddToCartButton({ productId }) {
  const [added, setAdded] = useState(false);
  return <button onClick={() => setAdded(true)}>{added ? 'Added' : 'Add to cart'}</button>;
}

Nothing in a Flutter background prepares you for this boundary. It's not a syntax problem — it's a rendering model you haven't needed to think about before.

Knowing when to stop translating

The instinct when learning a new framework is to look for the equivalent. What's the React version of this? Where does this concept live here? It's a good instinct.

But it has its limits. If you force a new concept to fit your existing mental models, you'll never truly understand it, and if you write code you don't understand, how will anyone else?

I've seen this mismatch in practice. I was reviewing a tech task from a React developer transitioning to Flutter. Rather than reaching for Flutter's own state and lifecycle model, they'd pulled in packages to emulate React hooks — effectively trying to write React inside Flutter. The build() method was doing things a build() method shouldn't be doing.

It wasn't a Flutter problem. It was a translation problem that had gone one step too far.

Server and client components caught me in exactly the same way. They were confusing for precisely as long as I was trying to map them onto something I already knew. The moment I stopped looking for the SPA equivalent and started treating the server/client boundary as its own concept to learn from scratch, it started to make sense.

That's the skill worth developing — not knowing everything, but recognising quickly which category you're in. Is this unfamiliar, or is this genuinely new?

What transfers

If you're coming to React from another framework, the instinct to map what you already know is the right one. Use it. The compositional thinking, the component boundaries, the habit of breaking a screen into its smallest meaningful pieces — these travel. The decade you've spent building UI isn't framework-specific knowledge.

The skill isn't knowing everything — it's recognising when you've hit the edge of what your existing models cover, and being willing to set them down and build something new.

You know more than you think, but there's always something new to learn.

If you're mid-transition between frameworks right now — what's transferred cleanly, and what's required a new model entirely? Drop it in the comments.