Coming from React

A migration guide for React/Next.js developers moving to the GOaT stack.

If you’re coming from React or Next.js, the GOaT stack will feel unfamiliar at first — then liberating. Here’s a mental model translation.

Concept Mapping

React/Next.jsGOaT Stack
JSX componentsGo html/template partials
useState / useReducerAlpine.js x-data
useEffect + fetchHTMX hx-get or Alpine fetch()
React Router / Next.js pageshttp.NewServeMux() patterns
API RoutesGo handler functions
getServerSidePropsJust… the handler. It’s all server-side.
Context providersGo context.Context
CSS Modules / styled-componentsTailwind utility classes
Redux / ZustandAlpine stores (or just x-data)
npm run build (30s)go build (2s)
node_modules (500MB)Go binary (15MB)

What You’re Losing

Be honest about the tradeoffs:

  • Component ecosystem. No npm install shadcn-ui. You build UI with Tailwind + DaisyUI classes.
  • TypeScript. Go is typed, but templates are not. Typos in {{ .FieldName }} are runtime errors.
  • Hot module replacement. You get hot reload with tools like air, but it’s a full page refresh, not component-level.
  • Rich devtools. No React DevTools. You use browser DevTools and log/slog.
  • Client-side routing. Page transitions are full requests (boosted by HTMX for speed). No instant SPA feel.

What You’re Gaining

  • No build step. go run . and you’re live. No webpack, Vite, Turbopack.
  • No hydration bugs. There’s no hydration. The server sends HTML. The browser renders it.
  • One language. Go for everything. No TypeScript + Go + SQL + JSX mental context switching.
  • Instant deploys. go build → copy binary → done. ~2 seconds total.
  • No dependency hell. Go has ~5 dependencies. React has ~500.
  • Operational simplicity. One binary, one process, no Node.js runtime.

The Mental Shift

From “render on client” to “render on server”

In React, you fetch data on the client and render it:

// React
function GamePage({ id }) {
  const [game, setGame] = useState(null);
  useEffect(() => {
    fetch(`/api/game/${id}`).then(r => r.json()).then(setGame);
  }, [id]);

  if (!game) return <Loading />;
  return <Scoreboard game={game} />;
}

In GOaT, the server does all of this:

// Go handler
func (h *Handler) GamePage(w http.ResponseWriter, r *http.Request) {
    game, _ := h.svc.GetGame(r.Context(), gameID)
    h.tmpl.ExecuteTemplate(w, "game", game)
}

The template renders HTML. The browser displays it. No loading state. No waterfall. No layout shift.

From “component state” to “server state + islands”

Most React state management is just caching server data. With GOaT, the server is the state. You only need client state for genuinely interactive bits:

<!-- HTMX: server manages the state -->
<div hx-get="/game/{{ .ID }}/scoreboard"
     hx-trigger="sse:refresh"
     hx-swap="innerHTML">
  {{ template "scoreboard" . }}
</div>

<!-- Alpine: client manages interactive state -->
<div x-data="{ score: {{ .Score }} }">
  <button @click="score++">+1</button>
  <span x-text="score"></span>
</div>

From “API + SPA” to “HTML over the wire”

Stop thinking in API endpoints that return JSON for a client to render. Think in HTML endpoints that return ready-to-display fragments.

// Don't do this (React mindset):
func GetTodos(w http.ResponseWriter, r *http.Request) {
    todos, _ := svc.ListTodos(r.Context())
    json.NewEncoder(w).Encode(todos)
}

// Do this (GOaT mindset):
func GetTodos(w http.ResponseWriter, r *http.Request) {
    todos, _ := svc.ListTodos(r.Context())
    tmpl.ExecuteTemplate(w, "todo-list", todos)
}

The exception: Alpine.js components that need JSON APIs for optimistic updates. That’s fine — use both.

Migration Strategy

  1. Start with a new page, not a rewrite. Add a GOaT-powered page alongside your existing React app.
  2. Move CRUD pages first. Lists, forms, detail views — these are 80% of most apps and trivial in GOaT.
  3. Keep React for genuinely complex UI. If you have a drag-and-drop builder or spreadsheet, keep it.
  4. Use Alpine.js as the bridge. If a page needs more interactivity than HTMX provides, Alpine.js is closer to React’s mental model than raw DOM manipulation.

The Honest Truth

The GOaT stack isn’t better than React for everything. But for the vast majority of web apps — forms, dashboards, CRUD, content sites — it’s simpler, faster to build, and dramatically easier to operate. The question isn’t “can GOaT do what React does?” It’s “does my app actually need what React provides?”

For most apps, the answer is no. 🐐