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, extractsnapshot(read path) andsend(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 π
