DEV Community

Cover image for Testing Redux toolkit in React / Nextjs application
Ajinkya Borade
Ajinkya Borade

Posted on • Updated on

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>
 )
}

Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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>()

Enter fullscreen mode Exit fullscreen mode

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>
 )
}

Enter fullscreen mode Exit fullscreen mode

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)
  ...
 })
})

Enter fullscreen mode Exit fullscreen mode

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 })
}

Enter fullscreen mode Exit fullscreen mode

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

Discussion (3)

Collapse
shahzad6077 profile image
Muhammad Shahzad Ali

Thanks @ajinkyax

Collapse
ajinkyax profile image
Ajinkya Borade Author

Glad it helped you :)

Collapse
mallikarjunamatla profile image
Mallikarjuna M

After all here I'm...You saved me.. Thanks.