How Can I Resolve the ‘FC’ Type Error in a React Component?

When I first ran into this error, it completely broke my React + TypeScript build. The compiler screamed at me with the following:

Type '({ onPickWinner, canPick, timeToPick, hasWinner, revealMode }: PropsWithChildren<IPickWinnerProps>) => void'
is not assignable to type 'FC<IPickWinnerProps>'.
  Type 'void' is not assignable to type 'ReactElement<...> | null'.  TS2322

It looked scary, but once I broke it down, the fix turned out to be straightforward. Let me walk you through the problem, the cause, and then the step-by-step solution including a practice version of the component with extra functionality.

The Original Code

Here’s the code I wrote initially:

// GamePlayBlack.tsx

export interface IPickWinnerProps {
    children?: undefined;
    canPick: boolean;
    timeToPick: boolean;
    revealMode: boolean;
    hasWinner: boolean;
}

export const PickWinnerIdle: React.FC<IPickWinnerProps> = (
    {
        onPickWinner,
        canPick,
        timeToPick,
        hasWinner,
        revealMode
    }
) => {
    const gameData = useDataStore(GameDataStore);
    const userData = useDataStore(UserDataStore);
    const [pickWinnerLoading, setPickWinnerLoading] = useState(false);
    setPickWinnerLoading(true);
}

Why This Error Happen

Looking closely, I found four separate issues here:

  1. A React.FC must return JSX (or null).
    My component body ended right after calling setPickWinnerLoading(true). That means my function returned void, not a ReactElement.
  2. I destructured a prop that didn’t exist.
    I tried to use onPickWinner, but my interface IPickWinnerProps didn’t define it. TypeScript saw the mismatch right away.
  3. The children type conflicted.
    I forced children?: undefined, but React.FC automatically injects children?: ReactNode. That caused a contract violation.
  4. I was calling setState during render.
    setPickWinnerLoading(true) runs unconditionally inside the render function. That creates an infinite re-render loop. State updates should be done inside useEffect or event handlers.

The Minimal Fix

Here’s the corrected component that TypeScript is happy with:

import React, { useEffect, useState } from "react";

export interface IPickWinnerProps {
  onPickWinner: () => void | Promise<void>;
  canPick: boolean;
  timeToPick: boolean;
  revealMode: boolean;
  hasWinner: boolean;
  children?: React.ReactNode;
}

export const PickWinnerIdle: React.FC<IPickWinnerProps> = ({
  onPickWinner,
  canPick,
  timeToPick,
  hasWinner,
  revealMode,
  children,
}) => {
  const [pickWinnerLoading, setPickWinnerLoading] = useState(false);

  useEffect(() => {
    // Control loading state properly
    setPickWinnerLoading(timeToPick && canPick && !hasWinner);
  }, [timeToPick, canPick, hasWinner]);

  return (
    <div>
      <p>
        {revealMode
          ? "Reveal mode is ON."
          : "Waiting for the round to finish…"}
      </p>

      <button
        disabled={!canPick || hasWinner || pickWinnerLoading}
        onClick={onPickWinner}
      >
        {pickWinnerLoading ? "Picking…" : "Pick Winner"}
      </button>

      {children}
    </div>
  );
};

Now the component returns JSX, includes the right props, and updates state safely.

The “Practice” Version With Extra Feature

Once I had the fix working, I built a practice version with a few extras:

  • A countdown timer when timeToPick is true.
  • A fake async handler for onPickWinner.
  • A status banner that shows current state.
  • A Reset button to test state changes.

Here’s the full practice component:

import React, { useEffect, useMemo, useState } from "react";

export interface IPickWinnerProps {
  onPickWinner?: () => Promise<void> | void;
  canPick: boolean;
  timeToPick: boolean;
  revealMode: boolean;
  hasWinner: boolean;
  initialCountdown?: number;
  children?: React.ReactNode;
}

export const PickWinnerIdle: React.FC<IPickWinnerProps> = ({
  onPickWinner,
  canPick,
  timeToPick,
  hasWinner,
  revealMode,
  initialCountdown = 10,
  children,
}) => {
  const [isLoading, setIsLoading] = useState(false);
  const [countdown, setCountdown] = useState(initialCountdown);
  const [error, setError] = useState<string | null>(null);
  const [winnerPicked, setWinnerPicked] = useState(false);

  const status = useMemo(() => {
    if (revealMode) return "Reveal mode";
    if (hasWinner || winnerPicked) return "Winner picked";
    if (!canPick) return "You cannot pick yet";
    if (timeToPick) return "Time to pick!";
    return "Waiting…";
  }, [revealMode, hasWinner, winnerPicked, canPick, timeToPick]);

  useEffect(() => {
    if (timeToPick && canPick && !hasWinner && !winnerPicked) {
      setCountdown(initialCountdown);
    }
  }, [timeToPick, canPick, hasWinner, winnerPicked, initialCountdown]);

  useEffect(() => {
    if (!(timeToPick && canPick) || hasWinner || winnerPicked) return;

    const id = setInterval(() => {
      setCountdown((s) => (s > 0 ? s - 1 : 0));
    }, 1000);

    return () => clearInterval(id);
  }, [timeToPick, canPick, hasWinner, winnerPicked]);

  const handlePickWinner = async () => {
    if (!canPick || hasWinner || winnerPicked) return;

    setError(null);
    setIsLoading(true);
    try {
      if (onPickWinner) {
        await onPickWinner();
      } else {
        await new Promise((res) => setTimeout(res, 900)); // fake API
      }
      setWinnerPicked(true);
    } catch (e) {
      setError(e instanceof Error ? e.message : "Failed to pick winner.");
    } finally {
      setIsLoading(false);
    }
  };

  const handleResetPractice = () => {
    setError(null);
    setIsLoading(false);
    setWinnerPicked(false);
    setCountdown(initialCountdown);
  };

  const canClickButton =
    canPick && !hasWinner && !winnerPicked && !isLoading && (!timeToPick || countdown > 0);

  return (
    <div style={{ padding: 16, border: "1px solid #ddd", borderRadius: 8 }}>
      <h3 style={{ marginTop: 0 }}>Pick Winner</h3>

      <div style={{ marginBottom: 8 }}>
        <strong>Status:</strong> {status}
      </div>

      {timeToPick && !hasWinner && !winnerPicked && (
        <div style={{ marginBottom: 8 }}>
          <strong>Countdown:</strong> {countdown}s
        </div>
      )}

      <div style={{ display: "flex", gap: 8, marginBottom: 8 }}>
        <button onClick={handlePickWinner} disabled={!canClickButton}>
          {isLoading ? "Picking…" : "Pick Winner"}
        </button>
        <button onClick={handleResetPractice} type="button">
          Reset (Practice)
        </button>
      </div>

      {error && (
        <div style={{ color: "red", marginBottom: 8 }}>
          Error: {error}
        </div>
      )}

      <div>{children}</div>
    </div>
  );
};

Final Thought

When I first saw the error “Type ‘void’ is not assignable to type ‘FC<…>’”, I thought TypeScript was just being overly picky, but it was actually pointing out real flaws in my component. I wasn’t returning JSX, I destructured props that weren’t even defined, I forced children incorrectly, and I called setState during render. Once I fixed these mistakes, the error disappeared and the component worked as expected. Building the “practice” version also helped me get more comfortable with timers, async handlers, and safer state management in React.

Related blog posts