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.

    36

    This website collects cookies to deliver better user experience

    Testing Redux toolkit in React / Nextjs application