需求
在公司的脚手架上应用主题,流程是
graph TD
获取主题索引 --> 从索引中获取主题文件位置 --> 获取指定主题
所以我写出了这样的代码
const ThemeContext = createContext<Theme|null>(null)
export function ThemeContextProvider(props:{children:ReactNode}){
const [themeMap,setThemeMap]=useState<ThemeMap>()
const [themeObj,setThemeObj]=useState<any>()
useEffect(()=>{
getThemeMap()
.then(async res=>{
const newThemeMap = res.data
const newThemeObj = await getThemeObj(newThemeMap)
setThemeMap(newThemeMap)
setThemeObj(newThemeObj)
})
},[])
const contextValue = useMemo(()=>{
return {themeMap,themeObj}
},[themeMap,themeObj])
return (
<ThemeContext.Provider value={contextValue}>
{props.children}
</ThemeContext.Provider>
)
}
export function useTheme(){
const theme = useContext(ThemeContext)
return theme
}
目前看上去非常合理
问题
直到我开始写一个主题选择器
const ThemeSelect = memo((props) => {
const { themeMap, themeObj } = useTheme()
const options = useMemo(() => {
return getOptions(themeMap)
}, [themeMap])
if (!themeMap) return null
return <Select defaultValue={themeObj.themeName} options={options} onSelect={props.onChange}></Select>
})
系统的设计是url也可以提供主题,主题索引可能没有相应的主题,这种情况下不应当应用主题,themeObj保持空值,因此这里只检测了themeMap。后来调试的时候发现这个Select的默认值总是空。
解决
直到我在这个组件中输出themeMap和themeObj,才发现这个组件一共渲染了三次。第一次输出两个undefined,第二次themeMap有值,themeObj是undefined,第三次两个都有值。
这只是一个脚手架,所以问题肯定出在设置主题索引和对象上。但是为什么呢?据我(当时)所知,setState是异步运行的,合并多个状态更新统一执行。
查阅资料时看到这篇文章react17+18 中 setState是同步还是异步更新,我明白了原因所在:react18之前,setState仅在react可调度范围内是异步执行的,而then和setTimeout这种范围外的是同步执行的,总是为每次更新进行完整更新流程。而我的公司用的脚手架就属于“之前”。解决方法也很简单,把两个状态合并即可。