Moving Blaze to React Without Leaving Meteor

Two questions come up in almost every conversation about an older Meteor app, and teams tend to collapse them into one. The first is do we stay on Meteor? The second is what do we do about Blaze? They are separate decisions, and treating them as one is how a UI modernization turns into a platform rewrite.

Blaze still works. It ships with Meteor, it is maintained at a low level of activity, and a Blaze app in maintenance mode is a perfectly reasonable thing to own. The pressure is usually not technical: it is hiring. React and Vue experience is easy to find, Blaze template experience is not, and a team that spends the first month of every onboarding explaining Template.instance() and ReactiveVar feels that cost every quarter.

The useful news is that you can convert a Blaze UI to React inside your existing Meteor app, screen by screen, with both rendering engines live in the same page. No parallel front end, no cutover date, no feature freeze. Here is how we sequence it.

Blaze and React can coexist in one app

Meteor supports both renderers simultaneously. The community bridge packages give you two directions:

  • react-template-helper (or the maintained equivalent in your package set) lets a Blaze template mount a React component.
  • blaze-to-react style wrappers, or a small hand-rolled component, let a React tree mount a legacy Blaze template that you have not converted yet.

In practice the first direction carries the migration and the second direction saves you from big-bang moments. A typical intermediate state looks like this — a Blaze layout that still owns routing and chrome, hosting a React island for the part you have converted:

<template name="ordersPage">
  {{> layoutHeader}}
  {{> React component=OrdersTable orgId=orgId}}
  {{> layoutFooter}}
</template>
Template.ordersPage.helpers({
  OrdersTable: () => OrdersTable,
  orgId() { return FlowRouter.getParam('orgId'); },
});

The React side subscribes and reads data with useTracker from react-meteor-data:

function OrdersTable({ orgId }) {
  const { orders, loading } = useTracker(() => {
    const handle = Meteor.subscribe('orders.byOrg', orgId);
    return {
      loading: !handle.ready(),
      orders: Orders.find({ orgId }, { sort: { createdAt: -1 } }).fetch(),
    };
  }, [orgId]);

  if (loading) return <Spinner />;
  return <Table rows={orders} />;
}

Nothing exotic is happening. Both renderers are reading the same Minimongo collections through the same Tracker reactivity, which is exactly why this migration is incremental in a way that a framework exit is not: the data layer never moves.

Where the boundary goes, and what leaks across it

The coexistence is not free. Three things need a deliberate decision rather than a discovery in production.

Props are a snapshot, reactivity is not. When a Blaze helper passes orgId into a React component, Blaze re-renders the island when that helper invalidates. Pass a whole document down as a prop and you get either stale data or a component that remounts on every unrelated field change. Pass identifiers across the boundary and let the React side read reactive data itself with useTracker.

One owner per subscription. The common bug in a half-converted app is a Blaze template and a React component both subscribing to the same publication for the same screen. It works, which is the problem — it doubles observer load on the server for no visible symptom until traffic grows. Decide per screen who owns the subscription, and delete the other one in the same pull request that converts the view.

Routing is the awkward part. If you are on FlowRouter or Iron Router, keep it until the React surface is the majority. Running FlowRouter and React Router at once is possible and rarely worth it; the route layer is the last thing to convert, not the first. Iron Router adds friction here because it owns layout rendering as well, so plan an extra step to move layout out of the router before you touch routes themselves.

Sequencing: convert the screens that pay

We do not convert alphabetically or by file size. We convert in this order:

  1. A low-traffic leaf screen first, entirely to prove the tooling: build config, bridge package, test setup, and one deployment. Expect the first screen to cost several times what the second one does. That is the tooling tax, not the migration rate.
  2. Screens on the active roadmap. If a page is getting feature work in the next two quarters, converting it first means the new work is written once, in the target stack, by whoever you can hire.
  3. The gnarly shared components — data tables, modals, form widgets. Do these deliberately, as a small shared component library, once you have two or three converted screens telling you what the real API needs to be. Building the library first is how teams produce abstractions nobody uses.
  4. Leave stable, boring, rarely-touched screens for last, possibly forever. An admin page that has not changed in four years and has no roadmap does not need to be React. Finishing is optional; that is the main advantage of an incremental route.

One discipline makes the whole thing safe: never convert a screen and change its behavior in the same commit. A conversion PR should be reviewable as "same behavior, different renderer," with the feature change landing separately. Where you have no tests, add a thin integration test for the screen before conversion — it is cheaper than the alternative and it survives the migration.

How this interacts with Meteor 3

If you are also facing the Fibers-to-async conversion, do the async work first. It is server-side, it is on a support clock because of Node and MongoDB versions, and it does not conflict with UI work. Blaze has no deadline attached to it. Sequencing UI modernization ahead of a runtime that is aging out is the most common ordering mistake we see.

One more thing worth saying plainly: converting Blaze to React inside Meteor also happens to be a decent hedge. If you later decide the right end state is off Meteor entirely, React components reading data through a thin hook layer are far easier to carry into a Next.js app than Blaze templates are. Doing this work does not commit you to staying, and it does not commit you to leaving. That is usually the right shape for a decision you do not have to make this year.

If you are weighing Blaze against hiring reality on a revenue-carrying app, that is a concrete engineering question with a boundable answer. We are happy to look at the codebase and tell you what the screens would actually cost.