跪拜 Guibai
← Back to the summary

AI-Generated Code Passed Review but Failed the Interview: Three 'Why' Questions That Exposed the Gap

A couple of days ago, I interviewed with a company. The first two rounds went fairly well — I could answer questions about project experience, tech stack choices, and team collaboration. The third round was a deep technical dive. The interviewer opened a project mentioned on my resume, shared their screen, and asked me to open the code for a specific component.

He pointed at a piece of logic and asked: "Why did you wrap this with useCallback?"

I looked at that code — it was indeed from my project, but in that instant, my mind went completely blank. That component was generated by Claude Code. The prompt I wrote was "write a panel component with a search function," and it generated the entire file. I saw that it ran, so I committed it. I had never thought about why useCallback was used.

After that interview, I did one thing: I reviewed the core code I'd written in the last six months and asked myself "why is it written this way" for every piece of logic. The result made me break out in a cold sweat.

The first question I couldn't answer: "Is this useCallback necessary?"

The original code the interviewer asked about looked roughly like this:

function SearchPanel({ onSearch }) {
  const [keyword, setKeyword] = useState('');

  const handleChange = useCallback((e) => {
    setKeyword(e.target.value);
  }, []);

  const handleSubmit = useCallback(() => {
    onSearch(keyword);
  }, [keyword, onSearch]);

  return (
    <div>
      <input value={keyword} onChange={handleChange} />
      <button onClick={handleSubmit}>搜索</button>
    </div>
  );
}

The interviewer followed up: "What is the benefit of using useCallback for handleChange here?"

My answer at the time was: "To prevent unnecessary re-renders of child components."

The interviewer asked again: "input is a native DOM element, not a child component wrapped in React.memo — will a native element re-render because the onChange reference passed from the parent changes?"

I froze.

The answer is no. Native DOM elements (<input>, <div>, <button>) do not participate in React's reference comparison optimization. They re-execute every time the parent component renders, regardless of whether the props reference has changed. useCallback here is completely meaningless — it doesn't reduce render count and adds the mental overhead of a closure and dependency array maintenance.

// ✅ Callbacks passed to native elements don't need useCallback
function SearchPanel({ onSearch }) {
  const [keyword, setKeyword] = useState('');

  const handleChange = (e) => {
    setKeyword(e.target.value);
  };

  const handleSubmit = () => {
    onSearch(keyword);
  };

  return (
    <div>
      <input value={keyword} onChange={handleChange} />
      <button onClick={handleSubmit}>搜索</button>
    </div>
  );
}

useCallback only makes sense in one scenario: when passed to a child component wrapped in React.memo, or when a stable reference is needed as a dependency for useEffect/useMemo. Passing it to native elements is pure waste.

How many times did I make this mistake? I checked my project — at least 12 places. Every single one was automatically added when the AI generated the whole component. I never questioned it.

The second question I couldn't answer: "How do you handle concurrent requests?"

The interview moved to a second code snippet, an autocomplete component:

function AutoComplete({ fetchSuggestions }) {
  const [query, setQuery] = useState('');
  const [suggestions, setSuggestions] = useState([]);

  useEffect(() => {
    if (query.length > 0) {
      fetchSuggestions(query).then((data) => {
        setSuggestions(data);
      });
    } else {
      setSuggestions([]);
    }
  }, [query]);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ul>
        {suggestions.map((s) => <li key={s.id}>{s.text}</li>)}
      </ul>
    </div>
  );
}

The interviewer asked: "If a user quickly types the five letters 'react', and the request for 'r' comes back later than the request for 'react', what happens?"

It took me a few seconds to realize — the displayed results would be for 'r', not 'react'. A race condition.

The interviewer continued: "How did you handle this scenario in your project?"

I honestly said I wasn't sure. This code was also generated entirely by the AI in one go. After it "worked," I never thought deeper about it.

The correct approach is to use AbortController or ignore stale requests:

function AutoComplete({ fetchSuggestions }) {
  const [query, setQuery] = useState('');
  const [suggestions, setSuggestions] = useState([]);

  useEffect(() => {
    if (query.length === 0) {
      setSuggestions([]);
      return;
    }

    const controller = new AbortController();

    fetchSuggestions(query, { signal: controller.signal })
      .then((data) => {
        setSuggestions(data);
      })
      .catch((err) => {
        if (err.name !== 'AbortError') throw err;
      });

    return () => controller.abort();
  }, [query]);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ul>
        {suggestions.map((s) => <li key={s.id}>{s.text}</li>)}
      </ul>
    </div>
  );
}

Or a lighter-weight solution — filtering stale responses by request ID:

useEffect(() => {
  if (query.length === 0) {
    setSuggestions([]);
    return;
  }

  let cancelled = false;

  fetchSuggestions(query).then((data) => {
    if (!cancelled) {
      setSuggestions(data);
    }
  });

  return () => { cancelled = true; };
}, [query]);

In the cleanup function, cancelled is set to true. When the next effect runs, even if the previous request returns, it won't update the state.

The crux of this problem: AI-generated code defaults to the happy path. It gives you a version that "works" but doesn't proactively think for you: "What if the network is slow / requests arrive out of order / the user acts quickly?" And after using AI for half a year, you get used to the "generate → runs → commit" flow and gradually stop asking yourself "what about the edge cases?"

The third question I couldn't answer: "Why split this into three components?"

The interviewer opened a form page from my project. The structure was roughly like this:

// FormPage.tsx
function FormPage() {
  return (
    <FormContainer>
      <FormHeader />
      <FormBody />
      <FormFooter />
    </FormContainer>
  );
}

// FormHeader.tsx — only a title and one line of description
function FormHeader() {
  return (
    <div>
      <h2>创建项目</h2>
      <p>填写以下信息创建新项目</p>
    </div>
  );
}

The interviewer asked: "FormHeader is just two lines of static text. Why extract it into a separate component?"

I froze again.

The honest answer is: I told Claude Code "generate a form page for creating a project," and it automatically split it into Header/Body/Footer. I thought it "looked neat" and accepted it. But if I really had to answer "why" — there is no reasonable justification here.

Criteria for a component worth extracting:

  1. Needs reuse — the same structure is used elsewhere
  2. Has its own state or logic — internally manages independent state or effects
  3. Is a performance isolation boundary — wrapped in React.memo to prevent parent re-renders from propagating

FormHeader meets none of these. It's not reused, has no state, and doesn't need performance isolation. The only effect of extracting it is an extra file, an extra import, and an extra layer in the component call stack — purely increasing the project's complexity.

// ✅ Inline static content directly; no separate component needed
function FormPage() {
  return (
    <FormContainer>
      <div>
        <h2>创建项目</h2>
        <p>填写以下信息创建新项目</p>
      </div>
      <FormBody />
      <FormFooter />
    </FormContainer>
  );
}

This isn't a question of "right or wrong" — many people would say "there's no harm in splitting it out." But what the interviewer was really asking was: Did you think about this decision when you made it? If your answer is "the AI generated it this way and I used it," you're telling the interviewer: you don't make design decisions; you're just the AI's commit tool.

Why using AI for a long time leads to this state

After the interview, I reflected for a long time. The essence of the problem isn't "my skills have regressed," but that AI has changed my way of working. Now, using Claude Code or Codex, a single sentence describing a requirement can generate an entire component or even a whole page. Efficiency is indeed ten times higher — but at a cost:

Previous workflow Current workflow
Figure out what to write → type code line by line One prompt lets AI generate the whole component → it runs → commit
Encounter a problem, first think about the cause Encounter a problem, directly throw it to AI for a rewrite
Every line of code is your own decision The entire file is AI-generated; I only reviewed it once
Can explain every choice during Code Review During Code Review, can't clearly explain the "why" behind AI-generated code

The key difference: Before, it was "think first, then code." Now, it's "describe requirements → AI fully implements → I glance at it → commit."

AI won't think about "why" for you. It gives you the "what" — a complete, runnable implementation. But what the interviewer always asks is "why" — why this approach and not another. If you only have the "what" without the "why," you're exposed in an interview.

Pre-interview self-check: Can you explain your code?

After the interview, I made a checklist for myself to go through when reviewing project code:

5 signals to judge whether you're an "AI agent" rather than a "code author":

  1. Open a component you wrote three months ago. Can you say within 30 seconds "why this Hook and not another"? If not, you didn't make this decision.
  2. How many useCallback/useMemo are in your project? Would removing them cause a measurable performance difference? Answering "not sure" means they were likely added by AI inertia.
  3. Search all useEffect instances in your project. Do you know why every cleanup function is written the way it is? If not, you haven't thought about lifecycle boundaries.
  4. Randomly open a utility function you "wrote." Can you draw its execution flow without looking at the code? If not, the AI wrote it; you just clicked accept.
  5. Print out the code from your most recent PR. Without looking at a computer, can you answer a colleague's follow-up questions on paper? If not, you "own" this code but don't "understand" it.

If you hit 3 out of 5, it means your current state is: You are responsible for writing prompts to describe requirements; the AI is responsible for the entire implementation. As soon as the interviewer asks "why" twice, you'll be exposed.

3-day pre-interview AI code self-review checklist

Review Dimension Question to ask yourself Common AI blind spots
Performance decisions Is every useMemo/useCallback necessary? Inertia of wrapping all functions with useCallback when generating a whole component
Edge case handling What about request failures/timeouts/concurrency conflicts? Only implements the happy path, doesn't proactively handle edge cases
Component boundaries Why split it this way? Is there a simpler way? Over-abstraction following "best practice templates"
Type design Why are TS types defined this way? Can they be narrowed? Lots of any or overly broad union types
State management Why is this state at this level? Can it be lifted or pushed down? Defaults to stuffing all state into the same component
Dependencies Do you know why every item in the useEffect dependency array is needed? Adding everything based on eslint prompts without thinking about necessity

Core principle: Before the interview, go through all the AI-written code in your resume projects. For every piece of logic, be able to answer "why this way" and "what would happen if it weren't this way." It's not about rewriting; it's about understanding.

It's not about not using AI, but asking one more "why" after using it

The biggest lesson from this interview isn't "AI is bad," but rather: I treated AI as an outsourced contractor I could fully delegate to, but it's really just a code generator that doesn't explain its reasoning.

When a colleague writes a piece of code for you, you ask "why did you write it this way?" When AI generates an entire component, you glance at it, see it runs, and merge it. This gap is invisible in daily work — the code runs, the review passes, nothing breaks in production. But the interviewer specifically pokes at this gap: Are you the author of the code, or just the AI's commit tool?

Going forward, I'll add one step to my AI habit: every time AI generates code, spend 5 minutes asking myself section by section, "If the interviewer asks me why, how would I answer?" If I can't answer — either figure it out before committing, or have the AI explain it clearly before I accept it.

Have you ever had a moment in an interview where the interviewer pressed you with "why" and you couldn't answer? Share your version in the comments.

Comments

Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.

星河微尘

Do you show your own business code directly to the interviewer during an interview?

kyriewen

That's pretty normal, right? Desensitized, it can boost your competitiveness.