Plakar UI #2: React

Julien Castets 1328 words May 25, 2026 7 min read
Plakar UI #2: React
Julien Castets
Julien Castets Software engineer @ Plakar Korp

When I started building websites in the early 2000s, the model was simple: HTML for the structure, CSS for the style, PHP for the backend, and a sprinkle of JavaScript for interactivity. Pre-Web 2.0, JavaScript wasn’t used that much, and that was fine.

The prevailing wisdom back then was clean separation of concerns: your HTML would only contain semantics, and your CSS would define how things looked. In theory, you could swap out style.css for a completely different stylesheet and the site would look entirely different while the HTML stayed untouched. Elegant idea.

In practice, you spent hours trying to find the right name for your classes and ended up with something like .alert-container-inner-title-too-long-classname-who-reads-this-anyway. And of course the CSS was deeply coupled to the HTML anyway: your .alert-container selector assumed a very specific DOM structure, so changing one silently broke the other. The “separation” was an illusion. The files were separate, but the concerns weren’t.

When I discovered React a few years ago, it was a relief. Yes, you still suffer from the JavaScript ecosystem (ETOOMANY dependencies). But React offers a genuinely different way to think about UI, one that maps surprisingly well to how backend developers already think.

We use React specifically rather than Vue or Svelte — both are solid alternatives — mostly because it’s what the team already knew, and the ecosystem around it (TanStack Query, TanStack Router, React Aria) is well-matched to what we needed. There’s no deep technical reason React is better for our use case. Most of what we cover in this series would look similar in either framework.

A React component is a function. It takes inputs (props from its parent, and state it manages itself) and returns a description of what the UI should look like. Same inputs, same output. When the inputs change, React re-runs the function and updates the DOM.

UI = f(state, props)

If you’ve written HTTP handlers, you already know this pattern. A handler takes a request and returns a response; given the same request and the same application state, it always returns the same thing. A React component is the same idea, applied to the DOM.

In practice, this means you stop thinking “when the user clicks this button, disable it and show a spinner” and start thinking “when isLoading is true, the button looks like this.” You describe the relationship between state and UI, and React handles the rest.

function SubmitButton({ isLoading, onClick }) {
  return (
    <button disabled={isLoading} onClick={onClick}>
      {isLoading ? <Spinner /> : "Submit"}
    </button>
  );
}

When isLoading changes, React re-renders this component and figures out what changed. You don’t touch the DOM directly. You just describe what the button looks like in each state.

What is a component?

#

A component is a function that returns a description of some UI. That description is written in JSX — HTML-ish syntax inside JavaScript.

JSX is like your first glass of whisky. At first you look at it and think “beuuurkkk, what is this, why would anyone mix HTML with JavaScript.” Then you get used to it, and after a while you can’t imagine going back. Here’s a real component from our packages/ui library, the Alert:

function Alert({ variant = "default", title, children, actions, size = "md" }) {
  const icon = {
    default: "info-circle",
    success: "success-circle",
    warning: "warning-triangle",
    error: "error-circle",
  }[variant];

  return (
    <div className={clsx("border-1 flex flex-wrap items-center ...", variantClasses[variant])}>
      <Icon type={icon} size="md" />
      <Typography weight="medium">{title}</Typography>
      <div>{children}</div>
      {actions && <div>{actions}</div>}
    </div>
  );
}
The Alert component in its different variants

A few things to notice. The structure, the logic, and the styles all live together. There’s no separate .css file that might drift out of sync. The icon is derived from the variant — no need to pass it manually, the component figures it out. And if you change something here, every place using Alert gets the update.

Under the hood, JSX compiles down to regular JavaScript function calls. <Alert variant="error" title="Connection failed" /> becomes something like Alert({ variant: "error", title: "Connection failed" }). No magic, just syntax.

This is what the HTML/CSS separation promise could never actually deliver. The structure and the presentation were always coupled — JSX just makes that honest.

You use a component by passing it props — function arguments, essentially. The caller decides what values to pass; the component decides how to render them:

<Alert variant="error" title="Connection failed">
  Could not reach the Plakar API. Check your network settings.
</Alert>

<Alert variant="warning" title="Storage almost full" size="sm">
  You have used 90% of your storage quota.
</Alert>

You can pass any JavaScript value as a prop: strings, numbers, arrays, objects, other components, even functions. Passing a function is how you handle events — a button takes an onClick prop, and the parent decides what happens when it’s clicked. If you’re used to Go, think of props as a struct passed to a function.

State

#

Props come from outside the component. State lives inside it. State is data that belongs to a component and can change over time.

import { useState } from "react";

function CollapsiblePanel({ title, children }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? "Hide" : "Show"} {title}
      </button>
      {isOpen && <div className="mt-2">{children}</div>}
    </div>
  );
}

useState(false) declares a piece of state initialized to false. It returns the current value and a setter. Call the setter with a new value, React re-renders the component with that new state. {isOpen && <div>...</div>} is just a JavaScript expression: render the div if isOpen is true, render nothing otherwise.

The component doesn’t touch the DOM directly. It just describes what it looks like given the current isOpen, and React takes care of the rest.

Composition

#

Components can render other components. This is how you build complex UIs from simple pieces — and where the component model really pays off.

Here’s how our Modal component is used in the codebase:

<Modal>
  <Modal.Dialog>
    <Modal.Header
      title="Delete backup"
      description="This action cannot be undone."
    />
    <Modal.Content>
      Are you sure you want to delete this backup?
    </Modal.Content>
    <Modal.Footer>
      <Button variant="secondary">Cancel</Button>
      <Button variant="error">Delete</Button>
    </Modal.Footer>
  </Modal.Dialog>
</Modal>

Modal, Modal.Dialog, Modal.Header, Modal.Content, Modal.Footer — each is a separate component that owns its own piece of the layout. The header handles the title, description, and close button. The footer handles the action area. None of them need to know about each other or about what’s above them in the tree.

This is how every page in Plakar UI is built: a tree of components, each responsible for its own piece, none aware of its ancestors. When we fix a bug in Modal.Header, every modal in both apps gets the fix. When we add an animation to Modal, it applies everywhere.

Compare this to the old model: a shared CSS class, a convention, and three copies of the same HTML structure that you hoped stayed in sync. The component is the actual unit of reuse, not an approximation of one.

One thing worth noting: even something as “simple” as a button is more complex than it looks. Our Button component has 8 variants (primary, secondary, tertiary, error, and a few more), two sizes, optional left and right icons, a loading state with an integrated spinner, a tooltip, and it can render as either a <button> or an <a> link depending on whether you pass an href. That’s before you factor in keyboard navigation and accessibility. The component handles all of it. The caller just writes <Button variant="error" isPending={isDeleting}>Delete</Button> and moves on.

This is the thing I got wrong about frontend development for years. I thought the complexity was in making things look nice. The actual complexity is managing state, composing behaviours, and keeping ten different things in sync on the same page — forms, API calls, loading states, error states, real-time updates. It’s no longer “one page = one action.” Modern UIs are closer to distributed systems than to static documents.


Next: TypeScript. Because a component that accepts any props with no validation is a bug waiting to happen.