0

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!

Che Coelho
  • 11
  • 2

1 Answers1

1

This really divides into 3 questions:

  • How does a Redux store announce it's been updated?
  • How do UI components know if the Redux store is updated and if this component needs to update?
  • How does Stencil specifically interact with a Redux store?

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 :

// 1) Create a new Redux store
const store = configureStore({reducer: counterReducer})

// 2) Subscribe to redraw whenever the data changes in the future
store.subscribe(render)

// Our "user interface" is some text in a single HTML element
const valueEl = document.getElementById('value')

// 3) When the subscription callback runs:
function render() {
  // 3.1) Get the current store state
  const state = store.getState()
  // 3.2) Extract the data you want
  const newValue = state.value.toString()

  // 3.3) Update the UI with the new value
  valueEl.innerHTML = newValue
}

// 4) Display the UI with the initial store state
render()

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 useSelector hook (and the older connect wrapper). RTK Query's React hooks like useGetPokemonQuery build 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/redux package.

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 wrapper mapStateToProps to select that data from the store. Assuming that @stencil/redux does what I think it does, that should trigger an update in your component.

markerikson
  • 63,178
  • 10
  • 141
  • 157