Stencil.js与RTK Query:避免无限回调问题
问题描述
我正在用RTK Query让Stencil.js组件状态与数据库保持同步,当前实现逻辑如下:
- 组件通过
store.dispatch()调用RTK Query接口获取数据,赋值到本地状态 - 用户修改组件时,同样通过
store.dispatch()发起RTK Query请求
目前只能通过componentWillLoad生命周期订阅store,在回调里调用store.dispatch(api.endpoints.fetchFunction.initiate())触发组件重渲染。虽然能实现状态同步,但会导致fetchFunction()与订阅之间出现无限循环,从console.log可观察到重复调用现象。
希望优化该实现方式,同时想了解:能不能不用store.subscribe()回调,直接通过Redux的createApi获取数据?
附相关代码
RTK Query配置(api.ts)
export const oracleApi = createApi({ reducerPath: 'oracleApi', baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:8000/api/v1/', prepareHeaders: async headers => { try { console.log(await localForage.getItem('CHLJWT')) const token = await getToken(localForage) if (token) { headers.set('Authorization', `CHLJWT ${token.access}`) } console.log('HEADERS Authorization: ', headers.get('Authorization')) return headers } catch (error) { console.error('Login Required: ', error) } }, }), tagTypes: ['Spaces', 'Auth', 'Users', 'Documents', 'Figures', 'Organisations'], endpoints: build => ({ //Auth login: build.mutation<CHLTokenData, CHLLoginData>({ query(body) { return { url: `auth/jwt/create/`, method: 'POST', body, } }, invalidatesTags: [{ type: 'Auth', id: 'LIST' }], }), })
Redux Store配置(store.ts)
export const store = configureStore({ reducer: { [api.reducerPath]: api.reducer, }, middleware: getDefaultMiddleware => getDefaultMiddleware().concat(api.middleware), }) setupListeners(store.dispatch)
API组合文件(index.ts)
export const api = { getSpace: async (id: SpaceId) => { try { const space = await store.dispatch(api.endpoints.getSpace.initiate(id)) return space } catch (error) { console.error(error) } }, getSpaces: async (data?) => { try { const spaces = await store.dispatch(api.endpoints.getSpaces.initiate()) return spaces } catch (error) { console.error(error) } }, deleteSpace: async (id: SpaceId) => { try { await store.dispatch(api.endpoints.deleteSpace.initiate(id)) } catch (error) { console.error(error) } }, createSpace: async data => { try { const res = await store.dispatch(api.endpoints.addSpace.initiate(data)) return res } catch (error) { console.error(error) } }, updateSpace: async (space, data) => { try { const id = space.id const res = await store.dispatch(api.endpoints.updateSpace.initiate({ id, ...data })) return res } catch (error) { console.error(error) } }, }
Stencil.js组件代码
import { store } from 'server_state/store' import { api } from 'server_state/index' @Component({ tag: 'app-topbar', styleUrl: 'app-topbar.css', }) export class AppTopbar { private unsubscribe: () => void @State() space: Space async componentWillLoad() { this.spaceId = Router.activePath.slice(8, 44) as SpaceId this.unsubscribe = store.subscribe(async () => { await this.loadData() }) await this.loadData() } disconnectedCallback() { this.unsubscribe() } async loadData() { try { console.log('Loading data:app-topbar') api.getSpace(this.spaceId) this.space = spaceResult.data } catch (error) { console.error(error) } } render() { /// } }
解决方案
1. 解决无限循环问题
无限循环的核心原因:store每次更新触发订阅回调调用loadData,而loadData里又dispatch请求更新store,再次触发订阅,形成闭环。
优化实现
不要在订阅回调里重复发起请求,而是直接从RTK Query的缓存中读取数据,仅当数据真正变化时更新组件本地状态:
import { store } from 'server_state/store' import { oracleApi } from 'server_state/api' import { Router } from '@stencil/router'; import { Component, State } from '@stencil/core'; @Component({ tag: 'app-topbar', styleUrl: 'app-topbar.css', }) export class AppTopbar { private unsubscribe: () => void; @State() space: Space; private spaceId: SpaceId; async componentWillLoad() { this.spaceId = Router.activePath.slice(8, 44) as SpaceId; // 首次发起请求 await store.dispatch(oracleApi.endpoints.getSpace.initiate(this.spaceId)); // 订阅store变化,仅在数据更新时同步本地状态 this.unsubscribe = store.subscribe(() => { // 从RTK Query缓存中获取目标数据 const spaceData = oracleApi.endpoints.getSpace.select(this.spaceId)(store.getState()); // 避免无意义的重复更新 if (spaceData.data !== this.space) { this.space = spaceData.data; } }); // 初始化本地状态 const initialSpace = oracleApi.endpoints.getSpace.select(this.spaceId)(store.getState()); this.space = initialSpace.data; } disconnectedCallback() { this.unsubscribe(); // 取消RTK Query订阅,避免内存泄漏 store.dispatch(oracleApi.util.resetApiState()); } render() { // 使用this.space渲染内容 } }
2. 无需store.subscribe()的实现方式
完全可以不用手动订阅store,利用RTK Query的自动缓存+标签失效机制实现数据同步,核心是直接通过select方法获取缓存数据,或订阅请求的更新事件:
import { store } from 'server_state/store' import { oracleApi } from 'server_state/api' import { Router } from '@stencil/router'; import { Component, State, Watch } from '@stencil/core'; @Component({ tag: 'app-topbar', styleUrl: 'app-topbar.css', }) export class AppTopbar { @State() space: Space; private spaceId: SpaceId; private getSpaceSubscription: any; async componentWillLoad() { this.spaceId = Router.activePath.slice(8, 44) as SpaceId; // 发起请求并订阅数据更新 this.getSpaceSubscription = store.dispatch(oracleApi.endpoints.getSpace.initiate(this.spaceId)) .unwrap() .then(data => { this.space = data; }); } // 监听spaceId变化(若路径可能动态改变) @Watch('spaceId') async onSpaceIdChange(newId: SpaceId) { // 取消之前的请求订阅 this.getSpaceSubscription?.unsubscribe(); // 发起新请求 this.getSpaceSubscription = store.dispatch(oracleApi.endpoints.getSpace.initiate(newId)) .unwrap() .then(data => { this.space = data; }); } disconnectedCallback() { this.getSpaceSubscription?.unsubscribe(); } render() { // 渲染逻辑 } }
3. 修复API封装的问题
你当前的index.ts封装存在错误:store.dispatch(api.endpoints.getSpace.initiate(id))返回的是QuerySubscription而非Promise,不能直接用await获取数据,需改用unwrap()方法:
// 修改index.ts import { store } from './store'; import { oracleApi } from './api'; export const api = { getSpace: async (id: SpaceId) => { try { const result = store.dispatch(oracleApi.endpoints.getSpace.initiate(id)); // 用unwrap()获取最终数据,自动处理成功/失败 const space = await result.unwrap(); return space; } catch (error) { console.error(error); } }, // 其他API方法同理修改 };
关键注意事项
- RTK Query会自动缓存请求结果,重复发起相同请求不会重复调用API,仅返回缓存数据
- 使用mutation(如
updateSpace)时,只要在mutation配置中设置invalidatesTags,RTK Query会自动重新获取关联的query数据,无需手动触发 - 组件销毁时必须取消RTK Query的订阅,避免内存泄漏
内容的提问来源于stack exchange,提问作者Che Coelho
相关产品推荐
相关产品推荐

