Redux Toolkit: How to test actions with uid prepare callback - redux

In the docs for testing incrementing todo ids, this assumes a predictable response.
In an example such as below, a unique id is generated.
How could this be tested?
This test passes, but I'm not sure if it's correct, shouldn't the id be defined based on what's in the prepare callback?
slice.js
add: {
reducer: (state, {payload}: PayloadAction<{id: string, item: Item}>) => {
state[payload.id] = payload.item
},
prepare: (item: Item) => ({
payload: {id: cuid(), item}
})
}
slice.test.js
it('should handle add', () => {
expect(
reducer(
{},
{
type: actions.add,
payload: {
id: 'id-here?',
item: {
other: 'properties...'
}
},
}
)
).toEqual({
'id-here?': {
other: 'properties...'
},
})
})

You can pull out the prepare function and also the reducer function into it's own constant and then test prepare in isolation:
todosSlice.js:
[...]
let nextTodoId = 0;
export const addTodoPrepare = (text) => {
return {
payload: {
text,
id: nextTodoId++
}
}
}
export const addTodoReducer = (state,
action) => {
const {id, text} = action.payload;
state.push({
id,
text,
completed: false
});
};
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: {
prepare: addTodoPrepare,
reducer: addTodoReducer,
},
}
})
[...]
todosSlice.spec.js:
import todos, {addTodo, addTodoPrepare} from './todosSlice'
describe('addTodoPrepare',
() => {
it('should generate incrementing IDs',
() => {
const action1 = addTodoPrepare('a');
const action2 = addTodoPrepare('b');
expect(action1.payload).toEqual({
id: 0,
text: 'a'
})
expect(action2.payload).toEqual({
id: 1,
text: 'b'
})
})
})
describe('todos reducer',
() => {
[...]
})

For unit testing, NO, just test each reducer independently.
For integration testing and e2e testing, Yes.

Related

How to mutation store state in build query redux toolkit

Created an initialState and will be updated the totalPage and currentPage after got the users list.
I found out onQueryStarted from docs, it able to update the store state in this method but only look like only for builder.mutation.
what's the correct way to get the user list and update the store page value in redux toolkit?
Listing two part of the code below:
apiSlice
component to use the hook
// 1. apiSlice
const usersAdapter = createEntityAdapter({})
export const initialState = usersAdapter.getInitialState({
totalPage: 0,
currentPage: 0,
})
export const usersApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query({ // <--- the docs are using builder.mutation, but i needed to pass params
query: (args) => {
const { page, limit } = args;
return {
url: `/api/users`,
method: "GET",
params: { page, limit },
}
},
validateStatus: (response, result) => {
return response.status === 200 && !result.isError
},
transformResponse: (responseData) => { // <<-- return { totalPages: 10, currentPage: 1, users: [{}] }
const loadedUsers = responseData?.users.map((user) => user)
return usersAdapter.setAll(initialState, loadedUsers)
},
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled
const {totalPages, currentPage} = data; <----- totalPages & currentPage values are still 0 as initialState
dispatch(setPages({ currentPage, totalPages }))
} catch (error) {
console.error("User Error: ", error)
}
},
providesTags: (result, error, arg) => {
if (result?.ids) {
return [
{ type: "User", id: "LIST" },
...result.ids.map((id) => ({ type: "User", id })),
]
} else return [{ type: "User", id: "LIST" }]
},
})
})
});
export const {
useGetUsersQuery,
} = usersApiSlice
component to use the hook
Try to use the hook in user landing page
const UsersList = () => {
const { data: users, isLoading, isSuccess, isError } = useGetUsersQuery({page: 1, limit: 10 })
return (
<div>return the users data</div>
)
}
update the store value after get the data return

Next js Redux, Objects are not valid as a React child

Error: Objects are not valid as a React child (found: object with keys {_id, name}). If you meant to render a collection of children, use an array instead.
Tried to fix this for days and no result.
i have a model
import mongoose from 'mongoose'
const CategoriesSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
parent: {
type: mongoose.Types.ObjectId,
ref: 'categories'
},
},
{
timestamps: true
})
let Dataset = mongoose.models.categories || mongoose.model('categories', CategoriesSchema)
export default Dataset
and i have getCategories like this
[getCategories ]
const getCategories = async (req, res) => {
try {
const categories = await Categories.find().populate("parent", "name");
res.json({ categories });
}
catch (err)
{
return res.status(500).json({ err: err.message });
}
};
in my Globale state i have
export const DataContext = createContext()
export const DataProvider = ({children}) => {
const initialState = {
notify: {}, auth: {}, cart: [], modal: [], orders: [], users: [], categories: []
}
const [state, dispatch] = useReducer(reducers, initialState)
useEffect(() => {
getData('categories').then(res => {
if(res.err)
return dispatch({type: 'NOTIFY', payload: {error: res.err}})
dispatch({ type: 'ADD_CATEGORIES', payload: res.categories })
})
},[])
return(
<DataContext.Provider value={{state, dispatch}}>
{children}
</DataContext.Provider>
)
}
when i call categories throw:exception
when i change dispatch in Globale state like :
dispatch({ type: 'ADD_CATEGORIES', payload: [] })
i get no elements in array :

How to add cases in ExtraReducer to match the actions created in currentReducer using createSlice() from #reduxjs/toolkit

Here below I have mentioned a redux slice. A fetchAllApps thunk function is created with createAsyncThunk for action 'allApps/allappsAdded/' which I dynamically got by allAppsAdded.type. When the fetchAllapps is dispatched it generated actions of type 'allApps/allappsAdded/pending', 'allApps/allappsAdded/fulfilled', 'allApps/allappsAdded/rejected' which I need to add in extraReducers to handle it by doing hardcode.Is there any way to add these action types like allAppsAdded.type programatically?. so that in future It makes easy for me to change these without redundant..
import {
configureStore,
createAsyncThunk,
createSlice
} from "#reduxjs/toolkit";
const initialState = {
apps: [],
categories: [],
loading: {
apps: false
}
};
const allappsSlice = createSlice({
name: "allapps",
initialState,
reducers: {
allappsAdded: (state, action) => {
state["apps"] = action.payload.apps;
state["categories"] = action.payload.categories;
}
},
extraReducers: {
}
});
export default () =>
configureStore({
reducer: allappsSlice.reducer
});
const { allappsAdded } = allappsSlice.actions;
const fetchAllApps = createAsyncThunk(allappsAdded.type, async () => {
console.log("ss");
setTimeout(() => ({ apps: [], categories: [] }), 2000);
});
export { allappsAdded, fetchAllApps };

Redux action payload being ignored when dispatching some other action inside axios interceptor

I need to call checkConnection before any other action so I thought of using axios interceptors:
axios.interceptors.request.use(
async config => {
await store.dispatch(checkConnection())
const { requestTime, hash } = intro(store.getState())
return {
...config,
headers: {
'Request-Time': requestTime,
'Api-Hash-Key': hash
}
}
}
)
intro is a reselect selector used to do some 'heavy' computing on serverTime (serverTime is the result of checkConnection)
checkConnection is a redux thunk action:
export const checkConnection = () => async (dispatch, _, {
Intro
}) => {
dispatch(introConnectionPending())
try {
const { data: { serverTime } } = await Intro.checkConnection()
dispatch(introConnectionSuccess(serverTime))
} catch ({ message }) {
dispatch(introConnectionFailure(message))
}
}
So, now every time I dispatch an action that calls for an API the checkConnection runs first.
The problem is when the reducer responsible for type that main action dispatched (not the checkConnection) gets called it doesn't even see the payload.
Here is an example of a action:
export const getData = () => async (dispatch, getState, {
API
}) => {
dispatch(dataPending())
const credentials = getUsernamePasswordCombo(getState())
try {
const { data } = await API.login(credentials)
dispatch(dataSuccess(data))
} catch ({ message }) {
dispatch(dataFailure())
}
}
and its reducer:
export default typeToReducer({
[SET_DATA]: {
PENDING: state => ({
...state,
isPending: true
})
},
SUCCESS: (state, { payload: { data } }) => ({
...state,
isPending: false,
...data
}),
FAILURE: state => ({
...state,
isPending: false,
isError: true
})
}, initialValue)
The reducer is totally wrong. It should be:
export default typeToReducer({
[SET_DATA]: {
PENDING: state => ({
...state,
isPending: true
}),
SUCCESS: (state, { payload: { data } }) => ({
...state,
isPending: false,
...data
}),
FAILURE: state => ({
...state,
isPending: false,
isError: true
})
}
}, initialValue)
Note the SUCCESS and FAILURE parts are now inside [SET_DATA]

Action creators handling axios get.request with state access for param

I'm trying to pass some value from a component to a action creators which is doing a get request with axios. I'm trying to follow this pattern from Dan Abramov :
export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
return (dispatch, getState) => {
const {items} = getState().otherReducer;
dispatch(anotherAction(items));
}
}
However I can't make it work. I think I have trouble on two level : my component and my action creator. Would be great to have some helps.
my component :
const timeR = ({
selectedTimeRange,
timeRange = [],
onTimeChange }) => {
return (
<div>
<div>
Filters:
<div>
Year:
<select
defaultValue={selectedTimeRange}
onChange={onTimeChange}>
<option value="all" >All</option>
{timeRange.map((y, i) =>
<option key={i} value={y}>{y}</option>
)}
</select>
</div>
</div>
</div>
);
}
function mapStateToProps(state) {
var range = ['30daysAgo', '15daysAgo', '7daysAgo'];
return {
selectedTimeRange: state.timeReducer.timerange[0],
timeRange: range
};
};
const mapDispachToProps = (dispatch) => {
return {
onTimeChange: (e) => {dispatch (onSetTimeRange(e.target.value));},
};
};
const TimeRange = connect(mapStateToProps, mapDispachToProps)(timeR);
export default TimeRange;
This component give me a dropdown menu. When selecting a timerange, for example '30daysAgo', it update my redux store state so I can access the value from my reducer.
Here is the action associated to my dropdown menu :
export function onSetTimeRange(timerange) {
return {
type: 'SET_TIME_RANGE',
timerange
}
}
and here is the action dealing with axios.get :
export const fetchgadata = () => (dispatch) => {
dispatch({
type: 'FETCH_DATA_REQUEST',
isFetching:true,
error:null
});
var VIEW_ID = "ga:80820965";
return axios.get("http://localhost:3000/gadata", {
params: {
id: VIEW_ID
}
}).then(response => {
dispatch({
type: 'FETCH_DATA_SUCCESS',
isFetching: false,
data: response.data.rows.map( ([x, y]) => ({ x, y }) )
});
})
.catch(err => {
dispatch({
type: 'FETCH_DATA_FAILURE',
isFetching:false,
error:err
});
console.error("Failure: ", err);
});
};
My question :
How do I bring these two actions together. At the end I would like to be able, when doing onChange on my drop-down menu, to call a action with the value selected from my menu as a param for my axios.get request.
I feel like I need to nest two actions creators. I've tried this but doesn't work ("fetchgadata" is read-only error in my terminal)
export const SET_TIME_RANGE = 'SET_TIME_RANGE';
export function onSetTimeRange() {
return (dispatch, getState) => {
const {VIEW_ID} = getState().timerange;
dispatch(fetchgadata = (VIEW_ID) => (dispatch) => {
dispatch({
type: 'FETCH_DATA_REQUEST',
isFetching:true,
error:null,
id:VIEW_ID,
});
});
return axios.get("http://localhost:3000/gadata", {
params: {
id: VIEW_ID
}
}).then(response => {
dispatch({
type: 'FETCH_DATA_SUCCESS',
isFetching: false,
data: response.data.rows.map( ([x, y]) => ({ x, y }) )
});
})
.catch(err => {
dispatch({
ype: 'FETCH_DATA_FAILURE',
isFetching:false,
error:err
});
console.error("Failure: ", err);
});
}
}
Edit:
reducers for API call :
const initialState = {data:null,isFetching: false,error:null};
export const gaData = (state = initialState, action)=>{
switch (action.type) {
case 'FETCH_DATA_REQUEST':
case 'FETCH_DATA_FAILURE':
return { ...state, isFetching: action.isFetching, error: action.error };
case 'FETCH_DATA_SUCCESS':
return Object.assign({}, state, {data: action.data, isFetching: action.isFetching,
error: null });
default:return state;
}
};
reducers for Drop-down :
const items = [{timerange: '30daysAgo'},{timerange: '15daysAgo'},{timerange: '7daysAgo'}]
const timeReducer = (state = {
timerange: items
}, action) => {
switch (action.type) {
case 'SET_TIME_RANGE':
console.log(state,action);
return {
...state,
timerange: action.timerange,
};
default:
return state;
}
}
I see a little typo in the catch of your axios.get request, it reads ype: FETCH_DATA_FAILURE. Otherwise, can you add in your reducer for me, I don't see it up there? If I understand correctly, you want one action to update two different pieces of state, in which case you would simply dispatch an action and add it to both reducers. Really it's best to just demonstrate:
//axios call
axios.get("some route", { some params } )
.then(response => {
dispatch({
type: UPDATE_TWO_THINGS,
payload: some_value
})
}) .... catch, etc
//reducer 1
import { UPDATE_TWO_THINGS } from 'types';
const INITIAL_STATE = { userInfo: '' };
export default function (state = INITIAL_STATE, action) {
switch(action.type) {
case UPDATE_TWO_THINGS:
return {...state, userInfo: payload };
}
return state;
}
//reducer 2
import { UPDATE_TWO_THINGS } from 'types';
const INITIAL_STATE = { businessInfo: '' };
export default function (state = INITIAL_STATE, action) {
switch(action.type) {
case UPDATE_TWO_THINGS:
return {...state, businessInfo: payload };
}
return state;
}
Hopefully this helps, but let me know if not, I'll do my best to get this working with you! Thanks for asking!

Resources