Testing Redux toolkit in React / Nextjs application

In brief we will be just using @reduxjs/toolkit and we will be creating a custom Render function which lets you pass store data without mocking useSelector from react-redux or any external libs

This article starts with a quick crash course on redux toolkit with respect to React. Then we also write test for the imaginary react component.

Lets start

To have Redux to any react application, you need to wrap your root App component with Provider.

Below is common app.ts template in a Nextjs application

  • Not having types for the sake of brevity

app.ts

import { Provider } from 'react-redux'
import { store } from '../redux/store'

const App = ({ Component, pageProps }) => {

 return (
  <Provider store={store}>
   <Component {...pageProps} />
  </Provider>
 )
}

Now that we have basic Root App component we also gota have a Store which actually configure the Redux and reducers. aka createStore.

redux/store.ts

import { configureStore } from '@reduxjs/toolkit'
import { userSelector, userStateSlice } from './userStateSlice'

export const reducers = {
  user: userStateSlice.reducer
}

// configureStore helps you createStore with less fuss
export const store = configureStore({
  reducer: reducers
})

export type AppDispatch = typeof store.dispatch
export type RootState = ReturnType<typeof store.getState>

// e.g. to call thunk
// store.dispatch(loadUserAsync())

userStateSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { RootState } from './store'
import { getAuth } from '../auth/getAuth'

interface UserState {
  loaded: boolean
  apiHost?: string
  username?: string
}

const initialState: UserState = { loaded: false }

export const userStateSlice = createSlice({
  name: 'env',
  initialState,
  reducers: {
    loadUser: (state, action: PayloadAction<UserState>) =>
      (state = { ...action.payload, loaded: true })
  }
})

// this internally uses Thunk
// store.dispatch(loadUserAsync())
export const loadUserAsync = () => dispatch =>
  getAuth().then(user => dispatch(userStateSlice.actions.loadUser(user)))

export const userSelector = (state: RootState) => state.env

redux-hooks.ts

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import { AppDispatch, RootState } from './redux'

export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

export const useAppDispatch = () => useDispatch<AppDispatch>()

Now we are in our imaginary React / nextjs component
where we consume Redux store

UserLandingPage component

import { useAppSelector } from '#src/redux/redux-hooks'
import { userSelector } from '#src/redux/userStateSlice'
...

const UserLandingPage = ({isAuthenticated}) => {

  // useAppSelector is just a typescript wrapper around useSelector

  const { user } = useAppSelector(userSelector)

  useEffect(() => {
    if (isAuthenticated && env?.apiHost) {
      fetchUserOrders(env.apiHost)
    }
  }, [isAuthenticated, env])

 return (
  <ContentWrapper>
    ...
  </ContentWrapper>
 )
}

Now the main part, where we write boilerplate test for the above Component

UserLandingPage -> spec.ts

import { renderWithStore } from '#test/render-with-store'

describe('<UserLandingPage>', () => {
 const customInitialState = {
   user: {
    loaded: true,
    apiHost: 'https://localhost:9000'
    username: 'ajinkyax'
   }
 }
 it('renders', async () => {
  const { getAllByRole, getByText } = renderWithStore(<UserLandingPage {...props} />, customInitialState)
  ...
 })
})

renderWithStore

Now the meat of this testing is renderWithStore which allows us to pass an initial store state and also prevents us from passing Provider to render. No more duplication of reducers for testing.

Also saves us from mocking useSelector

render-with-store.tsx

import { configureStore } from '@reduxjs/toolkit'
import { Provider } from 'react-redux'

import { render } from '@testing-library/react'

import { reducers, RootState } from '../src/redux/store'

const testStore = (state: Partial<RootState>) => {
  return configureStore({
    reducer: reducers,
    preloadedState: state
  })
}

export const renderWithStore = (component, initialState) => {
  const Wrapper = ({ children }) => (
    <Provider store={testStore(initialState)}>{children}</Provider>
  )
  return render(component, { wrapper: Wrapper })
}

Hope this was helpful, let me know in the comments if you get stuck anywhere.

28