SolidJS顶层Context Provider晚于子组件执行?求原因及修复方案
问题现象
在给定的SolidJS代码中,控制台日志输出顺序为:
"Card Body", undefined "CardProvider", {lighter: true}
即便Context Provider在组件层级中处于顶层,子组件Card.Body仍会先于CardProvider完成初始渲染,导致useContext获取不到预期的Context值。
代码示例
组件实现
import { JSXElement, createContext, createSignal, useContext } from "solid-js"; interface CardProps { children: JSXElement; lighter: boolean; } const CardContext = createContext<{ lighter: boolean }>(); function CardProvider(props: CardProps) { const [lighter] = createSignal(props.lighter); const value = { lighter: lighter() }; console.log("CardProvider", value); return ( <CardContext.Provider value={value}> {props.children} </CardContext.Provider> ); } function useInternalState() { return useContext(CardContext); } export function Card({ children, lighter = false }: CardProps) { return ( <CardProvider lighter={lighter}> <div class={`transition-all ease-in-out duration-300 bg-gray-800 border-[1px] border-gray-100 border-opacity-10 p-2 rounded-md my-2 md:basis-1/3 text-center md:mr-2 flex items-center hover:bg-gray-900 ${ lighter ? "bg-gray-200 hover:bg-gray-300" : "" }`} > {children} </div> </CardProvider> ); } interface CardTitleProps { title: string; } Card.Title = ({ title }: CardTitleProps) => { const state = useInternalState(); return ( <div class={`${ state?.lighter ? "text-gray-900" : "text-gray-100" } text-lg font-semibold`} > {title} </div> ); }; interface CardBodyProps { body: string; } Card.Body = ({ body }: CardBodyProps) => { const state = useInternalState(); console.log("Card Body", state); return ( <div class={`${ state?.lighter ? "text-gray-600" : "text-gray-400" } text-md font-semibold`} > {body} </div> ); };
使用方式
<Card lighter={false}> <Card.Title title="Title" /> <Card.Body body="Body" /> </Card>
原因分析
SolidJS是编译型框架,JSX会被编译为高效的渲染函数。在原代码的Card组件中,<div>{children}</div>这个JSX节点会先被完全求值——也就是Card.Title和Card.Body组件会先执行,此时CardProvider还未开始执行,CardContext.Provider也没有被创建,所以子组件调用useContext(CardContext)只能拿到Context的默认值undefined。
等子组件执行完毕后,求值得到的div节点才会作为props.children传递给CardProvider,此时CardProvider才开始执行,输出日志并创建Context.Provider,但这时候子组件的初始渲染已经完成了。
修复方案
方案1:将容器div移至CardProvider内部
把原Card组件中包裹children的div,移到CardProvider内部渲染,确保子组件在Context.Provider创建后才被求值:
// 修改CardProvider function CardProvider(props: CardProps) { const [lighter] = createSignal(props.lighter); const value = { lighter: lighter() }; console.log("CardProvider", value); return ( <CardContext.Provider value={value}> <div class={`transition-all ease-in-out duration-300 bg-gray-800 border-[1px] border-gray-100 border-opacity-10 p-2 rounded-md my-2 md:basis-1/3 text-center md:mr-2 flex items-center hover:bg-gray-900 ${ lighter() ? "bg-gray-200 hover:bg-gray-300" : "" }`} > {props.children} </div> </CardContext.Provider> ); } // 修改Card组件 export function Card({ children, lighter = false }: CardProps) { return ( <CardProvider lighter={lighter}> {children} </CardProvider> ); }
方案2:使用函数式子组件
让CardProvider接收函数类型的children,在Provider内部调用该函数,确保子组件在Context可用后再执行:
// 修改CardProvider的props类型 function CardProvider(props: { lighter: boolean; children: () => JSXElement }) { const [lighter] = createSignal(props.lighter); const value = { lighter: lighter() }; console.log("CardProvider", value); return ( <CardContext.Provider value={value}> {props.children()} </CardContext.Provider> ); } // 修改Card组件 export function Card({ children, lighter = false }: CardProps) { return ( <CardProvider lighter={lighter}> {() => ( <div class={`transition-all ease-in-out duration-300 bg-gray-800 border-[1px] border-gray-100 border-opacity-10 p-2 rounded-md my-2 md:basis-1/3 text-center md:mr-2 flex items-center hover:bg-gray-900 ${ lighter ? "bg-gray-200 hover:bg-gray-300" : "" }`} > {children} </div> )} </CardProvider> ); }
两种方案都能确保CardProvider先创建Context.Provider,再渲染子组件,此时Card.Body的useContext就能拿到正确的{lighter: ...}值,控制台日志顺序也会变为先输出"CardProvider",再输出"Card Body"和正确的Context值。
内容的提问来源于stack exchange,提问作者Dansekongen

