An AI assistant can produce a React component that looks plausible and still leave you with a broken search, inaccessible feedback, or an unrelated dependency change. The problem is not necessarily the prompt. It is the absence of a clear definition of done and a way to check the result.

A useful AI-assisted frontend workflow produces a small, reviewable change with evidence, not just generated code. Give the assistant a bounded task, specify behavior before implementation, test the important edge cases independently, and inspect every changed file before merging.

Here is that process applied to one deliberately small feature: filtering a list of articles by title. The example is not a report of an AI model's measured accuracy or productivity. It is a reproducible exercise for evaluating an AI-proposed change.

Begin with a change contract, not a tool

Imagine a React page that already displays articles. The requested enhancement sounds simple: "Add a search box." That sentence leaves several questions unanswered. Should matching be case-sensitive? What does an empty query do? Does the search inspect titles or summaries? What should a keyboard or screen-reader user encounter when nothing matches?

For this example, the acceptance contract is:

RequirementObservable result
Search scopeMatch article titles only, using a substring search
Text handlingTrim the query; compare English titles without case sensitivity
Empty queryShow all articles in their original order
No resultsShow a visible message and a count of zero
InteractionUse a labeled native search input; keep keyboard focus in the input while filtering
Data integrityDo not mutate the incoming article array or its objects
ScopeNo network requests, new packages, routing changes, or unrelated refactors

The title-only and English-language rules are intentional simplifications, not general-purpose search recommendations. Accent folding, locale-sensitive comparisons, ranking, fuzzy matching, and server-side search need separate requirements.

Before asking an assistant to edit anything, inspect the existing component, its props, test framework, and the project's reusable input or list components. A repository instruction file can communicate conventions, but it cannot replace understanding the actual code. GitHub documents repository-level custom instructions and cautions that they do not guarantee compliance.

A first prompt can now request a plan instead of a patch:

Inspect the existing article list and test setup. Propose the smallest change that satisfies the acceptance contract above. Identify ambiguous behavior and tests that would fail if the implementation is wrong. Do not modify files, install packages, or run commands yet. Cite the existing components you intend to reuse.

Review that plan. Then allow a narrow implementation. This pause is useful because a design error is cheaper to correct before it spreads across a large diff.

Put the behavior in a function you can test

For the agreed English-title search, the core implementation is small enough to inspect without a framework:

// filter-articles.mjs
export function filterArticles(articles, query) {
  const needle = query.trim().toLowerCase();

  if (needle === '') return articles;

  return articles.filter(({ title }) =>
    title.toLowerCase().includes(needle)
  );
}

The function assumes query is a string and each article has a string title. It preserves the original ordering because filter() keeps matching items in sequence; it does not sort or modify the input. Returning the input array for an empty query is safe under the stated read-only contract, although callers must not then mutate it.

This is a better place to catch mistakes than an end-to-end screenshot. For example, a generated implementation that searches JSON.stringify(article) would accidentally match hidden summaries and IDs. A version that calls sort() on the incoming array could reorder the caller's data.

The accompanying Node test uses frozen fixtures and checks five distinct behaviors:

// filter-articles.test.mjs
import test from 'node:test';
import assert from 'node:assert/strict';
import { filterArticles } from './filter-articles.mjs';

const articles = Object.freeze([
  Object.freeze({ id: 'a', title: 'React rendering' }),
  Object.freeze({ id: 'b', title: 'CSS container queries' }),
  Object.freeze({ id: 'c', title: 'React testing' }),
]);

test('blank query preserves the original list', () => {
  assert.deepEqual(filterArticles(articles, ' \t '), articles);
});

test('trims and matches without case sensitivity', () => {
  assert.deepEqual(
    filterArticles(articles, '  REACT ').map(({ id }) => id),
    ['a', 'c']
  );
});

test('unmatched query returns no results', () => {
  assert.deepEqual(filterArticles(articles, 'Vue'), []);
});

test('does not search summaries', () => {
  const entries = [{ id: 'x', title: 'CSS Grid', summary: 'React integration' }];
  assert.deepEqual(filterArticles(entries, 'React'), []);
});

test('does not mutate the input', () => {
  filterArticles(articles, 'CSS');
  assert.deepEqual(articles.map(({ id }) => id), ['a', 'b', 'c']);
  assert.equal(articles[1].title, 'CSS container queries');
});

The committed reproduction under docs/editorial/reproductions/ uses the ai-workflow- filename prefix; the snippets above use shorter filenames for readability. Run it from the repository root with node --test docs/editorial/reproductions/ai-workflow-filter-articles.test.mjs after this revision is merged. In an isolated editorial run with Node.js 22.16.0, all five tests passed. That result verifies this small filtering function and these fixtures, not an AI assistant, a rendered React page, or production behavior.

AI can suggest additional test cases. The developer should still decide which invariants matter and review the assertions themselves. A test that asserts an incorrect requirement can pass consistently.

Integrate the function without inventing more state

Now add a UI. Place a copy of the filter function next to this component under the shorter filename shown above. The following is an illustrative React client component; it has been checked against React and Next.js documentation, but was not executed as a browser integration test in this rebuild.

// ArticleFinder.jsx (client entry in a Next.js App Router project)
'use client';

import { useId, useState } from 'react';
import { filterArticles } from './filter-articles.mjs';

export function ArticleFinder({ articles }) {
  const [query, setQuery] = useState('');
  const inputId = useId();
  const results = filterArticles(articles, query);

  return (
    <section aria-label="Find articles">
      <label htmlFor={inputId}>Filter articles by title</label>
      <input
        id={inputId}
        type="search"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      <p role="status">
        {results.length} {results.length === 1 ? 'article' : 'articles'} found
      </p>
      {results.length === 0 && <p>No matching articles.</p>}
      <ul aria-label="Matching articles">
        {results.map(({ id, title }) => (
          <li key={id}>{title}</li>
        ))}
      </ul>
    </section>
  );
}

Only query is component state. results is calculated from props and state during rendering, so no syncing useEffect is needed. This follows React's guidance on derived values. The useId() call supplies an input-label ID, not a list key; list keys come from the article data, as explained in React's useId reference. In a Next.js App Router client entry point, the 'use client' directive supports this interactive state and event handling (Next.js documentation).

The label describes the input; the native search input supports familiar keyboard editing. The status role is an advisory live region with an implicit polite announcement setting (MDN). Whether its updates are announced as intended still needs screen-reader testing in the actual interface. Do not infer full accessibility from the presence of ARIA attributes.

If the real page uses a design-system search input, compare its semantics and behavior before replacing it with this example. If filtering becomes expensive on a genuinely large data set, measure first; an AI suggestion to add memoization or debouncing is not evidence that either is required.

Test the UI behavior that the function cannot see

Unit tests do not tell you whether the input has an accessible label, whether the result list remains visible, or whether typing unexpectedly moves focus.

A Playwright check for a page that actually mounts ArticleFinder could look like this:

// Illustrative Playwright test: requires a running /articles page
// with an article titled "React rendering" in its fixture data.
import { test, expect } from '@playwright/test';

test('filters by title without losing search focus', async ({ page }) => {
  await page.goto('/articles');

  const input = page.getByRole('searchbox', {
    name: 'Filter articles by title',
  });

  await input.fill('react');
  await expect(input).toBeFocused();
  await expect(page.getByRole('list', {
    name: 'Matching articles',
  })).toContainText('React rendering');

  await input.fill('no-such-title');
  await expect(page.getByText('No matching articles.')).toBeVisible();
  await expect(page.getByRole('status')).toHaveText('0 articles found');
});

Playwright's locator guidance recommends user-facing roles and accessible names where appropriate. This test is not executed here because no matching running /articles fixture was created. It is also not a replacement for checking keyboard operation, visual focus, or announcements with assistive technology.

If your CI browser test fails, examine the failed step and trace rather than treating a retry as proof of correctness. The Playwright local-versus-CI debugging guide covers that separate problem.

Review the diff as if the assistant were a new contributor

Before accepting the change, compare the patch to the contract, not to the wording of the prompt. This review table is designed to expose plausible but unwanted AI edits:

Suspicious changeQuestion to askEvidence to request
Extra state plus useEffect for filtered resultsIs this value already derivable from props and query?React data-flow explanation and the smallest working diff
New search dependencyDoes simple title matching require it?Package identity, necessity, lockfile change, security review
Snapshot test onlyCan a wrong filter still produce an approved snapshot?Assertions for empty query, case handling, order, and no results
Generated div pretending to be a button or inputAre native controls being replaced for no reason?Keyboard and accessible-name checks
Broad formatting or configuration editsAre these needed to satisfy the request?A file-by-file explanation; remove unrelated changes
"Tests pass" in the responseWhich exact commands ran, and against which revision?Actual process exit codes or CI links

Run the project's normal checks on the final diff, not on a previous intermediate patch. For this standalone example, the concrete check is node --test; a real feature also needs the repository's lint, type-check, build, and relevant browser tests.

A useful completion report is short and auditable: changed files, intended behavior, tests and actual outcomes, untested paths, new dependencies, and remaining reviewer decisions. The assistant can write that report, but it should not manufacture terminal output.

Limit what the assistant can read and do

An inline suggestion tool and an agent with terminal, network, and repository write permissions have different risk profiles. For a small UI enhancement, read-only planning followed by narrowly authorized edits is usually enough.

Do not paste production tokens, customer records, private logs, or proprietary code into a service unless the project's data-handling policy permits it. Treat issue descriptions, copied documentation, fetched web pages, tool output, and repository instructions as untrusted content, not permission to change goals. An agent should not install a package, modify CI security, run destructive commands, or push a branch merely because external text told it to.

OWASP's Secure Coding with AI Cheat Sheet describes indirect prompt injection, untrusted dependencies, test manipulation, and excessive agent permissions. These threats call for scoped credentials, approval boundaries, isolated execution where appropriate, and independent review. A second AI reviewer can surface questions, but it does not substitute for checking the code and the behavior yourself.

Decide whether AI actually helped

Compare the whole change, not generation time alone. Did the assistant preserve the existing architecture? Did its tests catch a genuine edge case? How much cleanup and review did its patch require? Could you explain the final component and confidently maintain it without the assistant?

For this example, the deliverable is a title filter with an explicit contract, a testable pure function, a client UI integration pattern, and a list of browser and security checks still to perform. The five passing unit tests do not prove an increase in developer productivity or production readiness. They prove something smaller and more useful: a defined behavior can be checked independently of the code generator.

The best place for AI in a frontend workflow is wherever it helps you reach a verified, understandable diff with less unnecessary work. If a task is too ambiguous to verify, clarify the requirements before generating more code.