React Forum
Blog & Guide Responses
Jan 20, 2023 · 04:00 PM

[Guide] Build a Job Board - implementation notes

Started by Noah Vance
guideroute sessionssession slugsstepperFieldsubmit on blur

For multi-step flows: do you prefer putting the stepper in route state (session slug + step key) or keeping it local and only writing on submit?

Read the guide

This guide models an application as a route session identified by a random slug. The URL identifies the session; route state drives the stepper; the application document stores derived review evidence (missing fields, completion percent).

How are you generating and persisting session slugs (pure client createSlug(), or server-issued)? Do you treat step transitions as route truth (route.write('step', ...)) or as local UI motion? When do you commit form state (blur vs keystroke), and how do you keep review screens deterministic? What derived evidence do you store for review screens (summary strings, missing fields, completion percent)?

Comments (10)

Back to latest
Keira Santos
Jan 20, 2023 · 04:05 PM

Why random slugs for the application route instead of /jobs/123/apply?

Is the point to avoid coupling the apply flow to the job id?

Noah Vance
Jan 20, 2023 · 04:10 PM

Exactly. The session slug is a stable handle for the application flow, which can outlive the job page shape.

It also makes 'resume application' a clean route story without encoding meaning in the URL.

Keira Santos
Jan 20, 2023 · 04:13 PM

Makes sense. I was thinking it was just a "pretty URL" thing.

Ari Novak
Jan 20, 2023 · 04:22 PM

On a larger team, the real win is that the stepper becomes route truth.

If steps are state, they're testable and snapshot-able, and any segment can self-correct the flow.

Camille Duarte
Jan 20, 2023 · 04:34 PM

Storing derived evidence for review prevents a lot of 'expensive recompute on submit'.

It also avoids weird cases where review renders different missing fields than the step validations.

Priya Liao
Jan 20, 2023 · 04:46 PM

I like that the apply doc has a stable shape (applicant, experience, answers, consents).

When the doc is stable, you can change the UI flow (file routes vs component routes) without migrating data models.

Keira Santos
Jan 20, 2023 · 04:58 PM

Submit-on-blur can surprise users if they're used to a big final submit.

We ended up using blur to commit into state (for stable review), but still gated the network mutation behind an explicit 'Submit' action.

Ari Novak
Jan 20, 2023 · 05:09 PM

In prod, the 'missing fields' list as stored derived state is huge for support/debugging.

We surfaced it as a debug panel for recruiters too. It made triage fast.

Noah Vance
Jan 20, 2023 · 05:15 PM

Yes. Derived evidence is not just perf—it’s observability.

If the route can render it, the route can explain itself.

Camille Duarte
Jan 20, 2023 · 05:28 PM

Question: do you ever allow step skipping?

We used allowSkip as a route key and then derived missingFields so review always stayed honest even if users jumped around.

Priya Liao
Jan 20, 2023 · 05:42 PM

Testing: route-flow tests feel especially natural for steppers.

Snapshots at after-blur, after-review, after-submit give you a durable audit trail of how the apply contract behaves.

Dmitri Kline
Jan 20, 2023 · 06:01 PM

For session slugs: we kept it client-generated but treated it as a contract key (and rendered it as evidence). The main goal wasn't security, it was reproducibility and support: a screenshot should include the session handle.

ts
export function createSlug() {
  const a = Math.random().toString(16).slice(2, 10);
  const b = Date.now().toString(16).slice(-6);
  return `${a}-${b}`;
}

route.write('sessionSlug', route.read('sessionSlug') ?? createSlug());
// Render it in the shell so screenshots/tests capture it:
// <section data-session={route.read('sessionSlug')} />
Salma Qureshi
Jan 20, 2023 · 06:12 PM

One thing I liked in the guide is separating step motion from step truth. We model truth as a small union and treat motion as a derived consequence, which keeps back/forward deterministic.

ts
type Step = 'profile' | 'experience' | 'questions' | 'review';
type ApplyDoc = { step: Step; completed: Step[]; missingFields: string[] };

function canEnter(step: Step, doc: ApplyDoc) {
  return step === 'review' ? doc.missingFields.length === 0 : true;
}

It feels overly formal until you debug a bug report where the user jumped around and ended up in review with half the doc missing.