嵌套QueryRenderer引发父渲染器使用内部查询数据重渲染致字段缺失问题
咱先把你遇到的问题捋清楚:外层的Wrapper组件是个QueryRenderer,查了product的id和name;内层的Inner组件也是个QueryRenderer,只查了product的id。结果页面加载时,Wrapper会先用上Inner查询返回的不完整数据(缺name)重渲染,而且因为Wrapper订阅了product的更新,后续只要Inner触发了数据变化,Wrapper也会跟着更新,但数据不全就会出问题对吧?
这本质是Relay缓存复用和订阅机制的特性导致的——Relay会优先从本地缓存取数据,要是缓存里只有Inner查的id,Wrapper就会先拿到这个不完整的结果,等完整数据回来才会再更一次;同时Wrapper作为订阅者,只要product有变动(哪怕是Inner触发的),它都会重新渲染,这时候如果缓存数据不全就会踩坑。
下面给你几个实用的解决办法:
1. 统一查询字段,从根源避免数据缺失
最简单的方式就是让Inner的查询包含Wrapper需要的所有字段,或者反过来让Wrapper的查询覆盖Inner的需求。比如把Inner的查询改成这样:
query productInnerQuery { product { id name # 补上Wrapper需要的name字段 } }
这样不管哪个QueryRenderer先返回数据,本地缓存里的product都是完整的,Wrapper拿到的就不会有name未定义的情况。
2. 用Fragment共享数据,抛弃多个顶层QueryRenderer
其实Relay的最佳实践是用Fragment来组合组件,而不是搞多个顶层的QueryRenderer。你可以把Wrapper作为唯一的顶层QueryRenderer,请求完整的product数据,然后通过props把Fragment传递给Inner:
- Wrapper的查询:
query productQuery { product { id name ...Inner_product # 引用Inner的Fragment } }
- Inner的Fragment定义:
fragment Inner_product on Product { id }
这样整个页面只有一个顶层数据请求,数据是完整的,Inner只取自己需要的字段,既不会出现缓存数据不全的问题,订阅也由Wrapper统一处理,完全不会有冲突。
3. 给Wrapper加数据完整性校验,避免提前渲染
如果必须保留两个QueryRenderer,可以在Wrapper的渲染逻辑里先判断数据是否完整,不完整就显示加载状态:
function Wrapper() { return ( <QueryRenderer query={productQuery} render={({ error, props }) => { if (error) return <div>出错啦!</div>; // 校验product的name是否存在,不存在就显示加载中 if (!props?.product?.name) return <div>加载中...</div>; return <div>商品名称:{props.product.name}</div>; }} /> ); }
这样就算缓存里只有id,Wrapper也不会用不完整的数据渲染,等完整数据回来后再显示正确内容。
4. 调整fetchPolicy,强制从网络拿完整数据
你还可以给Wrapper的QueryRenderer设置fetchPolicy为'network-only',强制它跳过缓存直接从网络获取完整数据:
<QueryRenderer query={productQuery} fetchPolicy="network-only" render={/* 你的渲染逻辑 */} />
不过这个方式会增加额外的网络请求,可能影响页面加载性能,除非万不得已不推荐用。
总的来说,最推荐的还是用Fragment组合组件的方式,既符合Relay的设计理念,又能彻底解决这类数据冲突问题。
内容的提问来源于stack exchange,提问作者jouerai

