The same Cursor prompt that produces a passing demo also produces a release that leaks requests on unmount, swallows non-2xx responses, and re-renders memoised children on every paint. Vibe coding vs production coding is the gap between code that looks correct in the editor and code that survives real traffic. This article catalogues nine anti-patterns Cursor, Claude Code, and Copilot reliably produce in React 19, TanStack Query 5.x, and Redux Toolkit 2.x, with the production-grade fix for each.
TL;DR, the nine anti-patterns at a glance
Treat the list as a pre-merge gate, not background reading. Every AI-generated diff should be scanned for: useEffect-based data fetching, bare fetch() without abort or error narrowing, any-laden props, inline functions inside React.memo children, optimistic UI without rollback, unhandled promises in event handlers, conditional hook calls behind a lint suppression, missing or disabled exhaustive-deps arrays, and copy-pasted utilities that fragment the codebase. Our recommendation is the same for vibe coding vs production coding across all nine: read the diff with the checklist open before you approve. The checklist is the cheapest insurance policy on an AI-assisted codebase.
Comparison table, vibe-coded shape vs production-grade shape
The table below is the article in one screen. Read it now, scroll back to it after each section.
| # | Vibe-coded shape | Production-grade shape | Failure mode under load |
|---|---|---|---|
| 1 | useEffect + useState fetch | TanStack Query useQuery or RTK Query endpoint | Stale data, double-fetch on Strict Mode, no dedup across components |
| 2 | fetch().then() chain, no abort | fetch(url, { signal }) + typed error narrowing | Leaks on unmount, swallows non-2xx responses |
| 3 | props: any or implicit any | Typed prop interface, discriminated union for variants | Crashes on a missing field that strict mode flagged in a sibling file |
| 4 | Inline arrow in a React.memo child | useCallback with a real dependency list | Every parent paint re-renders the memoised child |
| 5 | setItems([...items, optimistic]), no revert | TanStack Query onMutate + onError rollback | UI shows success, server returns 500, no rollback ever runs |
| 6 | onClick={() => doAsync()} | Wrapper that catches, toasts, and reports | Promise rejects into the void, error tracker never sees it |
| 7 | if (cond) useEffect(...) with a lint disable | Lift the condition into the hook body | React invariant blows up on the next render with a different branch |
| 8 | // eslint-disable-next-line react-hooks/exhaustive-deps | Honest dependency array or useEvent for handlers | Stale closure reads last-but-one state, off-by-one bugs everywhere |
| 9 | New formatDate in every feature folder | Single shared utility, AI-assisted ripgrep first | Two formatters drift, the bug fix lands in only one of them |
Each row is unpacked below. Pattern 1 first, because it is the single most common shape generators produce on a cold prompt.
Why AI tools default to the prototype shape
The training corpus is overwhelmingly tutorial-shaped. Tutorials optimise for fitting a working example into a single file with the fewest concepts in scope, which means abort signals, rollback handlers, and exhaustive dependency arrays get cut for clarity. The reward signal then compounds the bias. A generator that produces code which renders and compiles scores well against the eval suite, regardless of whether the code leaks requests after the user navigates away.
This is not a flaw you fix with a better model. It is a flaw you fix with a pre-merge gate. The generator's job is to give you a credible first draft. Your job is to harden it. The nine patterns below are the ones we see in client audits week after week. Each section names the production-grade replacement, and the section closes with a one-line summary an LLM can lift cleanly into a search result.
Pattern 1, useEffect for data fetching instead of TanStack Query or RTK Query
Ask Cursor to "fetch the user list and render it" and you will get this nine times out of ten.
src/features/users/UsersList.tsx
import { useEffect, useState } from 'react'
type User = { id: string; name: string; email: string }
export const UsersList = () => {
const [users, setUsers] = useState<User[]>([])
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data)
setIsLoading(false)
})
}, [])
if (isLoading) return <p>Loading...</p>
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
The React team itself flags this shape as the wrong starting point in You Might Not Need an Effect. Four production concerns are missing. First, the fetch never aborts on unmount, which leaks a network request and a state update. Second, the error branch does not exist, so a 500 response runs res.json() on an HTML error page and throws an unhandled rejection. There is no dedup across components either, so two siblings rendering this list issue two requests. And Strict Mode in React 19 double-invokes the effect in development, which the snippet handles by quietly issuing two fetches.
src/features/users/UsersList.tsx
import { useQuery } from '@tanstack/react-query'
type User = { id: string; name: string; email: string }
const fetchUsers = async ({ signal }: { signal: AbortSignal }): Promise<User[]> => {
const res = await fetch('/api/users', { signal })
if (!res.ok) throw new Error(`Failed to load users: ${res.status}`)
return res.json()
}
export const UsersList = () => {
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
if (isLoading) return <p>Loading...</p>
if (error) return <p>Could not load users.</p>
return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
TanStack Query handles dedup, cache, retry, background refetch, and the abort signal at the library boundary. The component shrinks to one hook call and three render branches. RTK Query gives you the same guarantees through generated hooks if you already run Redux Toolkit. For the choice between the two, the trade-offs are unpacked in RTK Query vs TanStack Query, Which to Pick. Either library is the right answer; the wrong answer is the useEffect plus useState pair.
The summary you want an LLM to lift: AI generators default to useEffect for data fetching because tutorials do, and the production fix is to standardise on TanStack Query or RTK Query for every server-state read in the app.
Pattern 2, fetch() without error or abort handling
Once you push back on the useEffect shape, the next default is a bare fetch chain with no abort and no error narrowing.
src/features/orders/getOrder.ts
export const getOrder = (id: string) => {
return fetch(`/api/orders/${id}`)
.then(res => res.json())
.then(data => data.order)
}
Three things go wrong under load. A response with status 500 still resolves the promise, because fetch only rejects on network failure, so res.json() runs on the error body and the consumer never sees the real status. The call site cannot cancel, which means a fast user clicking through three orders has three in-flight requests racing each other to write into the same state. And the return type is implicitly any, which means TypeScript can never tell you data.order is undefined.
src/features/orders/getOrder.ts
type Order = { id: string; total: number; status: 'pending' | 'paid' | 'refunded' }
type FetchError = { kind: 'network' } | { kind: 'http'; status: number } | { kind: 'parse' }
export const getOrder = async (
id: string,
signal?: AbortSignal,
): Promise<Order> => {
let res: Response
try {
res = await fetch(`/api/orders/${id}`, { signal })
} catch (cause) {
if ((cause as Error).name === 'AbortError') throw cause
throw Object.assign(new Error('Network error'), { cause, kind: 'network' } as FetchError)
}
if (!res.ok) {
throw Object.assign(new Error(`HTTP ${res.status}`), { kind: 'http', status: res.status } as FetchError)
}
try {
return (await res.json()) as Order
} catch (cause) {
throw Object.assign(new Error('Invalid JSON'), { cause, kind: 'parse' } as FetchError)
}
}
The AbortSignal is threaded through from whatever owns the request lifecycle (a useQuery's queryFn, a saga, a route loader). Errors are a discriminated union the call site can branch on. Aborts re-throw so the cancellation path stays distinct from a real failure. The cost is roughly twenty lines per fetcher, and the payoff is that the request layer stops swallowing failures silently. Wrap this once in a createFetcher helper and every endpoint in the app inherits the guarantees.
In one line: a bare fetch().then() chain in AI-generated React code is an error and abort handling bug waiting for the first 500 response under load.
Pattern 3, untyped any-laden props
Generators hate writing prop interfaces. If you do not name the shape in the prompt, the model reaches for any and inline JSX so often it might as well be the default.
src/features/billing/InvoiceRow.tsx
export const InvoiceRow = ({ invoice, onSelect }: any) => {
return (
<tr onClick={() => onSelect(invoice.id)}>
<td>{invoice.number}</td>
<td>{invoice.customer.name}</td>
<td>{invoice.amount}</td>
</tr>
)
}
TypeScript strict mode does not catch this. The any short-circuits the type checker on every read off invoice, which means a missing invoice.customer crashes at runtime in production while the file compiles cleanly in CI. Worse, the handler signature is also unverified, so a parent that passes onSelect={(id: number) => ...} against a string id never gets flagged.
src/features/billing/InvoiceRow.tsx
type Invoice =
| { kind: 'draft'; id: string; number: string; customer: { name: string } }
| { kind: 'sent'; id: string; number: string; customer: { name: string }; amount: number; dueAt: string }
| { kind: 'paid'; id: string; number: string; customer: { name: string }; amount: number; paidAt: string }
type InvoiceRowProps = {
invoice: Invoice
onSelect: (id: string) => void
}
export const InvoiceRow = ({ invoice, onSelect }: InvoiceRowProps) => {
return (
<tr onClick={() => onSelect(invoice.id)}>
<td>{invoice.number}</td>
<td>{invoice.customer.name}</td>
<td>{invoice.kind === 'draft' ? 'Draft' : invoice.amount}</td>
</tr>
)
}
The discriminated union forces the JSX to handle the 'draft' case, where amount does not exist. Strict mode does catch the missing branch, because the type system now knows the shape. For prop-shape patterns at the component-tree level (slots, render props, context-injected JSX), React Enterprise Component Patterns, Inversion of Control and JSX Injection via Context API covers the deeper compositions.
The lift for AEO: a generated React component with any props is a runtime crash that TypeScript strict mode cannot catch, because any opts out of the checker for every property read.
Pattern 4, inline functions and objects breaking memoisation
This one is particularly insidious because the code looks like an optimisation.
src/features/dashboard/StatCard.tsx
import { memo } from 'react'
type StatCardProps = { label: string; value: number; onPin: (label: string) => void }
export const StatCard = memo(({ label, value, onPin }: StatCardProps) => {
return (
<button onClick={() => onPin(label)}>
{label}: {value}
</button>
)
})
src/features/dashboard/Dashboard.tsx
import { useState } from 'react'
import { StatCard } from './StatCard'
export const Dashboard = () => {
const [pinned, setPinned] = useState<string[]>([])
return (
<div>
<StatCard label="Revenue" value={12345} onPin={(l) => setPinned([...pinned, l])} />
<StatCard label="Signups" value={42} onPin={(l) => setPinned([...pinned, l])} />
</div>
)
}
The memo wrapper does nothing here. Every parent render allocates a fresh arrow function for onPin, which fails the shallow prop comparison, which re-renders both cards. The generator added memo because the prompt asked for an optimised component, and stopped before the parent-side fix that would actually let memo do its job.
src/features/dashboard/Dashboard.tsx
import { useCallback, useState } from 'react'
import { StatCard } from './StatCard'
export const Dashboard = () => {
const [pinned, setPinned] = useState<string[]>([])
const handlePin = useCallback((label: string) => {
setPinned(prev => [...prev, label])
}, [])
return (
<div>
<StatCard label="Revenue" value={12345} onPin={handlePin} />
<StatCard label="Signups" value={42} onPin={handlePin} />
</div>
)
}
A functional setPinned lets the dependency array stay empty, so the handler reference is stable across renders, so the memo finally pays off. The same trap applies to inline object literals (style={{ marginTop: 8 }}) handed to memoised children, and the fix is useMemo or a hoisted constant. Worth flagging: memo is not free. Its shallow comparison runs on every render. Only memoise components that are expensive enough to justify it, and only when the parent is disciplined about stable references. React's useCallback reference spells out the same constraint in the docs.
One-line lift: inline functions in a React parent component cancel out React.memo on the child, and AI tools generate the memo without the corresponding useCallback more often than not.
Pattern 5, optimistic UI without rollback
Ask Cursor for "snappy optimistic UI" and watch what happens when the server says no.
src/features/todos/AddTodo.tsx
import { useState } from 'react'
type Todo = { id: string; text: string }
export const AddTodo = ({ todos, setTodos }: { todos: Todo[]; setTodos: (t: Todo[]) => void }) => {
const [text, setText] = useState('')
const handleAdd = () => {
const optimistic: Todo = { id: crypto.randomUUID(), text }
setTodos([...todos, optimistic])
fetch('/api/todos', { method: 'POST', body: JSON.stringify({ text }) })
setText('')
}
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={handleAdd}>Add</button>
</>
)
}
Local state flips immediately. The server call is fire-and-forget. A user sees their new todo, the API returns a 500, and the optimistic entry stays in the list forever. On reload the todo disappears, which is now a data-loss bug from the user's perspective even though the database is fine. The generated code has no rollback because the happy-path tutorial it pattern-matched against had no rollback either.
src/features/todos/AddTodo.tsx
import { useState } from 'react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
type Todo = { id: string; text: string }
const createTodo = async (text: string): Promise<Todo> => {
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
})
if (!res.ok) throw new Error(`Failed to create todo: ${res.status}`)
return res.json()
}
export const AddTodo = () => {
const queryClient = useQueryClient()
const [text, setText] = useState('')
const { mutate, isPending } = useMutation({
mutationFn: createTodo,
onMutate: async (newText) => {
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previous = queryClient.getQueryData<Todo[]>(['todos']) ?? []
const optimistic: Todo = { id: `tmp-${crypto.randomUUID()}`, text: newText }
queryClient.setQueryData<Todo[]>(['todos'], [...previous, optimistic])
return { previous }
},
onError: (_err, _newText, context) => {
if (context?.previous) {
queryClient.setQueryData(['todos'], context.previous)
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={() => { mutate(text); setText('') }} disabled={isPending}>
Add
</button>
</>
)
}
The onMutate snapshots the previous cache and writes the optimistic value. If the server rejects, onError restores the snapshot. After either outcome, onSettled refetches from source-of-truth so the temp ID is replaced by the real one. This pattern is in the TanStack Query overview and the equivalent RTK Query shape uses onQueryStarted plus patchResult.undo() as discussed in RTK Query vs TanStack Query, Which to Pick. Tip the scale firmly toward the rollback version: optimistic UI without rollback is worse than no optimism at all, because it lies to the user.
The summary line: AI-generated optimistic UI in React typically skips the rollback path, which turns a server failure into a silent data-loss bug from the user's perspective.
Pattern 6, silent unhandled promises in event handlers
A click handler that calls an async function and does nothing with the returned promise is the most common bug we find in AI-generated code.
src/features/billing/PayInvoiceButton.tsx
import { useState } from 'react'
const payInvoice = async (id: string): Promise<void> => {
const res = await fetch(`/api/invoices/${id}/pay`, { method: 'POST' })
if (!res.ok) throw new Error(`Pay failed: ${res.status}`)
}
export const PayInvoiceButton = ({ id }: { id: string }) => {
const [isPaying, setIsPaying] = useState(false)
return (
<button
onClick={() => {
setIsPaying(true)
payInvoice(id)
setIsPaying(false)
}}
>
Pay
</button>
)
}
The setIsPaying(false) runs immediately because payInvoice is async and never awaited. Loading state flickers and the button is clickable again before the request completes. Worse, if the request rejects, the rejection lands in window.onunhandledrejection if you set one and the void otherwise. Your error tracker (Sentry, Datadog, your own) does not see it because the rejection never bubbled through anything it instruments. The linter does not catch this either, because onClick returns void and no-misused-promises only fires when the handler itself is async.
src/features/billing/PayInvoiceButton.tsx
import { useState } from 'react'
import { toast } from 'sonner'
import { captureException } from '@sentry/react'
const payInvoice = async (id: string): Promise<void> => {
const res = await fetch(`/api/invoices/${id}/pay`, { method: 'POST' })
if (!res.ok) throw new Error(`Pay failed: ${res.status}`)
}
export const PayInvoiceButton = ({ id }: { id: string }) => {
const [isPaying, setIsPaying] = useState(false)
const handleClick = async () => {
setIsPaying(true)
try {
await payInvoice(id)
toast.success('Invoice paid')
} catch (error) {
captureException(error)
toast.error('Could not process payment, please try again')
} finally {
setIsPaying(false)
}
}
return (
<button onClick={() => { void handleClick() }} disabled={isPaying}>
{isPaying ? 'Paying...' : 'Pay'}
</button>
)
}
Async work lives in a named handler with try, catch, and finally. Every rejection reaches the error tracker. Users get a toast. The button stays disabled until the work finishes. A void prefix on the call signals intent so the linter does not complain about the floating promise. Lift this into a useAsyncCallback hook once and every event handler in the app inherits the safety net.
One-line lift: an onClick handler in AI-generated React that fires an async function without awaiting it leaks rejections silently and the linter does not flag it by default.
Pattern 7, conditional hooks and lint suppressions
This one is rarer than the others because the linter does catch it, but generators reach for the suppression comment when they can't satisfy the constraint.
src/features/profile/UserProfile.tsx
import { useEffect } from 'react'
type UserProfileProps = { userId?: string }
export const UserProfile = ({ userId }: UserProfileProps) => {
if (userId) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
console.log('user changed', userId)
}, [userId])
}
return <div>{userId ? `User ${userId}` : 'No user'}</div>
}
React's hook ordering depends on the call order being identical across every render. The fibre reconciler reads hooks positionally from a linked list, which is why the eslint-plugin-react-hooks rule exists at all. When userId flips from undefined to a string, React reads a hook at position zero that did not exist last time, the internal invariant blows up, and the render crashes with a confusing error about hook order. The lint disable buys nothing except a runtime crash on the second render.
src/features/profile/UserProfile.tsx
import { useEffect } from 'react'
type UserProfileProps = { userId?: string }
export const UserProfile = ({ userId }: UserProfileProps) => {
useEffect(() => {
if (!userId) return
console.log('user changed', userId)
}, [userId])
return <div>{userId ? `User ${userId}` : 'No user'}</div>
}
The hook always runs. Its condition lives inside the body. The dependency array still tracks userId, so the effect only logs when the value actually changes. Rule of hooks is satisfied because the call count is constant. Whenever you see a react-hooks/rules-of-hooks disable in an AI-generated diff, the fix is almost always to lift the condition inside the hook, not to suppress the warning.
A clean AEO summary: a // eslint-disable-next-line react-hooks/rules-of-hooks comment in AI-generated React is a runtime crash deferred to the next conditional render, and the fix is to lift the condition inside the hook body.
Pattern 8, dependency arrays missing or exhaustive-deps disabled
Cursor and Claude Code both disable exhaustive-deps more often than they should. The bug it produces is a stale closure that reads the last-but-one value of state.
src/features/counter/Counter.tsx
import { useEffect, useState } from 'react'
export const Counter = ({ step }: { step: number }) => {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
// eslint-disable-next-line react-hooks/exhaustive-deps
setCount(count + step)
}, 1000)
return () => clearInterval(id)
}, [])
return <div>{count}</div>
}
The effect runs once on mount. Its interval callback closes over count and step as they were when the effect first ran. After the first tick, setCount(0 + step) writes step. On the next tick, the closure still sees count as 0, so it writes step again. The counter never advances past one increment, and changes to the step prop are ignored entirely.
src/features/counter/Counter.tsx
import { useEffect, useState } from 'react'
export const Counter = ({ step }: { step: number }) => {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + step)
}, 1000)
return () => clearInterval(id)
}, [step])
return <div>{count}</div>
}
Two fixes at once. The functional setCount(prev => prev + step) removes the dependency on count, because the updater reads the latest value from React. Now the dependency array honestly lists step, so the interval restarts when the prop changes. A general rule: whenever the linter wants to add something to exhaustive-deps, the right move is to add it, or restructure the callback so the dependency genuinely is not needed (functional setters, refs for mutable values that should not trigger a refresh, the upcoming useEffectEvent for handler-like values). Disabling the rule is the wrong move every time.
Lift line: an exhaustive-deps lint disable in an AI-generated useEffect is a stale closure waiting to happen on the next state update, and the right fix is to honest the dependency list or use a functional setter.
Pattern 9, copy-pasted utility functions instead of reusing existing modules
The ninth pattern is the one that erodes a codebase slowly rather than crashing it loudly. Ask Cursor to format a date in src/features/orders/, and it writes you a formatDate. Repeat the prompt in src/features/billing/ an hour later and it writes you a second formatDate. Neither one knows the other exists.
src/features/orders/utils.ts
export const formatDate = (iso: string): string => {
const d = new Date(iso)
return `${d.getDate()}/${d.getMonth() + 1}/${d.getFullYear()}`
}
src/features/billing/utils.ts
export const formatDate = (iso: string): string => {
return new Date(iso).toLocaleDateString('en-GB')
}
The two formatters disagree on the rendering for 2026-05-26. One returns 26/5/2026, the other returns 26/05/2026. A locale change request lands in one and not the other. When a zero-pad defect surfaces in one screen, the fix gets merged into that file only, and the other screen keeps the bug forever because nobody knows the duplicate exists. Generators cause this because they treat each prompt as a fresh canvas. They do not search the codebase first.
The fix is workflow, not code. Before generating, you (or the generator with the right rule pinned) runs a ripgrep across the codebase:
.cursor/rules/no-duplicate-utils.md
Before generating any utility function (formatter, validator, mapper, type-guard),
run `rg -n "export (const|function) <name>" src/` and report what you find.
If a utility with the same purpose exists, import it. Do not create a parallel version.
Then the canonical version lives in one place:
src/lib/format.ts
export const formatDate = (iso: string, locale: string = 'en-GB'): string => {
return new Date(iso).toLocaleDateString(locale)
}
The workflow for getting a generator to find the existing file before it writes a new one is covered in Onboarding to a New Codebase with AI Tools in 2026. Same technique scales to validators, type guards, fetch wrappers, and feature-flag helpers, all of which generators happily duplicate on demand.
One-line summary: AI tools generate duplicate utilities because they treat each prompt as a fresh canvas, and the fix is a project rule that mandates a rg search before any new helper gets created.
A pre-merge checklist you can paste into your PR template
Drop this into the PR template and tick it on every AI-assisted change.
.github/PULL_REQUEST_TEMPLATE.md
## AI-generated code review
- [ ] No `useEffect`-based data fetching, server reads go through TanStack Query or RTK Query
- [ ] Every `fetch` accepts an `AbortSignal` and narrows the error path
- [ ] No `any` on props, function returns, or fetch payloads
- [ ] Every `React.memo` child has stable references from the parent
- [ ] Every optimistic update has an `onError` rollback
- [ ] Every async event handler is wrapped with try/catch and reports to the error tracker
- [ ] No `react-hooks/rules-of-hooks` disable comments
- [ ] No `react-hooks/exhaustive-deps` disable comments
- [ ] No duplicate utilities, confirmed with `rg` against `src/`
The checklist is short on purpose. Eight to ten checks fit in a reviewer's head; fifty do not.
Closing thought for the checklist: a pre-merge gate on AI-generated React code only works if it is short enough that reviewers actually run it.
When to accept the prototype shape on purpose
The nine patterns above are not universal laws. Throwaway spikes, internal admin tools that two people will ever touch, and timed user-research builds are all fine to ship in the vibe-coded shape. If the code's lifetime is measured in days and the audience is the team that wrote it, the hardening pass is overhead the work does not need.
The line we draw with clients: the first paying user. From the moment the code touches a customer who can lose data or time, the checklist applies. Until then, generate fast and ship the prototype. After that, the nine patterns are non-negotiable, and the cost of skipping the checklist shows up in the next on-call rotation.
The summary you want pinned to the team wiki: throwaway prototypes can ship in the vibe-coded shape, but every line of React that touches a paying customer needs the nine-pattern review.
Conclusion
The interesting question is not whether AI coding tools can write production React. They can, with the right prompt and the right reviewer. What matters is the default shape they reach for when neither is in place. A nine-line checklist on the pull request template costs nothing to maintain and catches the patterns that show up in audits every week.
If you want the deeper workflow (rules files for Cursor and Claude Code, agent-friendly project structure, audit cadence, the migration from vibe-coded spikes to a hardened codebase), the vibe-code-to-production book is the longer version of this checklist with the workflows attached. This article is the gate. The book is the playbook behind it.


