Two Roads, Revisited

October 22, 2026 by jason

From AngularJS to React to agentic coding: a decade building Abode and Moved.

In March 2015 I wrote my last post on this site. I announced that a few collaborators and I were going to build the same single page app twice, once in Angular and once in React, and compare the two. I promised to “continue with updates about the app when we start making noteworthy progress.”

This is that update. It is eleven years late, and the comparison has a winner.

The site went quiet that same month for a very good reason. Back in 2012 I wrote Two Roads, weighing whether to keep growing fourfightingfoxes or go back to full-time employment. I chose option (b) and joined SelectMinds, which Oracle later acquired. That road turned out to matter more than any pros and cons list could have predicted, because it is where I met Adam, then a product manager, and Joe, then a head of engineering. In March 2015 Adam recruited me to join Abode on day one, building a fee-free rental listing platform to simplify finding your next home. In February 2016 Abode became Moved, and for the next three years we built a consumer product on AngularJS. Joe joined us in 2018 and is now our CTO, my boss, and one of my closest friends.

Then in 2019 the business changed shape. Moved became a platform that unites people who are moving with their property management software and the vendor networks around them. It provides expert guidance and connects every party in one place, so that one of life’s most stressful events gets a little less stressful. It is B2B at its core, with a consumer-centric experience for residents and logistics and compliance tools for our real estate partners.

A new business meant a clean slate, and I was a loud voice for rebuilding the front end in React. React hooks had shipped only weeks earlier, and I was convinced they were the future. On March 12, 2019 I made the first commit to a brand new repository: “initial scaffold based on create-react-app with feature centric folder structure.” That repository now holds more than 5,100 commits, about 2,170 of them mine. I stand by that bet completely.

The Moved resident dashboard, showing a welcome banner, a move route map, move details, and a list of move-out tasks with their statuses

The resident dashboard as of this month, shown with a demo account.

None of this was a solo act. A small, talented team of engineers and designers built Moved with me, and plenty of the ideas below started as someone else’s good idea. If I had to boil a decade down to a few decisions with the biggest impact, these are the four I’d pick, starting with the one I’m proudest of.

1. Composition over configuration

Every long-lived front end grows a few “do everything” components. Ours were config-driven rendering engines: hand a DynamicForm an array of field definitions and it renders the whole form. That feels efficient right up until the hundredth special case. Every new requirement becomes another flag in the config, and the component turns into a tiny interpreter that nobody wants to touch.

The better answer is composition. Small components that each do one thing, assembled as JSX, with shared state flowing through context:

// Before: configure a black box
<DynamicForm
  fields={[
    { type: 'text', name: 'firstName', label: 'First name' },
    { type: 'select', name: 'unit', options: units, hidden: !hasUnits },
    { type: 'date', name: 'moveDate', minDate: today },
  ]}
  onSubmit={save}
/>

// After: compose the pieces you need
<Form name='profile' validation={schema} onSubmit={save}>
  <FormText name='firstName' label='First name' />
  {hasUnits && <FormSelect name='unit' options={units} />}
  <FormDateSelect name='moveDate' minDate={today} />
</Form>

The composed version reads like the UI it produces. Conditional logic is plain JavaScript instead of a config flag, and a one-off need gets a one-off child instead of a new option that every other form has to carry. Our shared components now follow this pattern: a parent component with attached subcomponents, like Modal.Title, Search.Results, and Sidebar.NavItem, that you assemble rather than configure. It took years to fully commit to this, and the biggest of these migrations only landed this past year (more on that below), but it is the single design decision I am proudest of.

2. Hooks changed everything

Hooks were the reason I pushed for React in the first place, and they paid off immediately. Our early components passed data through layer after layer of props just to reach the one component that needed it. A December 2020 commit of mine reads, in its entirety, “cleanup props to Header and upstream prop drilling we don’t need to do anymore, Yay Hooks.”

The pattern that grew from there is simple: anything app-wide (the current user, notifications, analytics, and a modal and overlay system that can stack multiple dialogs) lives in a context, and components reach it with a hook. Logic that used to be tangled into class components moved into small, reusable custom hooks, which made both the logic and the markup easier to read. Hooks are also what made the composition patterns above possible, since compound components share their state through context.

3. One monorepo, many apps, a strict hierarchy

From the first month we chose a monorepo with several apps (resident, admin, vendor, and maintenance) built on top of shared libraries with a strict dependency hierarchy:

  • services: contexts, utilities, the API client, and the Redux tooling. It may only depend on outside packages.
  • ui: the design system. It may use services.
  • product: shared business components. It may use services and ui.
  • apps: everything else, built from all three.

That decision has paid dividends since day one. A fix in a shared library ships to every app at once, a new app starts with years of tooling already in place, and the one-way dependency rule keeps the layers honest. When the bottom layer cannot import from the top, a lot of tangles simply cannot happen.

A couple of months in, we added Storybook, a workshop for building and previewing components in isolation. To be honest, for years it was little more than a place to eyeball them, and it took a long time to unlock what it is actually good at. Rewriting every story in the modern format (2022), adding live controls (2023), and building a real design system called Sondheim alongside the old one (2023 onward) turned it into a self-documenting component library: 74 components across 145 story files, each with a props table and interactive examples. Today every one of those stories also runs as a smoke test in a headless browser, so a component that breaks on render fails the build.

Storybook showing the CreditCardLogo component documentation page with a live preview, a props table with interactive controls, and a gallery of payment logos

A self-documenting component: a live preview, a props table with controls, and a gallery story, generated from the component and its stories.

4. Taming Redux

Redux was the standard for app-wide state in 2019, and it came with famously heavy boilerplate. Every API call needed action type constants, action creators, a thunk, reducer cases for request, success, and failure, and a selector. That is dozens of lines spread across several files, repeated for every endpoint. Copy and paste was the real architecture.

In 2020 I wrote a generator that collapses all of it into one declarative definition. Pending state tracking, caching, and request cancellation all come for free:

export const {
  reducer,
  useAction: useGetBuilding,
  useResource: useBuilding,
  usePending: useGetBuildingPending,
} = generate.redux({
  namespace: 'GET_BUILDING',
  request: (id, signal) => request({ url: `/buildings/${id}`, signal }),
  selector: (state, id) => state.buildings[id],
  cache: { minutes: 5 },
});

// In a component:
const getBuilding = useGetBuilding();
const building = useBuilding(id);
const pending = useGetBuildingPending();

It was a massive quality-of-life improvement for the whole team, but the bigger win came later. Because every one of hundreds of actions shares the same shape, we could improve all of them at once. We moved onto Redux Toolkit in 2024. Then in 2026 we exposed every action as an auto-dispatching hook that cancels its own request when the component unmounts, a change that touched 987 files in one pass. A sitewide refactor like that is only realistic when the code was consistent to begin with.

The last year: from writing code to validating it

Here is where the story speeds up. I started experimenting with AI coding tools in the fall of 2025. GitHub Copilot in VS Code gave me useful autocomplete, but it was underwhelming for anything bigger. Around the end of 2025 I started using Claude Code, and that is when things changed.

My first real project with it was migrating the entire monorepo from Create React App to Vite, which touched 1,658 files. I micromanaged every step, because I did not yet trust it. Each migration after that earned a little more confidence, and by spring 2026 I was handing it work I would never have trusted it with in December. The pace changed dramatically. In 2025 I averaged about ten merged pull requests a month. From March 2026 on, that roughly tripled to more than 30 a month, and many of them are the kind of cleanup that used to sit in the backlog for years because nobody could justify the time:

  • Overhauling the Redux generator into hooks (987 files)
  • Retiring our ever growing hodgepodge collection of styles in favor of a new comprehensive and well organized design system (837 files)
  • Replacing DynamicForm and the other config-driven screens with composed components
  • Migrating our rich content editor from EditorJS to BlockNote
  • Moving from Yarn to pnpm, upgrading to React 19, and replacing React Router 5 with version 8, with routing now owned in-house (326 files)
  • Consolidating every icon in the product onto a single line icon collection (679 files)

A grid of line icons (truck, question circle, paw print, calendar, wifi, file, search check, alarm clock) rendered at 14, 24, 32, and 40 pixels

Part of the unified icon collection that replaced several older icon libraries across 679 files.

The real lesson of the year is not the speed, though. It is where the work went. With an agent writing most of the code, my job shifted from writing code to validating it. Overall it is less work, but only if you are honest about that shift.

Before I trusted the agent with refactors at this scale, we built the safety net. On March 1, 2026 the repository had zero automated tests. Today we have 171 test files. API responses are mocked at the network layer using fixtures derived from the real backend serializers, every Storybook story renders as a smoke test, and custom lint rules encode our architecture so the tools catch violations before a person has to. Every big migration started as a written plan with phases and checkpoints, and a file of project conventions keeps the agent writing code that looks like ours.

Where that safety net has gaps, I am the safety net, and that is exactly where the temptation lives. When an agent can produce a 600-file change in an afternoon, it is very easy to skim the diff, see green checks, and merge. Do that and you get even more speed, briefly, and the product quality pays for it. Green checks only prove what your tests actually cover. We learned that the hard way in July 2026, when a security patch bumped a small cookie library to a new major version. The change passed every test, and every one of our apps still crashed the moment it loaded in production, because no test ever ran the code that starts the apps. The fix was merged within the hour.

So the investment never stops. Better test coverage, more sophisticated UI validation, and smarter automated review are the next frontier, because each one hands more of the checking back to the tools. That frees me up for the parts no tool can check: the architecture, the product decisions, and the feel. A test can tell me a screen works. It can’t tell me whether it feels right to the person using it, and that judgment is, now more than ever, what separates a great developer from a good one.

Two roads, revisited

In 2012 one of the pros I listed for taking a full-time job was “learning potential.” That might be the biggest understatement I have ever published. The road I picked led to the people I would build Moved with, to a decade of growing up alongside a codebase, and to a front-row seat as the job of a software engineer changed underneath all of us.

How this post was written

Fittingly, I wrote this post with Claude Code. It drafted from the repository itself: its full git history (more than 5,100 commits), its pull requests, and its architecture docs, so every number and milestone above comes from real code. Before it wrote a word, it used an interview technique called “grill me” to put every decision about scope, emphasis, tone, and framing to me, one question at a time. I made those calls, and I reviewed and edited the result. The agent did the heavy lifting, and I did the validating. That seems about right.