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:
- A React.FC must return JSX (or null).
My component body ended right after callingsetPickWinnerLoading(true)
. That means my function returnedvoid
, not aReactElement
. - I destructured a prop that didn’t exist.
I tried to useonPickWinner
, but my interfaceIPickWinnerProps
didn’t define it. TypeScript saw the mismatch right away. - The
children
type conflicted.
I forcedchildren?: undefined
, butReact.FC
automatically injectschildren?: ReactNode
. That caused a contract violation. - 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 insideuseEffect
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.