React and Ember: What I Actually Learned Using Both

Photo by Fili Santillan on Unsplash
I started learning React in early 2018. I caught the tail end of class components, wrote a few, understood componentDidMount, then watched the ecosystem pivot to hooks and functional components within a year. That transition taught me more about React than any tutorial. The ground just shifted under a codebase I was actively writing, and I had to keep up.
Later I ended up working with Ember at LinkedIn. That’s when I started having actual opinions instead of preferences.
React Gave Me Freedom I Didn’t Know What to Do With
React gives you useState, useEffect, and a component model. Everything else you figure out yourself. When I was learning, this felt great. I could structure a project however I wanted. I picked React Router because it was popular. I tried Redux, got frustrated with the boilerplate, switched to Context plus useReducer, and eventually landed on whatever the team I joined was already using.
That last part is the reality of React. You don’t really choose your stack. You inherit it. Every React codebase I’ve opened has a different opinion about state management, data fetching, file structure, and testing. Some of those opinions are good. Some are whatever the original developer was excited about that month.
Here’s what a typical React data-fetching component looks like. It works fine, but notice how many decisions are baked in that have nothing to do with React itself:
// You've already chosen: React Router for params, TanStack Query
// for fetching, and where to put this file is anyone's guess
import { useParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
function UserPage() {
const { userId } = useParams();
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
});
if (isLoading) return <div>Loading...</div>;
if (error) return <p>Something went wrong.</p>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}Loading state? You handle it. Error state? You handle it. Where does this component live in the file tree? Up to you. Every React project reinvents these answers.
Ember at LinkedIn: Conventions I Respected, Tooling I Didn’t
When I opened LinkedIn’s Ember codebase for the first time, the structure was immediately legible. Routes in /routes, components in /components, services in /services, adapters and serializers in their own directories. I didn’t have to ask where things lived. Ember told me.
The equivalent of that React component above looks different in Ember. Not just syntactically, but architecturally. Routing, data loading, and error handling are framework concerns:
// app/router.js - routes are declared once, centrally
Router.map(function () {
this.route('user', { path: '/user/:user_id' });
});
// app/routes/user.js - the framework calls this before rendering
import Route from '@ember/routing/route';
export default class UserRoute extends Route {
async model({ user_id }) {
return this.store.findRecord('user', user_id);
}
}{{!-- app/templates/user/loading.hbs - Ember renders this automatically --}}
<LoadingSpinner />
{{!-- app/templates/user/error.hbs - and this on failure --}}
<p>Failed to load user.</p>
{{!-- app/templates/user.hbs - your actual template --}}
<h1>{{@model.name}}</h1>
<p>{{@model.email}}</p>Loading and error states aren’t your problem. The framework handles the lifecycle. You define a model() hook, Ember does the rest. I liked this a lot. Ember Data, the model and adapter layer, was one of the things I appreciated most. You define how your API maps to models once, and the store service handles caching, relationships, and serialization across the app. On a large codebase with hundreds of contributors, that consistency matters.
That said, LinkedIn’s Ember codebase didn’t have TypeScript when I was working in it. Ember’s type story was not there yet. Coming from the React ecosystem where TypeScript adoption was already strong, this felt like a step backward. I’d gotten used to autocomplete, type-checked props, and catching bugs before runtime. In Ember, I was back to reading documentation and hoping I spelled a property name right.
Then there was Broccoli. Ember’s legacy build tool. If you’ve used Webpack or Vite, Broccoli feels like a relic. Build times were slow. Configuration was opaque. The React world had already moved to fast HMR with Webpack dev server, and Vite was making it feel instant. Ember’s build pipeline was from a different era.
The templating language is another friction point. Handlebars served Ember well for years, but writing .hbs files in 2022 feels dated. No inline expressions, limited composability, a syntax unfamiliar to anyone coming from JSX. Every time I switched between JSX and Handlebars, there was a context-switch tax. Handlebars isn’t bad on its own. But the rest of the industry moved toward colocating markup and logic, and Handlebars keeps them separate in a way that feels more like an ideology than a practical choice.
The Reactivity Difference Under the Hood
One thing I didn’t appreciate until I’d worked with both: they think about rendering differently.
React re-runs your entire component function when state changes. Then it diffs a virtual DOM tree to figure out what actually changed in the real DOM. This is fine for most cases, but at scale you start sprinkling React.memo, useMemo, and useCallback everywhere to prevent unnecessary re-renders. Debugging why a component re-rendered when it shouldn’t have is a whole category of React work.
// React: you manage render optimization manually
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []);
// Without useCallback, this function is recreated every render,
// which can cascade re-renders through child componentsEmber’s Glimmer engine works differently. It compiles templates and tracks which specific values each DOM node depends on via the @tracked decorator. When a tracked property changes, only the DOM nodes that reference it update. No diffing a whole tree. No memoization hooks.
// Ember: the framework tracks dependencies automatically
@tracked count = 0;
@action increment() {
this.count++; // Glimmer knows exactly which DOM node to update
}At LinkedIn’s scale, with deep component trees and hundreds of components on screen, this meant fewer “why is this re-rendering?” investigations. The tradeoff is that when something does go wrong in Glimmer, the abstraction is thicker and harder to debug. But day to day, I spent less time thinking about render performance in Ember than I ever did in React.
What Ember Needs to Survive
I think Ember has a real future, but only if it modernizes in specific ways.
TypeScript support needs to be first-class. Not an addon, not experimental. Baked in. The React ecosystem proved that TypeScript isn’t optional anymore. Developers expect type safety, and they’ll choose frameworks that provide it.
Vite needs to replace Broccoli and Embroider needs to finish the job. The Embroider project is moving Ember toward standard JavaScript tooling, but it’s been a long migration. Ember needs builds that feel as fast as vite dev. Until then, the developer experience gap is real.
The templating story needs to evolve. I’m not saying Ember should adopt JSX wholesale, but something needs to change. Whether it’s a JSX-like syntax, single-file components, or first-class TypeScript in templates, developers coming from React or Vue shouldn’t have to learn Handlebars in 2022. The community has explored ideas like <template> tags and GJS/GTS formats. That’s the right direction.
Ember’s conventions are genuinely valuable. The framework-level opinions about routing, data, testing, and project structure solve real problems that React teams solve ad hoc, differently, every time. But conventions only matter if people adopt the framework, and adoption requires modern tooling.
Where I Actually Land
If someone asks me to start a new project with a small team and get to market fast, I’m picking React. The ecosystem is enormous, hiring is easy, and the tooling is mature. There are packages for everything. When you get stuck at 2am, Stack Overflow has the answer.
If someone asks me to build an application that hundreds of engineers will contribute to for years, I’d seriously look at Ember. Or at least I’d take what Ember taught me about conventions and apply them to whatever framework I’m using. A framework having opinions about project structure, data management, and testing isn’t a constraint. It’s a feature that most React teams end up reinventing poorly.
React optimizes for the individual developer’s freedom. Ember optimizes for the team’s consistency. Which one matters more depends on how big your team is and how long you plan to maintain the code. I didn’t understand that until I’d worked on both sides.