Every complex tool used to demand cognitive rent: learn my abstractions, internalize my concepts, think my way.

Coding agents ended that. Describe what you want using concepts from your mental model and a coding agent will build it for you.

That’s a bigger shift than it sounds.

#AI #LLM #coding

Understanding agents orchestration matters because it changes what's possible.

You’ve probably been using coding agents the way most developers do: open Claude Code or Cursor, describe what you want, watch it work, course-correct when it drifts. One task at a time, one conversation at a time, you in the driver’s seat.

That works. But it makes you the bottleneck.

Over the past year, a wave of tools and projects has promised to change this. Devin, OpenHands, Claude Code’s background tasks, Cursor’s background agents, OpenAI Codex — all positioning themselves as “autonomous” developer tools. The hype makes them sound revolutionary. The reality is more interesting and more useful: they are all different forms of the same thing. They are orchestrators — systems that structure how an LLM agent is invoked, given context, checked for correctness, and recovered from failure. They differ in how they handle those concerns, and those differences explain why some work and others don’t.

Understanding orchestration matters not because you need to build your own framework, but because it changes what’s possible. Manual mode means you supervise every step. Orchestration means agents work while you sleep, in parallel, on tasks you’d never sit through one by one. It’s not a speedup — it’s a qualitative shift in what a single developer can accomplish.

This post walks through three concrete approaches to orchestration, extracts the key design decisions they make differently, and gives you a practical path to start applying these ideas.

Three ways to orchestrate a coding agent

The easiest way to understand orchestration is to watch the same task handled three different ways and see what breaks at each level.

The task: add authentication to a web app. Create the user model, add login and signup routes, wire up JWT token handling, add auth middleware, and update the tests.

Approach 1: Just tell the agent everything at once

This is what most developers do today. You open a session with your coding agent and type something like:

“Add authentication to this app. Create a User model with email and password fields, add login and signup API routes, implement JWT token generation and validation, add auth middleware that protects existing routes, and update the test suite to cover all new functionality."

The agent starts working. It creates the User model, moves to the routes, gets the JWT logic mostly right. But somewhere around the middleware step, things start to go sideways. Maybe the context window is getting full and the agent loses track of the exact middleware requirements. Maybe it decides the tests aren’t important and skips them. Maybe your session crashes — and when you restart, the agent has no memory of what it already did or what’s left.

This is context-window orchestration: the agent’s working memory is the only place the plan exists. There is no external record of what needs to happen, what already happened, or what went wrong.

The failure modes are predictable:

  • Context overflow. Complex tasks fill the context window. Auto-compaction kicks in and silently rewrites or drops parts of your original instructions. The agent continues confidently, working from a corrupted plan.
  • Selective execution. The agent decides some steps are unnecessary or “already handled” when they aren’t. You won’t notice until you look at the result.
  • Crash = total loss. If the session dies, the plan dies with it. You restart from zero, re-explain everything, and hope the agent doesn’t make different mistakes this time.
  • No checkpoint. There’s no clean boundary between “step 3 is done” and “step 4 is starting.” A failure mid-step leaves the codebase in a half-mutated state that’s hard to reason about.

This approach is fine for small, single-step tasks. It breaks down the moment complexity or duration increases.

Approach 2: Put the plan in a file and loop

This is the idea behind the “Ralph” technique, named by developer Geoffrey Huntley after using it to build an entire programming language with a coding agent.

The key insight: take the plan out of the agent’s head and put it in a file.

You create a plan.md that describes exactly what needs to be done — the same auth task, but written as a checklist with specific acceptance criteria for each step. Then you run the coding agent in a loop: each iteration, the agent reads the plan file, picks the next unchecked item, does the work, commits the result to git, and checks the box. If the agent crashes or makes a mess, you reset to the last commit and the next iteration picks up where things left off.

What changes:

  • The plan survives the agent. Context window overflow, auto-compaction, crashes — none of them touch the plan file. It persists on disk.
  • Each iteration starts clean. A fresh context window every time. No accumulated confusion, no compaction artifacts. The agent reads the current plan, sees what’s done and what isn’t, and focuses on one thing.
  • Git as a safety net. Each successful step is committed. A failed step is rolled back via git reset --hard. The codebase is never left half-mutated.
  • The human can steer between iterations. You can edit the plan file while the loop runs — reprioritise, add detail, remove steps that turned out to be unnecessary.

This is a real improvement. The plan is persistent, iterations are transactional (succeed and commit, or fail and roll back), and a crash costs you one iteration instead of everything. Huntley used this approach to run agents overnight, unsupervised, producing reviewable branches of committed work by morning.

But there’s a gap, and it’s deeper than self-evaluation. In the Ralph loop, everything — including critical checks like “did the full test suite pass?” — happens because the prompt tells the agent to do it. The agent can decide not to. Or it can run the tests, see failures, and rationalise them as unrelated to its changes. Anything you want guaranteed to happen has to be executed by the orchestrator itself, not by instructions to the model. Checks run by code always run. Checks run by prompt are suggestions. A wrapper script that runs npm test after every iteration and resets on failure catches broken tests every time; a prompt that says “make sure tests pass” may or may not.

This is why the next step isn’t just adding a reviewer — it’s moving critical logic out of the prompt and into the orchestrator itself.

Approach 3: Separate the worker from the judge

This is the conceptual step that systems like StrongDM’s Attractor take. The idea, stripped to its core: don’t let the same agent that does the work decide whether the work is correct — or whether the check runs at all.

In the auth example: after the agent implements JWT middleware, a separate verification step checks whether the middleware actually rejects unauthenticated requests, handles expired tokens correctly, and returns the right error codes. This check isn’t written by the same agent that wrote the middleware. It comes from a pre-defined set of scenarios — concrete end-to-end test cases that describe what a working auth system looks like from the user’s perspective.

The scenarios are stored separately from the codebase. The agent can’t see them, can’t modify them, and can’t game them. They function like a holdout set in machine learning: the thing being optimised never gets to read the test it’s being graded on.

What changes:

  • Verification is independent. The thing that judges “did this work?” is separate from the thing that did the work. This catches “success but wrong” — the most common and most insidious failure mode with LLM agents.
  • Quality becomes measurable. Instead of “the agent said it’s done,” you get “14 of 17 scenarios pass.” That’s a number you can track across iterations, compare across approaches, and use to decide whether to ship.
  • The human’s job shifts. You stop reviewing code line-by-line and start writing scenarios — descriptions of what correct behaviour looks like. This is a higher-leverage activity: one good scenario catches a class of bugs, while one line-by-line review catches one instance.

This is harder to set up. You need to write meaningful scenarios before the agent starts working, and the scenarios need to be specific enough to catch real problems without being so brittle that they break on irrelevant details. But when it works, it’s the difference between “I hope the agent got it right” and “I can prove the agent got it right.”

What you lose

Going from manual to autonomous isn’t free, and pretending otherwise would be dishonest.

In manual mode, you see every decision the agent makes. You catch the bad ones in real time. You bring judgment that no automated check can replicate — you know which trade-offs are acceptable, which corners can be cut, which “passing tests” are actually testing the wrong thing.

When you hand that over to an orchestrator, you lose fine-grained control and real-time judgment. You gain throughput and parallelism. The design challenge — really, the only challenge that matters — is making the automated verification good enough to compensate for the loss of human eyes on every step. The three approaches above are points on that spectrum: context-window orchestration barely tries; the loop pattern adds structural safety nets; scenario-based verification attempts to replace human judgment with something independently trustworthy.

No approach fully replaces a skilled developer paying attention. The bet is that one developer paying attention to the right things (specs, scenarios, architecture) and delegating the rest to orchestrated agents produces more than one developer writing every line by hand.

Five questions that define every orchestrator

The three approaches above look different on the surface, but they’re all making the same set of design decisions — just answering them differently. If you understand these five questions, you can evaluate any orchestration tool or technique, including ones that haven’t been built yet.

1. Where does the plan live?

This is the most basic question and the one with the biggest impact on reliability.

  • In the agent’s context window. This is approach 1. The plan exists only as tokens in a conversation. It’s convenient but fragile — compaction can rewrite it, crashes destroy it, and there’s no external record of what the agent was supposed to do.
  • In a file on disk. This is approach 2 (Ralph). The plan persists independently of the agent process. Crashes, context resets, even switching to a different agent — the plan survives. This is the single biggest reliability upgrade most developers can make.
  • In a durable workflow engine. This is where production systems are heading. Tools like Temporal record every step of a workflow so that a crash is recovered automatically — the engine replays from the last checkpoint without human intervention. OpenAI’s Codex and Replit’s Agent both run on Temporal in production for exactly this reason.

The pattern: the further the plan lives from the agent’s ephemeral context, the more resilient the system is.

2. Who writes the plan?

  • You write the full plan. You specify every step, every acceptance criterion, every constraint. The agent is a pure executor. This gives maximum control and maximum spec-satisfaction, but it’s labour-intensive. It’s what Huntley does with Ralph.
  • The agent writes the plan from a goal. You say “add auth” and the agent figures out the steps. This is flexible but risky — the agent’s plan may miss requirements you assumed were obvious.
  • Hybrid. You write the high-level requirements and acceptance criteria; the agent decomposes them into steps. This is the sweet spot for most real work. You define what and how good; the agent figures out how.

For spec-satisfaction specifically: the more precisely the human defines “done,” the more likely the output matches what was wanted. Agents are good at executing well-specified plans; they’re unreliable at inferring unstated requirements.

3. How do you know it worked?

This is the question that determines output quality more than any other.

  • The agent says so. The weakest signal. The agent declares it’s done, and you trust it. This is what context-window orchestration defaults to.
  • Tests pass. Better. A concrete check runs after each step. But if the agent wrote both the code and the tests, the check is self-referential — the agent is grading its own homework.
  • Something independent checks. Best. Scenarios written before the agent starts, stored separately, evaluated by a different process. This is the only form of verification that reliably catches “success but wrong.”

There’s a principle here worth stating explicitly: as human oversight decreases, verification must get stronger. In manual mode, you are the verification. You read every diff, you run the app, you exercise the edge cases. When you step away and let an orchestrator run, something must fill that role. If nothing does, quality drops — not immediately, but inevitably.

This is why many autonomous coding projects produce code that looks right but isn’t. The orchestrator runs, the agent reports success, tests pass — but the tests don’t cover the cases that matter, and nobody checked.

4. What happens when it breaks?

Every agent will eventually fail. The question is what the system does next.

  • Start over. Context-window orchestration’s default. The session crashes, you restart from scratch. Expensive.
  • Retry the step. The loop pattern’s answer. Roll back the last commit, try again from the last known good state. Cheap, and it works surprisingly often because LLM outputs are non-deterministic — the same prompt may succeed on the second attempt.
  • Compensate and continue. More sophisticated systems can undo the effects of a failed step (revert a database migration, cancel an API call) and try an alternative path. This is where workflow engines like Temporal shine — they track what happened and can unwind it.
  • Escalate. When automated recovery fails, surface the problem to a human with enough context to fix it. This requires good observability (see question 5).

The practical takeaway: even a simple retry-on-failure mechanism (the Ralph loop’s git reset --hard on error) is dramatically better than nothing. Most developers skip this entirely and lose hours to avoidable restarts.

5. When do you step in?

This is ultimately a trust question, and the honest answer is: it depends on how strong your answers to questions 1-4 are.

  • Every step. This is manual mode. Maximum quality, minimum throughput.
  • At defined checkpoints. The agent works autonomously through a block of steps, then pauses for your review before continuing. A good middle ground when you’re building trust in a new workflow.
  • Only at the end. The agent completes the full task; you review the final result. This works when you have strong verification (question 3) and good recovery (question 4).
  • Only when the agent is stuck. The agent runs indefinitely and flags you when it hits a problem it can’t resolve. This requires the agent to know what it doesn’t know — a hard problem.
  • Never. This is the endpoint some teams are exploring — fully autonomous with scenario-based verification as the only quality gate. It’s achievable for well-specified, well-tested domains. It’s reckless for everything else.

Most developers should start somewhere in the middle and move toward less oversight gradually as they build confidence in their verification and recovery mechanisms. Jumping straight to “never” is how projects join the 40% that get cancelled.

Applying this in practice

Understanding these concepts is necessary but not sufficient. The gap between “I understand orchestration” and “I can orchestrate effectively” is filled by practice, and the practice has to be structured or you’ll learn the wrong lessons.

Start where failure is cheap. Your first orchestrated agent run should not be on your production codebase. Pick a side project, a throwaway experiment, a greenfield feature branch. Something where a bad result costs you an afternoon, not a release.

Add one concept at a time. The most common mistake is jumping from manual mode to a full orchestration setup in one step. When it breaks — and it will — you won’t know whether the problem is your plan file format, your loop logic, your verification, or the agent itself. Instead:

  • First, try just externalising the plan. Write a plan.md, give it to the agent at the start of each session, and manually check the boxes. This alone will improve your results because it forces you to think through the steps before the agent starts.
  • Once that feels natural, add the loop. Run the agent repeatedly against the plan file, with git commits as checkpoints. See how far it gets unsupervised.
  • Once the loop is stable, add verification. Write scenarios that define “done” independently of the agent’s own judgment. See how often the agent’s “done” matches your scenarios' “done.”

Each step teaches you something that reading about it cannot. The plan file teaches you how much specification agents actually need (more than you think). The loop teaches you how agents fail (usually by confidently doing the wrong thing, not by crashing). Verification teaches you what “correct” actually means in your domain (usually more nuanced than your test suite captures).

Don’t over-invest in tooling before you understand the concepts. You don’t need LangGraph, Temporal, or a custom framework to start. A markdown file and a bash while-loop will teach you 80% of what matters. Upgrade to more sophisticated tools when you hit a specific limitation, not because the tool looks impressive.

Write better specs, not more code. The biggest leverage shift in autonomous orchestration is that the human’s job moves from writing code to writing specifications. A precise spec with clear acceptance criteria will produce better autonomous results than a vague spec with a sophisticated orchestrator. Invest your time accordingly.

Where this is heading

Three trends are converging.

Orchestration is becoming declarative. Instead of writing code that tells an agent what to do step by step, developers are writing specifications that describe the desired outcome and letting orchestration engines figure out the execution plan. This is the trajectory from bash loops to workflow graphs to spec-first tools.

Durability is becoming infrastructure. The “what happens when it crashes” problem is being absorbed by platform-level tools. Temporal and similar engines make crash recovery automatic and invisible. Within a year, “durable agent execution” will be a checkbox feature, not an engineering project.

Verification is becoming the bottleneck. As orchestration and durability become commoditised, the hard problem shifts to: how do you know the output is correct? Writing good scenarios, defining meaningful acceptance criteria, building verification that catches “success but wrong” — this is where the real skill gap will be. The developers who can specify what “correct” means precisely enough for automated checking will get dramatically more leverage from autonomous agents than those who can’t.

The common thread: the value is shifting from writing code to specifying outcomes. The agents will write the code. Your job is to define what good looks like, clearly enough that a machine can check it.

That’s not a future prediction. It’s already happening. The question is whether you start learning now, one concept at a time, or later, when the gap is wider.

Prompts Have Dependencies Too

You find a prompt on a forum thread. Four hundred upvotes. Someone says it transformed their code review workflow. You copy it, paste it into your setup, run it — and get results that are… fine. Not bad. Not what they described. You tweak some words. You add context. Still mediocre. You move on, quietly concluding that the hype was overblown. But the prompt wasn’t broken. You just ran an npm install that compiles fine and breaks at runtime. The dependency manifest was never written down.

The dependency nobody declares

Every prompt encodes a set of assumptions the author never articulated, because to them, those assumptions were invisible. They were part of the air they breathed while writing it. In code, we’ve learned — sometimes painfully — to declare our dependencies. You know what Python version is expected. You know what environment variables need to exist. You can run pip list and see the full picture. You can’t do any of that with a prompt. What you’re actually inheriting when you copy someone’s prompt is a crystallized set of decisions made in a specific context. And that context has at least three layers.

Layer 1: Context assumptions

The author wrote the prompt against the backdrop of their specific situation — their tech stack, their team’s conventions, their workflow. These shape the prompt in ways that are almost impossible to see from the outside. Take a prompt for “write a ticket.” That phrase carries a world of assumptions: what a ticket means in their process, how granular it should be, who reads it, what fields exist in their tracker. A developer working in a tight-knit four-person startup and a developer working inside an enterprise Jira workflow might both call the output “a ticket” while meaning entirely different things. The prompt is optimized for one of those worlds. When you run it in the other, it’s not wrong — it just has the wrong priors.

Layer 2: Model assumptions

Prompts are also optimized for a specific model’s behavior, including its failure modes. A lot of the instruction in high-quality prompts isn’t telling the model what to do — it’s compensating for what the model tends to do wrong. “Don’t include preamble” exists because a particular model loved preamble. “Think step by step before answering” was a meaningful intervention at a specific point in model development. Some of those instructions still load-bear; others are now inert, or actively counterproductive on a different model. The model the author used shaped the prompt as much as their intentions did. When you switch models — even to a newer version of the same one — you’re running code compiled against a different runtime. Some prompts survive the transition. Others quietly degrade.

Layer 3: Author assumptions

This one is the most overlooked: the author’s own writing style is baked into the prompt. Language models infer intent from phrasing, formality, vocabulary, and structure. A prompt written by someone who writes terse, technical prose signals something different than the same instructions written by someone who writes in full paragraphs with hedges and qualifications. The model picks up on those signals and calibrates accordingly. When you copy a prompt written in someone else’s voice, you’re running it in yours — and those two things interact in ways that are hard to predict. The gap between what the author got and what you get can be almost entirely stylistic, with nothing explicitly wrong.

The problem compounds in agent systems For individual prompts, these hidden dependencies are an annoyance. For agent systems, they’re an architectural hazard. A complex agent setup — skills, sub-agents, orchestration logic — is a stack of prompts, each carrying its own embedded assumptions, all interacting with each other. When you borrow someone’s agent setup, you’re not copying a function. You’re cloning a codebase, minus the README, minus the architecture docs, minus the incident history that shaped the current design. Each component was tuned against the others. The routing logic assumes certain output formats from upstream agents. The summarization step was calibrated to the verbosity of a specific model. Pull one piece out and drop it into a different system, and what breaks may not be obvious or immediate. This is why “it worked in their demo” is not a strong signal that it’ll work in your setup. The demo is a controlled environment. Your system is a different runtime.

Read prompts like code

The prescription isn’t “don’t share prompts.” Sharing is valuable. The prescription is to read shared prompts critically — the same way you’d read someone else’s code before integrating it. Look for load-bearing phrases. When you see an unusual instruction, ask: what is this compensating for? What problem was the author solving? If you removed this sentence, what would degrade? Treat shared prompts as illustrations of principles, not as portable solutions. A prompt that handles ambiguous user input in a specific way is showing you an approach. Extract the approach. Rewrite the implementation in your context, against your model, in your voice. The prompt is the output of a reasoning process. The reasoning is what you actually want.

What good sharing looks like The missing artifact in most prompt sharing is the README. Not a heavy document — just the few sentences that declare the dependencies. What model was this built for? What does the input look like? What conventions does it assume? What did you try that didn’t work? Some communities are starting to develop norms around this. But for the most part, prompts get shared the way code was shared before package managers: as raw files, trust the consumer to figure out the rest. A simple norm shift would help: when you share a prompt that worked well, add a short context block. “Built for Claude Sonnet, internal code review workflow, assumes tickets are already written.” Ten seconds of annotation that saves the next person an hour of debugging.

The mental model shift

The underlying reframe is this: prompts are not reusable artifacts. They are crystallized decisions, made in a specific context, by a specific person, against a specific model, for a specific purpose. That doesn’t make sharing useless — it makes understanding essential. The goal when you encounter a well-crafted prompt isn’t to copy it. It’s to understand the decisions well enough to make your own. When you do that, you stop wondering why the four-hundred-upvote prompt underdelivered in your setup. You start asking the right question instead: what problem was this person solving, and how would I solve mine?

I’ve spent the last two weeks trying spec-driven development, and my own prompts beat every “AI dev kit” I’ve tried. Those kits mostly waste my time, tokens, and context. #SpecDrivenDevelopment #LLM #DevTools #PromptEngineering #AI

“Review fatigue” is real. AI can flood teams with generated PRs, making it easier for subtle bugs to slip through. Without better review strategies, we risk trading speed for reliability. #AIcoding #DevPatterns #CodeReview

A hidden cost of React Native: your app is a patchwork of packages by different authors. Even if each works fine alone, they’ve likely never been tested together. Native dev has other challenges—but this kind of fragility isn’t one of them. #ReactNative #MobileDevelopment

First I saw a wave of posts about a0.dev and now bolt.new added #reactnative support. There’s definitely been a lot of progress in #ai lately and it might convince some companies to switch from fully native to RN to reduce costs.

I’ve been using Orion browser on iOS for about a week now. Ability to install Firefox and Chrome extensions is nice, but I found that don’t really need any new extensions. My current set of Safari extensions on iOS cover all my needs.

React Native for SwiftUI devs

After a quick review of TypeScript handbook I started reading React Native documentation. I hope my notes will be useful for other SwiftUI devs who want to try RN.

@ViewBulilder -> JSX

JSX is an embeddable XML-like syntax that allows developers to describe UI in a declarative manner, just like view builders in SwiftUI. TypeScript files that contain JSX must have .tsx extension.

To “escape back” into TypeScript world use curly braces.

export default function Hello() {
    const name = "John";
    return (
      <Text>Hello, {name}!</Text>
    );
}

You can only use curly braces in two ways inside JSX: 1 As text directly inside a JSX tag, as in the example above. 2 As attributes immediately following the = sign: src={avatar} will read the avatar variable, but src="{avatar}" will pass the string "{avatar}".

In addition to primitive types and simple expressions, you can pass objects in JSX. Objects are also denoted with curly braces, like { name: "John Johnson", posts: 5 }. So, to pass a TS object into JSX, you must wrap the object in another pair of curly braces: person={ { name: "John Johnson", posts: 5 }}. You will see this with inline CSS styles in JSX.

Conditional rendering

  1. You can use if to assign conditional content to a variable and later use it inside JSX block:
if (isLoggedIn) {
  content = <AdminPanel />;
} else {
  content = <LoginForm />;
}
return (
  <Container>
    {content}
  </Container>
);
  1. Or, you can use the conditional ? operator.
<Container>
  {isLoggedIn ? (
    <AdminPanel />
  ) : (
    <LoginForm />
  )}
</Container>

You can’t use if inside JSX tag like you do in SwiftUI’s ViewBuilder.

Responding to Events

You can respond to events by declaring event handler functions inside your components:

function MyButton() {
  function handleTap() {
    alert('You tapped me!');
  }

  return (
    <Button onPress={handleTap}>
      Tap me
    </Button>
  );
}

Notice how onClick={handleTap} has no parentheses at the end! We do not want call the event handler function, only to pass it down, otherwise function would be called on each re-render. If you want to define your event handler inline, wrap it in an anonymous function like so: <MyButton onPress={() => alert('You tapped me!')}>

@State

To add state to your component import useState from React, and then declare a state variable inside your component:

import { useState } from 'react';
function MyButton() {
  const [count, setCount] = useState(0); 
  // …
}

You’ll get two things from useState: the current state (count), and the function that lets you update it (setCount). While you can choose any names, the conventional format is [variable, setVariable]. Initially, count will be 0 since you initialized it with useState(0). Just like with @State this state is tied to a component instance, so if you declare multiple instances of the same component each of them will have its own state. Keep multiple state variables if their states are unrelated, but if two variables frequently change together, consider merging them into a single object. To update the state, simply call the setter function: function handleClick() {setCount(count + 1);}.

You can’t use regular variables to represent state for two reasons: 1. Local variables don’t persist between renders. When React renders this component a second time, it renders it from scratch - it doesn’t consider any changes to the local variables. 2. Changes to local variables won’t trigger renders. React doesn’t realise it needs to render the component again with the new data.

A Quick Word About Hooks

Functions starting with use (like useState above) are called Hooks. useState is a built-in Hook provided by React. You can find other built-in Hooks in the API reference. You can also write your own Hooks by combining the existing ones. Comparing to other functions hooks have several restrictions. Hooks can only be called at the top level of your components or your own Hooks. You can’t call Hooks inside conditions, loops, or other nested functions. Hooks are functions, but it’s helpful to think of them as unconditional declarations. You “use” React features at the top of your component similar to how you “import” modules at the top of your file. If you want to use useState in a condition or a loop, extract a new component and put it there.

@Binding

In React Native, the closest equivalent to SwiftUI’s @Binding is passing props and using callback functions, but it’s not as seamless as SwiftUI’s two-way binding: <ChildComponent count={count} onPress={handlePress} />

The information you pass down like this is called props. In the child component read props and use them to implement child functionality:

function ChildComponent({ count, onPress }) {
  return (
    <Button 
        title={`${count}`} 
        onPress={() => {
          // Must use the passed setter function
          onCountChange(prevCount => prevCount + 1)
      }}
    />
  );
}

Props can have default values: function Avatar({ user, size = 100 }).

For more complex scenarios, you might use:

  • Props drilling (passing props through multiple layers)
  • Context API (similar to SwiftUI’s @Environment, see example below)
  • Dedicated state management libraries

@Observable / @ObservableObject -> useReducer

useReducer is very different from observation mechnisms in SwiftUI and probably closer to TCA, but this is probably the closest thing React has to offer. Instead of updating properties of an object that describes state of the app you will need to write a function that take current state and an “action” that says how state should be updated. This function called reducer and there is a lot of information available on this topic, so I won’t go in to details.

The following example should be enough for general understanding of how it works.

function counterReducer(state, action) {
    switch (action.type) {
        case 'INCREMENT':
            return { count: state.count + 1 };
        case 'DECREMENT':
            return { count: state.count - 1 };
        default:
            return state;
    }
}

function CounterComponent() {
    const [state, dispatch] = useReducer(counterReducer, { count: 0 });
    return (
        <View>
            <Text>{state.count}</Text>
            <Button title="Increment" onPress={() => dispatch({ type: 'INCREMENT' })} />
        </View>
    );
}

Official documentation has a very detailed explanation with usage examples.

.onAppear & .onChange -> useEffect

Syntax and implementation differ, but the concept is similar: declaratively handle lifecycle and state-dependent operations without mixing them directly into the rendering logic.

function UserProfile() {
  const [userId, setUserId] = useState(null);
  const [userData, setUserData] = useState(null);

  // Similar to .onAppear - runs when component first mounts
  useEffect(() => {
    // Initial data fetch when component appears
    fetchUserProfile(userId);
  }, []); // Empty dependency array means "run only on mount"

  // Similar to .onChange - runs when userId changes
  useEffect(() => {
    if (userId) {
      // Fetch user data whenever userId changes
      fetchUserProfile(userId);
    }
  }, [userId]); // Dependency array specifies which values trigger the effect

  return (
    <View>
      <Text>{userData?.name}</Text>
    </View>
  );
}

Warning: The main purpose of Effects is to allow components connect to and synchronize with external systems. Don’t use Effects to orchestrate the data flow of your application. See you might not need an Effect for details.

Other considerations

Design your components as pure functions that don’t cause side effects. In React, side effects usually belong inside event handlers. Event handlers are functions that React runs when you perform some action—for example, when you click a button. Even though event handlers are defined inside your component, they don’t run during rendering! So event handlers don’t need to be pure. In React there are three kinds of inputs that you can read while rendering: props, state, and context. You should always treat these inputs as read-only.

I took extensive notes while reading the React Native and React documentation this weekind. I plan to post them after verifying that all code snippets in my notes are correct. Similar to my previous posts, I will try to explain React Native using concepts familiar to SwiftUI developers.

I found a few additional differences in the TypeScript documentation that are not mentioned in the Swift-TypeScript cheatsheet I linked in one of my previous posts:

  • The return type of a function can be inferred. If there’s no return type specified in the declaration, it doesn’t mean the function returns void.
  • Optionality in TypeScript is implemented at the property level, not in the type system. To make a property optional, add ? at the end of the property name. There are utility types in TypeScript that can change the optionality of properties, such as Partial<Type> and Required<Type>.
  • Use extends keyword to add type constraints in generics: <T1, T2 extends T1>
  • Use & to “combine” two or more types into one. It will create a new type that contains properties from all combined types.
  • Use | to create a union type that can be one of the types being “united”.
  • To make illegal states unrepresentable Swift devs often use enums with associated types. To mimic Swift’s switch on enum with associated types use union of interfaces representing associated types and add a tag with unique literal types to each interface. Then, use switch on the tag of union type, that’s enough to guarantee type safety.
  • For RawRepresentable enums it’s usually more efficient to use a union of literal types representing values because it doesn’t add extra run-time code like TS’s enum. Another alternative is const enum, but there is something in the TS documentation about potential problems with it.
  • keyof is a way to provide a type for dynamic member lookups. It’s somewhat similar to keypath in Swift.
  • typeof, when used inside conditions, allows for narrowing the type in a conditional branch.
  • “Indexed access type” allows to get type of a property by using indexed access syntax: type ContactID = Contact["id"]
  • Tuple in TS is a sort of Array type that knows exactly how many elements it contains, and exactly which types it contains at specific positions. Example: type StringNumberPair = [string, number]; describes array whose 0 index contains a string and whose 1 index contains a number.

During my journey from Swift to TypeScript, I often wonder how developers migrating in the opposite direction feel. What do they appreciate about Swift, and what aspects of TypeScript do they miss?

One of the things I like about Swift is the ability to make illegal states unrepresentable using type system. It looks like something similar is possible in TypeScript using combination of union types and literal types.

So far, Swift-Typescript Cheatsheet dabbott.github.io/webdev-pr… has been the best resource for learning TS for me.

I’ve been writing native iOS apps using Swift for the last 8 years (and years of ObjC before that). Next week I will start working on a new project with ReactNative and TypeScript. All learning materials I found are written either for web developers or people completely new to programming 🤔