如何将useTransition的isPending状态冒泡到祖先组件?
我有一个通用客户端组件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

