Realtime database with Redux Toolkit [Typescript]

h1kigaeru
4 min readMar 30, 2021

Realtime database from Firebase is very convenient, because all you need to do is subscribe to a collection and it will listen for all changes, passing to you the updated state. Redux toolkit is another convenient tool with a magic createAsyncThunk that’ll make working with asynchronous code a bliss. How lucky we are? Everything’s been done for us and now we can simply fill the templates. I’d love if everything was that easy. However, createAsyncThunk isn’t designed to work properly with such case as ours and I want to introduce the way how I implement the needed code.

First of all, let’s create a component which will render data:

./app

export interface Blog {  title: string;  content: string;}function App() { const { blogs, loading, error } = useSelector(listSelector); const dispatch = useDispatch();  useEffect(() => {  dispatch(subscribeBlogs());}, []);  const createBlog = () => {    const title = faker.commerce.product();    const content = faker.commerce.productDescription();    dispatch(addBlog({      title,      content    }));
};
if (error) return <div>Something went wrong...</div>;return loading ? ( <div>Loading...</div>) : ( <div> <h1>Blogs</h1> <span onClick={createBlog}>Add blog</span> {blogs.map(({ title, content }, i) => ( <div key={i}> <h4>{title}</h4> <p>{content}</p> </div> ))} </div> );};export default App;

Basically, this project renders all blogs from the store and creates a new blog when we click an appropriate element. In this example I use faker for creating fake data. You can replace those values with any data.

Next, we’re going to set our store:

./store/index.ts

import blogsReducer from './testSlice';
export const rootReducer = combineReducers({ blogsReducer,});
const store = configureStore({ reducer: rootReducer,});
export type AppState = ReturnType<typeof rootReducer>;export default store;

Nothing new and you should be familiar with everything. I just want to point out that AppState’s function is bringing an easy access to the state with selector. You’ll this in action-creators.ts.

There was nothing so special so far but now we’re going to see that configuration for which we all have gathered here. In the store folder we create following file:

./store/types.ts

export const PENDING = 'PENDING';export const FULFILLED = 'FULFILLED';export const REJECTED = 'REJECTED';
export interface LoadingState { loading: boolean; error: Error | null;}interface LoadingActions<ActionState> { pending: ActionCreatorWithoutPayload; fulfilled: ActionCreatorWithPayload<ActionState, string>; rejected: ActionCreatorWithPayload<Error, string>;}
const createLoadingActions = <ActionState>(prefix: string) => { return { pending: createAction(`${prefix}/${PENDING}`), fulfilled: createAction(`${prefix}/${FULFILLED}`), rejected: createAction(`${prefix}/${REJECTED}`), } as LoadingActions<ActionState>;};export const fetchLoadingActions = createLoadingActions<Blog[]>('fetchLists');

In this file I have attempted to simulate createAsyncThunk function and get all its main advantages. According to original function, it should dispatch pending, fulfilled and rejected and a user should be able to handle those dispatches, accessing state they supply. And for this reason, createLoadingActions was implemented as a helper function to create appropriate actions and delegate provided state with a unique prefix. createLoadingActions creates those kinds of actions for manipulating our blogs. Their usage is shown in the following samples:

./store/action-creators

import { Blog } from ‘../App’;import { fetchLoadingActions } from ‘./types’;
export const subscribeBlogs = () => { return async (dispatch: Dispatch<PayloadAction<any>>) => { dispatch(fetchLoadingActions.pending());
db.collection(‘blogs’).onSnapshot(querySnapshot => { const blogs = querySnapshot.docs.map(doc => doc.data()) as Blog[];
dispatch(fetchLoadingActions.fulfilled(blogs)); };};
export const addBlog = (blog: Blog) => { return async (dispatch: Dispatch<any>) => { dispatch(fetchLoadingActions.pending()); try { db.collection('blogs').add(blog); } catch (err) { const error = err as Error; dispatch( fetchLoadingActions.rejected({ name: 'Firebase', message: error.message, }) ); } };};

In this example we simply subscribe to the collection in our database and invoke our fetch pending action on init and fetch fulfilled action on success or rejected on fail as it’s shown in addBlog.

db is a simple reference to firestore:

import firebase from ‘firebase/app’;export const firestore = firebase.firestore;

As thus, we need to handle the state changes:

./store/blogSlice

import { Blog } from ‘../App’;import { AppState } from ‘./store’;import { fetchLoadingActions, LoadingState } from ‘./types’;interface State extends LoadingState {  blogs: Blog[];}const initialState: State = {  loading: true,  error: null,  blogs: [],};const blogSlice = createSlice({  initialState,  name: ‘blog slice’,  reducers: {},  extraReducers: builder => {      builder.addCase(fetchLoadingActions.pending, state => {      state.loading = true;});    builder.addCase(fetchLoadingActions.fulfilled, (state, { payload }) => {      state.blogs = payload.blogs;      state.loading = false;});    builder.addCase(fetchLoadingActions.rejected, (state, { payload }) => {      state.error = payload;    });  },});
export default blogSlice.reducer;export const listSelector = (state: AppState) => state.blogsReducer;

At the end we have a completely ordinary slice, listening to previously created actions (fetchLoadingActions) with only difference that Error is called as payload.

To sum up you can handle unsubscribe and probably create a helper for adding/removing/modifying collection as it happens in addBlog to avoid recoding the same parts. But that’s not an objective of the current article and I’m assured you’ll figure this out. I hope you’ll find this approach useful or it’ll help you coming up with a better one! Thank you for reading!

--

--

h1kigaeru
0 Followers

A rook_ie craving to become a bishop.