You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将useTransition的isPending状态冒泡到祖先组件?

问题:在Next.js中跨服务端组件层级共享Client组件的useTransition状态

我有一个通用客户端组件ClientButton,它用startTransition包裹Server Action来创建点击事件处理函数:

'use client';

export default function ClientButton({ onClick, buttonText, className }: ClientButtonProps) {
    const [isPending, startTransition] = useTransition();
    return <button className={className} onClick={async () => startTransition(() => onClick())}>{buttonText}</button>;
}

我希望在它的祖先组件TicTacToeGame中获取这个isPending状态,TicTacToeGame的代码如下:

'use client';

import { useTransition } from 'react';
import LoadingOverlay from 'react-loading-overlay-ts';

export default function TicTacToeGame({ currentPlayer, grid, cellClickHandler }: TicTacToeStateProps) {
    const [isPending, startTransition] = useTransition();

    return (
        <LoadingOverlay
            active={ isPending }
            spinner
            className='game'
            text='Processing...'
        >
            <Board
                currentPlayer={currentPlayer}
                grid={grid}
                cellClickHandler={cellClickHandler}
            />
            <ClientButton buttonText="New Game" onClick={newGame}/>
        </LoadingOverlay>
    );
}

Board的深层子组件是ClientButton,但TicTacToeGame和ClientButton之间的所有组件都是服务端组件。游戏逻辑全部在服务端,我认为没法用useState或useContext。目前TicTacToeGame里的isPending始终是false。

cellClickHandler和newGame都来自带有'use server'标识的文件,简化代码如下:

'use server';

async function cellClickHandler(currentPlayer: TicTacToePlayer, column: number, row: number) {
    // getGameState()是同步方法,返回仅服务端.ts文件的局部变量,无'use server'以避免大量异步方法
    const currentState = getGameState();
    currentState.currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
    testDelay();
    revalidatePath('/tictactoe');
}

async function newGame() {
    const currentState = getGameState();
    currentState.currentPlayer = 'X';
    testDelay();
    revalidatePath('/tictactoe');
}

function testDelay() {
    const startTime = performance.now();
    while (performance.now() - startTime < 5000) {
        // 延迟5秒模拟慢速代码
    }
}

我之前找到过提到用unstable_wrapCallback的指南,但这个API已经被移除了,找不到现代React/Next.js的替代方案。除了LoadingOverlay没法显示“Processing...”之外,其他功能都正常,就算给服务端函数加了延迟,isPending还是没变化。除了逐层传递回调之外,还有别的解决方案吗?


解决方案

方案1:提升startTransition到祖先客户端组件,传递包装后的回调

既然TicTacToeGame是客户端组件,我们可以把startTransition的逻辑提到这里,然后把已经用startTransition包裹好的回调传递给下层的ClientButton(Next.js支持跨服务端组件传递客户端回调)。

修改TicTacToeGame:

'use client';

import { useTransition } from 'react';
import LoadingOverlay from 'react-loading-overlay-ts';

export default function TicTacToeGame({ currentPlayer, grid, cellClickHandler }: TicTacToeStateProps) {
    const [isPending, startTransition] = useTransition();

    // 用startTransition包裹服务端Action
    const wrappedNewGame = () => startTransition(newGame);
    const wrappedCellClickHandler = (player: TicTacToePlayer, col: number, row: number) => 
        startTransition(() => cellClickHandler(player, col, row));

    return (
        <LoadingOverlay
            active={ isPending }
            spinner
            className='game'
            text='Processing...'
        >
            <Board
                currentPlayer={currentPlayer}
                grid={grid}
                cellClickHandler={wrappedCellClickHandler}
            />
            <ClientButton buttonText="New Game" onClick={wrappedNewGame}/>
        </LoadingOverlay>
    );
}

修改ClientButton,去掉内部的useTransition:

'use client';

export default function ClientButton({ onClick, buttonText, className }: ClientButtonProps) {
    // 直接使用上层传递的已包装回调
    return <button className={className} onClick={onClick}>{buttonText}</button>;
}

这样TicTacToeGame里的isPending就能正确捕获服务端Action的执行状态,LoadingOverlay会正常显示加载状态。

方案2:使用useOptimistic配合服务端Action(可选,优化用户体验)

如果需要在等待服务端响应时给用户即时UI反馈(比如先更新棋盘状态再等服务端同步),可以结合useOptimistic和startTransition:

'use client';

import { useTransition, useOptimistic } from 'react';
import LoadingOverlay from 'react-loading-overlay-ts';

export default function TicTacToeGame({ currentPlayer, grid, cellClickHandler }: TicTacToeStateProps) {
    const [isPending, startTransition] = useTransition();
    // 乐观更新状态
    const [optimisticGrid, setOptimisticGrid] = useOptimistic(grid, (prevGrid, {col, row, player}) => {
        const newGrid = [...prevGrid];
        newGrid[row][col] = player;
        return newGrid;
    });

    const handleCellClick = (player: TicTacToePlayer, col: number, row: number) => {
        // 先更新乐观状态
        setOptimisticGrid({col, row, player});
        // 再执行服务端Action
        startTransition(() => cellClickHandler(player, col, row));
    };

    const wrappedNewGame = () => startTransition(newGame);

    return (
        <LoadingOverlay
            active={ isPending }
            spinner
            className='game'
            text='Processing...'
        >
            <Board
                currentPlayer={currentPlayer}
                grid={optimisticGrid} // 使用乐观更新后的grid
                cellClickHandler={handleCellClick}
            />
            <ClientButton buttonText="New Game" onClick={wrappedNewGame}/>
        </LoadingOverlay>
    );
}

这个方案不仅解决pending状态问题,还能提升用户体验。

原方案失效原因

  • useTransition的isPending是局部状态,仅和当前组件内调用的startTransition绑定,无法跨组件共享;中间的服务端组件不支持React Context,所以Context也无法穿透。
  • 服务端组件仅能接收序列化后的props,无法传递Context或状态订阅,必须把状态管理逻辑提升到最上层客户端组件,再传递包装好的回调。

内容的提问来源于stack exchange,提问作者user3534080

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 15:11:25