When watching AI tool demos, clicking a few buttons and typing a few sentences produces stunning UIs. The problem is reality. The moment you bring code spewed out by AI into an existing project, styles break completely and state management gets tangled up. Almost everyone has experienced spending twice as much development time fixing and wrestling with this code.
In the end, to use AI code as-is, you must bind static analysis, state structuring, and visual verification into automated guardrails. Here is a concrete setup guide for bringing in AI code in a ready-to-use state without pulling all-nighters to fix it.
Stripping Away Arbitrary Tailwind Classes and Inline Styles
AI can understand context well, yet suddenly ignore project tokens out of nowhere. It recklessly shoves in bracketed arbitrary values like w-[327px] or inline styles containing hex codes. Design systems collapse in an instant, and CSS specificity gets twisted.
Catching and fixing this manually with human eyes is too late. You need to block the entry path using a Linter.
- Install the necessary packages in your project.
`bash
npm install -D eslint-plugin-tailwindcss @html-eslint/eslint-plugin
`
- Add rules to the
eslint.config.mjsfile that catch bracketed arbitrary values and inline styles as errors. - Register
npx eslint --fixin Package scripts and run it immediately after receiving code.
With this setup, the moment bracketed values or inline styles enter, the linter throws them as errors and forcefully replaces them with the nearest standard utility classes. Eliminating this tedious chore saved over 5 hours per week previously spent aligning styles.
`javascript
// eslint.config.mjs
import eslintPluginTailwindcss from "eslint-plugin-tailwindcss";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
plugins: {
tailwindcss: eslintPluginTailwindcss,
},
settings: {
tailwindcss: {
cssConfigPath: "./styles/tailwind.css",
},
},
rules: {
"tailwindcss/no-arbitrary-value": "error",
"tailwindcss/no-custom-classname": [
"error",
{ whitelist: ["custom\-*"] },
],
"tailwindcss/classnames-order": "warn",
},
},
]);
`
| Linting Rule | Inspection Method | Block Target Example | Auto-Fix Result |
|---|---|---|---|
| @html-eslint/no-inline-styles | Block JSX inline styles | <div style={{color: '#ff0a00'}}> |
<div className="text-destructive"> |
| tailwindcss/no-arbitrary-value | Block bracketed arbitrary values | <button className="w-[327px]"> |
<button className="w-80"> |
| tailwindcss/no-custom-classname | Detect unregistered class names | <div className="my-custom-card"> |
<div className="rounded-lg border bg-card shadow-sm"> |
Cleaning Up useState Sprawled Inside Components
When asking AI to build a React component, nine times out of ten it scatters three or four useState hooks inside a single file. When asynchronous API calls and error handling get tangled in on top of this, a rendering explosion occurs.
Client UI state should be consolidated into Zustand, while server data should be separated using React Query. Input forms should have their structure anchored with a combination of Zod and react-hook-form.
- Install dependency libraries.
`bash
npm install @tanstack/react-query zod react-hook-form @hookform/resolvers
`
- Ban direct
fetchusage in the AI prompt schema and force types to be defined first using Zod schemas. - Attach
zodResolvertoreact-hook-formto automate validation, and structure it to calluseMutationat the submit timing.
Unnecessary re-renders disappear completely. Input validation failures cannot even enter the inside of the component.
`typescript
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
export const userProfileSchema = z.object({
username: z.string().min(2, { message: '이름은 최소 2자 이상이어야 합니다.' }),
email: z.string().email({ message: '올바른 이메일 형식이 아닙니다.' }),
role: z.enum(['admin', 'user'], { required_error: '역할을 선택하세요.' }),
});
export type UserProfileInputs = z.infer;
export function ProfileForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(userProfileSchema),
});
const onSubmit = (data: UserProfileInputs) => {
// React Query Mutation 실행
};
return (
{/* 폼 레이아웃 */}
);
}
`
| Area | AI Anti-Pattern | Refined Structure | Benefit |
|---|---|---|---|
| Client State | Fragmented useState | Single Zustand store | Prevents unnecessary re-renders |
| Server Data | fetch inside useEffect | Wrapped in React Query | Automatic caching and declarative loading handling |
| Form Validation | Manual conditional checks | Zod + react-hook-form | Type safety and validation automation |
Visual Testing Loops to Catch Layout Breaks
Looking at just a desktop screen and saying "Oh, looks good" only to open it on mobile and find a complete layout mess is all too common. According to WebAIM's 2025 Web Accessibility Report, an average of over 51 WCAG errors occur on the homepages of the top 1 million sites alone. Let alone responsive layout breaks in freshly generated AI code.
Automating regression testing with Playwright and Pixelmatch eliminates the need to shrink browser windows and check manually one by one.
- Install test tools.
`bash
npm install -D @playwright/test @axe-core/playwright
`
- Upload a script that takes snapshots across Mobile (375px), Tablet (768px), and Desktop (1440px) viewports.
- Set the
maxDiffPixelRatiothreshold to 0.02. If it exceeds this, resend the pixel difference image to the AI prompt to force media queries to be rewritten.
After running this rendering test loop, manual UI inspection time was reduced by over 80%.
`typescript
import { test, expect } from '@playwright/test';
const viewports = [
{ name: 'mobile', width: 375, height: 667 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1440, height: 900 },
];
for (const vp of viewports) {
test(Responsive layout test - ${vp.name}, async ({ page }) => {
await page.setViewportSize({ width: vp.width, height: vp.height });
await page.goto('/render-test-harness');
await expect(page.locator('#ai-component-root')).toHaveScreenshot(
`component-${vp.name}.png`,
{
maxDiffPixelRatio: 0.02,
threshold: 0.2,
animations: 'disabled',
}
);
});
}
`
Filtering Out Weird Code Right Before Committing
No matter how well you craft your prompts, weird garbage code occasionally slips through just as an air conditioner might misbehave. Code that breaks types or fails linting right before a commit must be blocked from entering the Git workspace.
Set up Git hooks using Lefthook.
- Install Lefthook in your project and create
lefthook.ymlat the root.
`bash
npm install -D lefthook
`
- Hook ESLint, Prettier, and TypeScript check commands into the
pre-commitstage. - Turn on the
stage_fixed: trueoption so auto-fixed changes are immediately reflected in the commit.
If there are unfixed type errors or unauthorized deleted props, the commit gets blocked on the spot. It is the most reliable way to end the inefficiency of developers standing by as manual code reviewers to clean up AI remnants.
`yaml
lefthook.yml
pre-commit:
commands:
eslint-autofix:
glob: ".{js,ts,jsx,tsx}"
run: npx eslint --fix {staged_files}
stage_fixed: true
prettier-format:
glob: ".{js,ts,jsx,tsx,css,json}"
run: npx prettier --write {staged_files}
stage_fixed: true
typescript-check:
glob: "*.{ts,tsx}"
run: npx tsc --noEmit
`
