I am trying to decide if I am using RTKQuery to keep my Stencil.js components' state in sync with the DB correctly.
The behaviour I have is that my component will fetch the data using RTK query and store.dispatch() and assign it to its local state. Then the user mutates the component, which is also a request made using the rtk query api via a store dispatch() function.
The only way I have managed to get my component to rerender is by using the componentWIllLoad() lifecycle mthod to subscribe to the store and pass in a fetch function store.dispatch(api.endpoints.fetchFunction.initiate()) as the callback.
While this keeps the state in sync very well, it does cause an infinite invocation cycle between the fetchFunction() which is dispatched as an action and invokes the subscription and which invokes the fetchFunction() and so on. This can be seen with a simple console.log() statement in the subsciption.
While this behaviour is not the end of the world, it does not feel very elegant. Can it be improved upon?
RTK Query setup: I have an API:
- 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' }],
}),
a redux store:
- store.ts
export const store = configureStore({
reducer: {
// Add the generated reducer as a specific top-level slice
[api.reducerPath]: api.reducer,
},
// Adding the api middleware enables caching, invalidation, polling,
// and other useful features of `rtk-query`.
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(api.middleware),
})
// optional, but required for refetchOnFocus/refetchOnReconnect behaviors
// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization
setupListeners(store.dispatch)
and an index.ts file to combine them
-index.ts
export const api = {
//Spaces
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)
}
},
}
Finally, I have a stencil.js component
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() {
///
}
}
Along with improving this pattern, I would specifically be interested in whether it is a possible to use the createApi from redux to fetch data without invoking the store.subscribe() callback.
Thanks!
This really divides into 3 questions:
For the first two topics, see the long explanations in my blog post The History and Implementation of React-Redux and talk A Deep Dive into React-Redux, but the TL;DR can be seen in our docs at https://redux.js.org/tutorials/fundamentals/part-5-ui-react#integrating-redux-with-a-ui :
Every UI layer that integrates with Redux needs to do the same basic operations: subscribe, get latest state, diff values needed by this component, force a re-draw if those values changed and an update necessary.
For React, we have all that logic encapsulated in the React-Redux package and our
useSelectorhook (and the olderconnectwrapper). RTK Query's React hooks likeuseGetPokemonQuerybuild on top of that.For Stencil, you'd need to start by using whatever the equivalent of React-Redux is. I see that there's already a Stencil docs page that talks about using Redux, at https://stenciljs.com/docs/stencil-redux , and that there's a
@stencil/reduxpackage.Like the rest of Redux and RTK, RTK Query is also UI-agnostic. So, you can use it without React, but you do have to do some more work.
We cover some of the key bits here in the docs:
In this case, you'll probably want to generate an endpoint selector along the lines of
const selector = api.endpoints.getPokemon.select("pikachu"), and pass that to the Stencil wrappermapStateToPropsto select that data from the store. Assuming that@stencil/reduxdoes what I think it does, that should trigger an update in your component.