37
loading...
This website collects cookies to deliver better user experience
Games
slice has a state that's basically an object of objects. It allows for an asynchronous fetchGamesSummary
action that calls an external API, and a synchronous updateGameInterest
action.fetchGamesSummary
async thunk is called with a userId
and returns a list of games that looks like this:
{
call_of_duty: {
interest_count: 10,
key: "call_of_duty",
user_is_interested: true,
},
god_of_war: {
interest_count: 15,
key: "god_of_war",
user_is_interested: false,
},
//...
}
updateGameInterest
action is effected by a button toggle, where a user is able to toggle whether they are interested (or not) in a game. This increments/decrements the interestCount
, and toggles the userIsInterested
value between true/false. Note, the camelcase is because it relates to frontend variable. Snake case is what's received from the API endpoint.
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
export const initialStateGames: TStateGames = {
games: {},
}
export const fetchGamesSummary = createAsyncThunk('games/fetch_list', async (userId: string) => {
const response = await gamesService.list(userId)
return response
})
export const gamesSlice = createSlice({
initialState: initialStateGames,
name: 'Games',
reducers: {
updateGameInterest: (state, action: PayloadAction<TUpdateGameInterestAction>) => ({
...state,
games: {
...state.games,
[action.payload.gameKey]: {
...state.games[action.payload.gameKey],
interest_count: state.games[action.payload.gameKey].interest_count + action.payload.interestCount,
user_is_interested: action.payload.userIsInterested,
},
},
}),
},
extraReducers: {
[fetchGamesSummary.fulfilled.type]: (state, action: { payload: TGames }) => {
const games = action.payload
return {
...state,
games,
}
},
},
})
combineReducers
. e.g.export default combineReducers({
games: gamesSlice.reducer,
// your other reducers
})
describe
looks like this:import store from './store'
describe('Games redux state tests', () => {
it('Should initially set games to an empty object', () => {
const state = store.getState().games
expect(state.games).toEqual({})
})
})
store
is where you set up your configureStore
. See the documentation here for more info. getState()
is a method that returns the current state tree, from which I'm particularly interested in the games
slice.ApiClient
class, which I use to set up my base API Axios settings. If you're interested in learning more about this, read my previous blog post on Axios wrappers. In this app, I've defined a getClient()
method within my ApiClient
class that returns an AxiosInstance
.axios-mock-adapter
. There are other packages available, so browse around for whatever works best for you. The MockAdaptor
takes in an Axios instance as an argument, and from there, enables you to mock call your GET endpoint with your defined mock response. Note here that the API endpoint /games/list/?user_id=${userId}
is in effect what my gamesService.list(userId)
calls in my fetchGamesSummary
function above.import ApiClient from '../api/ApiClient'
import MockAdapter from 'axios-mock-adapter'
import store from '../../store'
const userId = 'test123'
const getListResponse = {
game_1: {
interest_count: 0,
key: 'game_1',
user_is_interested: false,
},
}
const apiClient = new ApiClient()
const mockNetworkResponse = () => {
const mock = new MockAdapter(apiClient.getClient())
mock.onGet(`/games/list/?user_id=${userId}`).reply(200, getListResponse)
}
fetchGamesSummary
async action.fulfilled
i.e. matches how I defined my extraReducers
.games
state reflects what I fetched from the API.import ApiClient from '../api/ApiClient'
import MockAdapter from 'axios-mock-adapter'
import store from '../../store'
// import your slice and types
const userId = 'test123'
const getListResponse = {
game_1: {
interest_count: 0,
key: 'game_1',
user_is_interested: false,
},
}
const apiClient = new ApiClient()
const mockNetworkResponse = () => {
const mock = new MockAdapter(apiClient.getClient())
mock.onGet(`/games/list/?user_id=${userId}`).reply(200, getListResponse)
}
describe('Games redux state tests', () => {
beforeAll(() => {
mockNetworkResponse()
})
it('Should be able to fetch the games list for a specific user', async () => {
const result = await store.dispatch(fetchGamesSummary(userId))
const games = result.payload
expect(result.type).toBe('games/fetch_list/fulfilled')
expect(games.game_1).toEqual(getListResponse.game_1)
const state = store.getState().games
expect(state).toEqual({ games })
})
})
beforeAll
block calling the mockNetworkResponse()
(since ultimately, all your tests will be in this one file).fetchGamesSummary
async action to fill out our games
state.updateGameInterest
action.games
state updates the interestCount
and userIsInterested
values correctly.
import ApiClient from '../api/ApiClient'
import MockAdapter from 'axios-mock-adapter'
import store from '../../store'
// import your slice and types
const userId = 'test123'
const getListResponse = {
game_1: {
interest_count: 0,
key: 'game_1',
user_is_interested: false,
},
}
const apiClient = new ApiClient()
const mockNetworkResponse = () => {
const mock = new MockAdapter(apiClient.getClient())
mock.onGet(`/games/list/?user_id=${userId}`).reply(200, getListResponse)
}
describe('Games redux state tests', () => {
beforeAll(() => {
mockNetworkResponse()
})
it('Should be able to toggle interest for a specific game', async () => {
await store.dispatch(fetchGamesSummary(userId))
store.dispatch(
gamesSlice.actions.updateGameInterest({
interestCount: 1,
userIsInterested: true,
gameKey: 'game_1',
}),
)
let state = store.getState().games
expect(state.games.game_1.interest_count).toBe(1)
expect(state.games.game_1.userIsInterest).toBe(true)
store.dispatch(
gamesSlice.actions.updateGameInterest({
interestCount: -1,
userIsInterested: false,
gameKey: 'game_1',
}),
)
state = store.getState().games
expect(state.games.game_1.interest_count).toBe(0)
expect(state.games.game_1.userIsInterest).toBe(false)
})
})
export type TGame = {
interest_count: number,
key: string,
user_is_interested: boolean,
}
export type TGames = { string: TGame } | {}
export type TStateGames = {
games: TGames,
}
export type TUpdateGameInterestAction = {
gameKey: string,
userIsInterested: boolean,
interestCount: number,
}