Status: accepted
The meetings widget needs to keep track a number of different (>10) matrix events (see ADR001). Each event type should be added to a Redux slice with limited complexity.
We will use the RTK Query library to setup the slice, load data, and perform mutations. It will automatically setup the state structure and provide loading events that can be used in other reducers if needed.
RTK Query is an opinionated framework that was designed by the Redux maintainers. It provides a wrapper around CRUD APIs and provides caching, error handling, and loading states out-of-the-box. Originally, it is designed to support HTTP and GraphQL APIs, but it can also be used with other transport types.
We will use createEntityAdapter<StateEvent<_TestEvent_>>()
as a datastructure for each type (learn more).
This will give us convenient helpers to store, access, and mutate data.
- We will create a new API that provides a
get<EventType>
handler for each event (see also no-op query):
import { createEntityAdapter, EntityState } from '@reduxjs/toolkit';
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query/react';
import { TestEvent } from './model';
export const meetingsApi = createApi({
reducerPath: 'meetingsApi',
// don't use a built-in transport
baseQuery: fakeBaseQuery(),
endpoints: (builder) => ({
// create a getter for test events.
// <void> is the endpoint parameter.
getTestEvent: builder.query<EntityState<StateEvent<TestEvent>>, void>({
// use queryFn to execute custom logic
queryFn() {
/* TODO */
},
}),
// ...
}),
});
-
We will call the
WidgetApi
in thequeryFn
to receive the initial state (learn more):// ... getTestEvent: builder.query<State<TestEvent>[], void>({ async queryFn(_, { extra }) { // the API is provided by the redux thunk middleware const { widgetApi } = extra as ThunkExtraArgument; const events = await widgetApi.receiveStateEvents( 'my.state.event' ); const data = entityAdapter.upsertMany( entityAdapter.getInitialState(), // let's discard invalid events events.filter(validateTestStateEvent) ); return { data }; }, }),
-
We will use
onCacheEntryAdded
to keep the events updated (learn more):// ... getTestEvent: builder.query<State<TestEvent>[], void>({ async queryFn(_, { extra }) { /* ... */ }, async onCacheEntryAdded( _, { cacheDataLoaded, cacheEntryRemoved, extra, updateCachedData } ) { const { widgetApi } = extra as ThunkExtraArgument; // wait until first data is cached await cacheDataLoaded; const subscription = widgetApi .observeStateEvents('my.state.event') .pipe(filter(validateTestStateEvent)) .subscribe((event) => { // update the state with the new event. updateCachedData((state) => { entityAdapter.upsertMany(state, events); }); }); // wait until subscription is cancelled await cacheEntryRemoved; subscription.unsubscribe(); } }),
The
bufferTime
operator could batch incoming events to e.g. 100ms windows. -
We will subscribe to the endpoint when the application is started:
let subscription = dispatch(meetingsApi.endpoints.getTestEvent.initiate()); await subscription; // ... on application shutdown ... subscription.unsubscribe();
RTK Query provides hooks to access data:
const { data, isLoading } = useGetRoomMembersQuery();
It also provides selectors that can be used to compose different events together.
We will provide a select[All]Meetings(state)
selector that will compose each individual event state and provide a usable state to all the components.
const allMeetings = useAppSeletor(selectAllMeetings);
We will use RTK Query to do mutations.
They are created similar to queries:
export const meetingsApi = createApi({
reducerPath: 'meetingsApi',
baseQuery: fakeBaseQuery(),
endpoints: (builder) => ({
createTest: builder.mutation<RoomEvent<unknown>, TestOptions>({
// create a mutation.
// TestOptions describes the format of opts
queryFn: async (opts, { extra }) => {
// the API is provided by the redux thunk middleware
const { widgetApi } = extra as ThunkExtraArgument;
const event = await widgetApi.sendRoomEvent('my.room.event', opts);
return { data: event };
},
}),
// ...
}),
});
They are used with hooks:
const [createTest, { data: createTestResult }] = useCreateMeetingMutation();
// await also returns the result
const result = await createTest({ my: 'content' }).unwrap();
The hook also provides status data for the current component:
const [createTest, { isError, isLoading: isCreating }] =
useUpdateMeetingWidgetsMutation();
It is also possible to react to the the matchFulfilled
, matchPending
, or matchRejected
actions of each query or mutation in other slices.
But this feature won't be used in the meetings widget.