β€’

tech

Components take care of themselves

Most frontend bugs are hidden behind confused business logic and race conditions inside components. Easy solution: make all components dumb, no business logic inside them, and let AI take care of them.


Sandro Maglione

Sandro Maglione

Software

Key rule for React frontend: keep business logic away from components πŸ™Œ

Components only read/display data, and send events 🀝

If a component is "dumb", you just need to review the rendered UI, not the implementation (most of the times) πŸ’πŸΌβ€β™‚οΈ

Here is how I make sure that components stay dumb, and AI follows this rule πŸ‘‡


No logic in components

When I say "no business logic", I mean it fully:

  • No inline functions
  • No useEffect
  • No error handling

It's worth repeating again (and again):

Components only read/display data, and send events πŸ’‘

Since I use xstate (everywhere) the pattern goes as follows:

A single useMachine, extract snapshot (read path) and send (write/event path).

Nothing else, that's all the client can use πŸ’πŸΌβ€β™‚οΈ

You may have other hooks, either React-native like useRef, or custom providers or libraries.

But those will need to delegate to events as well. Example:

export default function SubscriptionScreen() {
  // Other hooks
  const { getToken } = useAuth();

  // Machine machine
  const [snapshot, send] = useMachine(machine, {
    input: {
      getToken, // πŸ‘ˆ Logic inside machine
    },
  });

  // Extract data
  const detailsOpen = snapshot.matches({ Purchasing: "Open" });
  const completionSource = snapshot.context.completionSource;
  const purchaseActorRef = snapshot.children.purchase;

  // ...

Help AI understand

AI doesn't normally follow the above principle, at all πŸ™Œ

It's too easy to mess with components in React, simple instructions are not enough.

Welcome custom linting rules (strict!) πŸ”’

You need to get extreme, with strict rules. Like, strict. The AI will have no way to mess it up (eventually πŸ’πŸΌβ€β™‚οΈ).

No useEffect, and no useState

It's too easy for AI to jump for the easy solution, and also the most broken one. So I have an explicit no-react-state-hooks that bans useEffect and also useState.

Both of those are handled better by state machines and actors. The AI will be reminded of that during linting 🀝

No multiple useMachine

If the AI is required to use machines, its tendency will be to create many of them, and wire callbacks and imperative calls between those.

This can be fixed by requiring a single useMachine/useActor/useActorRef inside a component, with another simple reminder:

This component uses multiple @xstate/react actor hooks. Compose machines with actors instead.

Declarative XState and Effect

Actors in xstate are responsible for "state management" directly.

But the "secret" that wires the business logic all together is called effect πŸͺ„

xstate acts an an "orchestrator" of actors (state, context, events). The actual "logic" is all written with effect (error handling, dependency injection and more).

Example:

export const externalLinkMachine = setup({
  types: {
    events: {} as { type: "link.open"; url: string },
    context: {} as {
      targetUrl: string | null;
      error: string | null;
    },
  },
  actors: {
    openLink: fromPromise(({ input }: { input: { url: string } }) =>
      RuntimeClient.runPromise(
        Effect.gen(function* () {
          const externalLinks = yield* ExternalLinks;
          return yield* externalLinks.openUrl(input.url);
        })
      )
    ),
  },
}).createMachine({
  id: "externalLink",
  context: {
    targetUrl: null,
    error: null,
  },
  initial: "Idle",
  states: {
    Idle: {
      on: {
        "link.open": {
          target: "Opening",
          actions: assign(({ event }) => ({
            targetUrl: event.url,
            error: null,
          })),
        },
      },
    },
    Opening: {
      on: {
        "link.open": {
          target: "Opening",
          reenter: true,
          actions: assign(({ event }) => ({
            targetUrl: event.url,
            error: null,
          })),
        },
      },
      invoke: {
        src: "openLink",
        input: ({ context }) => ({ url: context.targetUrl ?? "" }),
        onDone: {
          target: "Idle",
          actions: assign({
            targetUrl: null,
            error: null,
          }),
        },
        onError: {
          target: "Idle",
          actions: assign(({ event }) => ({
            targetUrl: null,
            error: errorMessage(event.error),
          })),
        },
      },
    },
  },
});

While the component sees only useMachine, in reality is effect that implements the logic.

In this example, the machines handles the states/context (available to the component), but then it's the ExternalLinks effect service that does the work.


I have many more custom linting rules to tame xstate and effect implementations as well.

The loop is so tight, that the AI "one prompts" most implementations 🀝

Most often is not a "one shot", but the linting loop will eventually guide the AI to the expected result, in one prompt.


My workflow is getting more and more strict: I prompt, let AI write, check the result (always, all the code), implement other custom linting rules to tight the loop even more, and repeat.

Success guaranteed β˜‘οΈ

See you next πŸ‘‹

Start here.

Every week I dive headfirst into a topic, uncovering every hidden nook and shadow, to deliver you the most interesting insights

Not convinced? Well, let me tell you more about it