You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 19:48:17