DEV Community

Cover image for React Recap, years later, thanks to Astro
Ingo Steinke, web developer
Ingo Steinke, web developer Subscriber

Posted on

React Recap, years later, thanks to Astro

While others are over React, like I was about five years ago, I decided to give React another chance after I discovered Astro, reviving my abondoned attempt of a reading list app side project back to learn how to use React 19 properly in 2025.

I also rediscovered Sara Vieira's Opinionated Guide to React, taking me back to 2020, when React 17 deprecated lifecycle methods in favor of hooks. While some chapters are clearly outdated, others feel surprisingly up-to date and valid today in 2025, especially her pragmatic "you might not need it" approach.

Most of my discontent with React in our project was caused by decisions that are either outdated now, or opinionated like the ESLint Airbnb preset or using CSS-in-JS with React Emotion or Styled Components. Other aspects are not React-related at all, like using ES6+ arrow functions which still felt unfamiliar for me.

After the so-called "useEffect scandal", when React 18 changed the useEffect behavior, React introduced the use() hook as an alternative in React 19 in 2024.

Compact Code Style vs. TypeScript

React hooks, JSX, spread operators and arrow functions make code more compact, maybe by sacrificing readability, but that's in the eye of the beholder. Astro and MDX files also mix syntax in a compact and implicit way, and so do the Vue and Svelte, two popular React alternatives. We get less words and more symbols, but it is a defined syntax that works. TypeScript brings back some clarity and verbosity, turning JSX into TSX and turning my coding flow into a stop-and-go-interrogation situation. I'm still optimistic that I'll eventually like TypeScript as much as I like JSDoc.

Optimistic

React 19 has many built-in optimizations making explicit suspense boundaries, useEffect, useMemo, and useCallback less critical than in earlier React versions. Passing refs as props obsoletes forwardRef higher order components in most cases. New React hooks inlcude useActionState, useForm status, and useOptimistic. React 19 also improved server-side components, but that's irrelevant when we're using Astro.

Outdated or Deprecated

  • React lifecycle methods (use hooks instead)
  • class components (write functional instead)
  • React.PropTypes (use TypeScript instead)
  • useEffect: not deprecated, but prefer to use use
  • forwardRef, useMemo, useCallback: not deprecated, but needed less

Thus, we can finally use React without most of the things that made it so awkward to write and hard to understand. Apart from hooks, React terminology includes unusual or counterintuitive terms that we just need to learn once. Some examples:

  • State means mutable (changeable) data within a component.
  • Props are read-only data passed from parent to child components.
  • A "side effect" is anything a function does apart from returning a value, more specifically called "an effect" when used with useEffect.
  • As opposed to an unidirectional top-to-bottom "drill-down", an "inverse data flow" sends data from components, like an input text field, to their parent elements or to a global context.
  • State Lifting means finding the least complex solution for sharing data between connected components like in a form's fieldset.

Opinionated

  • MobX, Redux or any other additional state management system
  • CSS-in-JS, React.Emotion, styled components
  • Controlled input components
  • eslint-config-airbnb or any other syntax preference

We can use React without JSX, TSX, or ES6+ syntax, explicitly calling h functions like some Preact code does. We can use plugins like Redux or MobX or try to rely on React's built-in state and context functionality. We can use styled components, use SCSS, "vanilla" CSS, or libraries like Tailwind and daisyUI.

Controlled Components vs. Form State Hooks

Controlled input makes React's VirtualDOM the single source of truth, often causing rerendering input fields while the user is typing, which might reduce in a sluggish UI behavior. Replacing HTML5 web forms with custom equivalents might lose built-in accessibilty if not done correctly. However, controlling input allows for enhanced validation in complex scenarios like a multi-step insurance application process.

Opting against controlled input, we can still interact with a form field's onChange event handler. The new React 19 hooks useActionState and useFormStatus significantly improve handling form fields without the traditional controlled input pattern.

Re-Rendering and Hydration vs. Web Performance

React has tried to mitigate delayed initial interactivity, maybe its most crucial pain point, with server-side rendering, learning from JAMStack framworks like Gatsby, Eleventy, or Astro.

Time to interactive is an important web performance and usability metric. If a website takes too long to load, users might give up and go away. When they can see an element but fail to interact with it, they might get frustrated.

Astro's Interactive Islands

Astro solves this problem by rendering everything 100% static by default: no JavaScript, immediate interactivity. We can mix Astro, React, Vue, or any other syntax and decide per component if and in which situation it becomes interactive, allowing for lazy loading.

Astro Islands sketch with tech stack logos and a ferryboat called useContext

In a content-rich website with an interactive search, we can render a static initial view. Good for the user, good for search engine optimization! The search bar will be the only interactive component initially, before a search result, rendered interactively by React, will replace parts of our initial static view.

React and software architecture notes in a sketchbook as elaborated in this blog post

The remote React "islands" can be connected using React context (useContext) while the static ones remain static, ignored by the interactive parts which will stay more lightweight in turn.

With Astro, we can use and reuse the same React components statically and interactively. We can write clean TypeScript code and define our content properly using Zod, while keeping it compact in our extended markdown/yaml/front matter files.

Unfortunately, this orderly approach makes every single mismatch, be it important or not, a potential point of failure at build time.

Code editors can show helpful advice or a nerdy quiz challenge like

Astro: Type
{ id: string; body?: string | undefined; collection: "book"; data: { title: string; author: string; pubYear: number; id?: string | undefined; description?: string | undefined; coverUrl?: string | undefined; ... 6 more ...; showOnHomepage?: boolean | undefined; }; rendered?: RenderedContent | undefined; filePath?: st...
is not assignable to type
{ title: string; author: string; pubYear: number; id?: string | undefined; description?: string | undefined; coverUrl?: string | undefined; coverClassName?: "bg-blue" | "bg-blue-slate" | ... 8 more ... | undefined; ... 5 more ...; showOnHomepage?: boolean | undefined; }[]

Why spoil the fun and tell the developer what doesn't match here?

Astro: "Invalid arguments passed to component."

Even worse, fatal build errors like /app/index.html[InvalidComponentArgs] Invalid arguments passed to component. without showing the root cause line number, a known bug or shortcoming of Astro that reminds me of early React versions. And that's not the only problem I encountered suggesting that Astro might still be too new and immature for extensive production use despite its 5.x release version number. If in doubt, use React for writing components, and use Astro for content and static rendering.

Astro's Dynamic Content Alternatives

Between static and interactive, we've got dynamic content which is interactive at build time, but turns static at run time in the client browser.

Astro renders content collections mostly implicitly, unless we want to sort and filter. Still, that's not a lot of logic to handle. Writing the same content to a JSON file adds an alternative static output, handled dynamically while being built.

In Astro's pages directory, file names imply routing public URLs. Unsurprisingly, pages/app.astro becomes /app in the browser. A layout wraps a grid component which renders our content collection using books components statically or interactively.

import {
  getCollection,
  type CollectionEntry,
  type InferEntrySchema,
} from 'astro:content';
const books: CollectionEntry<'book'>[] = (
  await getCollection(
    'book',
    ({ data }: { data: InferEntrySchema<'book'> }) => {
      return !!data.showOnHomepage;
    },
  )
).sort((a, b) => b.data.pubYear - a.data.pubYear);
Enter fullscreen mode Exit fullscreen mode

An alternative pages/books.json.ts rendes a books.json, and I learned that we must explicitly emit an HTTP content type at build time, which will get lost when a static JSON file is written to the dist directory, and which will be recreated by a web server based on the .json file extension. I think the point is, that Astro has an optional server mode (SSR) offering dynamic backend functionality, as opposed to the static SSG mode where the GET function feels like useless overhead.

import { getCollection, type CollectionEntry } from 'astro:content';

export async function GET() {
  const books: CollectionEntry<'book'>[] = (
    await getCollection('book')
  ).sort(
    (a, b) => 
      b.data.pubYear - a.data.pubYear,
  );

  return new Response(JSON.stringify(books), {
    headers: {
      'Content-Type': 'application/json',
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

In my simple demo app with < 200 items, it's enough to write the whole dataset into one single static JSON file that React can load and filter, but a scalable use case should avoid overfetching and request an limited search result from an actual server.

Sources and Further Reading

There are some really good tutorials for React, Storybook, Vitest and Astro, including

Conclusion

I have successfully rediscovered and modernized an abondoned side project, updated my React and TypeScript knowledge and started to learn how to use Tailwind, daisyUI, Vite, Vitest, and Playwright in a modern frontend web application with Astro, to achieve interactivity without sacrificing usability and performance!

While still in progress, my side project is continuously deployed to dev-ux-lesezeichen.de and I can continue to work and learn between paid projects.

Top comments (0)