issues on redux toolkit EntitiyAdapter - reactjs

I created a website and use redux-toolkit
but I have an issue with createSlice. my two records data receive correctly. but when I set data into adapter just the first record was added. this is my slice code
import { createEntityAdapter, createSlice, PayloadAction, createSelector, createAsyncThunk } from "#reduxjs/toolkit";
import { fetchTop10Podcasts } from "../../../services/endpoints/Podcasts";
import { podcastState } from "../../initialStates/Podcasts";
const podcastAdapter = createEntityAdapter();
// interface podcastState {
// entities: Array<Podcast>
// loading: 'idle' | 'loading' | 'successed' | 'failed'
// }
export const PodcastSlice = createSlice({
name: 'podcasts',
initialState: podcastAdapter.getInitialState({
entities: [],
loading: 'idle'
} as podcastState),
reducers: {
}, extraReducers: (builder) => {
builder.addCase(fetchTop10Podcasts.pending, (state, action) => {
state.loading = 'loading';
}).addCase(fetchTop10Podcasts.fulfilled, (state, action) => {
state.loading = 'successed';
podcastAdapter.setAll(state, action.payload.data.podcasts)
console.log('podcasts', action.payload.data.podcasts); // here I recieved two record
}).addCase(fetchTop10Podcasts.rejected, (state, action) => {
state.loading = 'failed';
})
}
});
export const {
selectById: selectPodcastById,
selectAll: selectPodcasts
} = podcastAdapter.getSelectors((state: any) => state.podcasts)
export const selectPodcastIds = createSelector(
selectPodcasts,
podcasts => podcasts.map((podcast: any) => podcast._id)
)
export default PodcastSlice.reducer;

I think the issue may be with your initial state.
When you call adapter.getInitialState(), the state structure it returns is {ids: [], entities: {}}, plus whatever additional fields you passed in.
You're providing entities: [] as an additional field. It's likely you're overwriting the original object structure as a result.
Remove that line and I think this will work.

Related

How to store search criteria param redux toolkit

I am fetching journeys base on search criteria. I want to store those criteria in store.
I tied to access action.payload in extrareducer pending but it is not working. May I know how to store the criteria param into store.
import { createSlice, createAsyncThunk, PayloadAction } from '#reduxjs/toolkit'
import axios from "axios"
import { Journey, SearchCriteria } from '../types'
import constants from "../constants";
type InitialState = {
loading: boolean
journeys: Journey[]
criteria: SearchCriteria | null
error: string
}
const initialState: InitialState = {
loading: false,
journeys: [],
criteria: null,
error: '',
};
// generates pending, fulfilled and rejected action types
export const fetchJourneys = createAsyncThunk("user/fetchUsers", async (criteria: SearchCriteria) => {
const response = await axios.get(`${constants.api_server}/journeys?from=${criteria.from}&to=${criteria.to}&departure=${criteria.departure}`);
return response.data.journeys;
});
const journeySlice = createSlice({
name: "journey",
initialState,
reducers: {},
extraReducers: builder => {
builder.addCase(fetchJourneys.pending, (state, action) => {
console.log(action)
state.loading = true
})
builder.addCase(fetchJourneys.fulfilled, (state, action: PayloadAction<Journey[]>) => {
state.loading = false
state.journeys = action.payload
state.error = ''
})
builder.addCase(fetchJourneys.rejected, (state, action) => {
state.loading = false,
state.journeys = [],
state.error = action.error.message || 'Someting went wrong'
})
}
});
export default journeySlice.reducer

How to create a strongly typed generic function for redux slices and their actions?

I am using #reduxjs/toolkit and want to create an easily extendible function that creates a slice with default reducers. The implementation that I have now works but is not strongly typed. How can I create a function so that the slice's actions type contains not only the default reducers but also the ones that are passed in? I have tried using inference types but could not get it to work.
Any guidance would be appreciated. Thanks.
Minimum example:
in common.ts file (where logic can be shared between slices)
export interface StoreState<T> {
data: T
status: 'succeeded' | 'failed' | 'idle'
error: string | null
}
// create a slice given a name and make it possible to extend reducers so they include more than just reset and updateStatus
export const createStoreSlice = <T>(props: {
sliceName: string
defaultState: T
reducers?: SliceCaseReducers<StoreState<T>> // <-- want to infer this in slices/<sliceName>.ts
}) => {
const { sliceName, reducers, defaultState } = props
const initialState: StoreState<T> = {
data: defaultState,
status: 'idle',
error: null,
}
return createSlice({
name: sliceName,
initialState,
reducers: {
...reducers, // <--- want to somehow infer the type of this when exporting slice actions
reset: (state) => {
Object.assign(state, initialState)
},
updateStatus: (state, action) => {
state.status = action.payload
},
},
})
}
in slices/<sliceName>.ts (specific slices with extra logic)
export const genericSlice = createStoreSlice({
sliceName: 'someSliceName',
defaultState: { someField: 'some value' },
reducers: {
setSomeField: (state, action) => {
const { payload } = action
state.data.someField = payload
},
},
})
// these actions should be strongly typed from the createStoreSlice function parameters and contain the default reducers (eg. reset, updateStatus) and extra ones specific to the slice (eg. setSomeField)
export const { reset, updateStatus, setSomeField } = genericSlice.actions
You were really close but there were three parts you were missing
You must infer the type of reducers with a generic type
You must use the generic reducers type (i.e. R) along with the provided ValidateSliceCaseReducers type to determine the resulting type for reducers.
Make reducers type required. I was only able to make it work with required reducers but there may be a way around this but unlikely as the types come from #reduxjs/toolkit.
Note: I just pulled out the createStoreSlice props into the StoreSliceProps type to make it easier to read.
import { SliceCaseReducers, createSlice, ValidateSliceCaseReducers } from '#reduxjs/toolkit';
export interface StoreState<T> {
data: T;
status: 'succeeded' | 'failed' | 'idle';
error: string | null;
}
interface StoreSliceProps<T, R extends SliceCaseReducers<StoreState<T>>> {
sliceName: string;
defaultState: T;
reducers: ValidateSliceCaseReducers<StoreState<T>, R>;
}
export function createStoreSlice<T, R extends SliceCaseReducers<StoreState<T>>>(props: StoreSliceProps<T, R>) {
const { sliceName, reducers, defaultState } = props;
const initialState: StoreState<T> = {
data: defaultState,
status: 'idle',
error: null,
};
return createSlice({
name: sliceName,
initialState,
reducers: {
...reducers,
reset: (state) => {
Object.assign(state, initialState);
},
updateStatus: (state, action) => {
state.status = action.payload;
},
},
});
};
export const genericSlice = createStoreSlice({
sliceName: 'someSliceName',
defaultState: { someField: 'some value' },
reducers: {
setSomeField: (state, action) => {
const { payload } = action;
state.data.someField = payload;
},
},
});
export const { reset, updateStatus, setSomeField, fakeReducer } = genericSlice.actions; // only fakeReducer throws error as unknown as expected
Here is a working TS Playground
See this example from their docs that explains it a bit better.

Can I change state from one slice to another slice in Redux?

I have 2 slices, the first of which contains state errors and the second of which contains logic.
Is it possible to change the value state in the error slice from a logical slice?
Error slice
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
error: false,
};
export const errorSlice = createSlice({
name: "error",
initialState,
reducers: {
setError: (state, action) => {
state.error = action.payload;
},
},
});
export const { setError } = errorSlice.actions;
export default errorSlice.reducer;
Logical slice
import { createSlice } from "#reduxjs/toolkit";
export const doSomething = (data) => {
return (dispatch) => {
dispatch(setData(data.text))
// here I want dispatch setError from errorSlice
// dispatch(setError(data.error))
};
};
const initialState = {
data: null,
};
export const logicalSlice = createSlice({
name: "logical",
initialState,
reducers: {
setData: (state, action) => {
state.error = action.payload;
},
},
});
export const { setData } = logicalSlice.actions;
export default logicalSlice.reducer;
And I need to run it from a component with a single dispatch
dispatch(doSomething(data))
Is there such a possibility?
Thank you!

How can I change state of another reducer from extra reducers add cases

I am trying to create a notification Component.
My notification Component is at root level and when ever user tries to login the process of the the async function is relayed to him i.e. pending fulfilled or rejected.
The Problem is that I don't know how to call notification reducer from userSlice or even if its possible is this a good way or not.
User Slice
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import axios from "axios";
const initialUserState = {
currentUser:null
}
export const getUser = createAsyncThunk(
'user/getUser',
async (endpoint, data) => {
return(
await axios.post(endpoint, data)
.then(res =>{
return res.data.user
})
.catch(error =>{
throw Error(error.response.data)
})
)
}
)
const userSlice = createSlice({
name: 'user',
initialState: initialUserState,
reducers:{
currentUser(state, action){
state.currentUser = action.payload
}
},
extraReducers:
(builder) => {
builder.addCase(getUser.pending, ()=>{
console.log("authing")
})
builder.addCase(getUser.fulfilled, (state, action)=>{
state.currentUser = action.payload
console.log("fulfilled")
})
builder.addCase(getUser.rejected, (state, action)=>{
console.log("failed")
alert(action.error.message)
})
}
})
export const userActions = userSlice.actions;
export default userSlice.reducer;
notificationSlice
import React from 'react'
import { useSelector } from 'react-redux'
function Notification() {
const toast = useSelector(state => state.notification)
console.log(toast)
return (
toast.active &&
<div className="notification" style={{backgroundColor:toast.backgroundColor}} >
{toast.message}
</div>
)
}
export default Notification
I want to change notification state when ever one of the extra reducer in userSlice is called
I think you are thinking about this almost exactly backwards. What you want is NOT to "call notification reducer from userSlice," but to LISTEN for userSlice actions in a notificationSlice.
I have done something like the following, which I think would work well for you:
import { createEntityAdapter, createSlice, isAnyOf } from '#reduxjs/toolkit'
const notificationsAdapter = createEntityAdapter()
const initialState = notificationsAdapter.getInitialState({
error: null,
success: null,
})
const notificationsSlice = createSlice({
name: 'notifications',
initialState,
reducers: {
clearNotifications: state => {
state.error = null
state.success = null
},
setError: (state, action) => {
state.success = null
state.error = action.payload
},
setSuccess: (state, action) => {
state.success = action.payload
state.error = null
},
},
extraReducers: builder => {
builder
.addMatcher(
isAnyOf(
getUser.fulfilled,
),
(state, action) => {
state.error = null
state.success = action.payload.message
}
)
.addMatcher(
isAnyOf(
getUser.rejected
// can add as many imported actions
// as you like to these
),
(state, action) => {
state.error = action?.payload
state.success = null
}
)
// reset all messages on pending
.addMatcher(
isAnyOf(
getUser.pending
),
(state, action) => {
state.error = null
state.success = null
}
)
},
})
export const { clearNotifications, setError, setSuccess } = notificationsSlice.actions
export default notificationsSlice.reducer
export const getErrorMsg = state => state.notifications.error
export const getSuccessMsg = state => state.notifications.success
Having added the above, you can now create a notification component that listens for
const error = useSelector(getErrorMsg)
const success = useSelector(getSuccessMsg)
and shows the messages accordingly.
Caveat:
My notificationSlice code assumes that when an action completes, there will exist a "message" object on the success payload. So, on my async thunks, if my api does not return this I must add this explicitly to the result.

Is there a way to access global state in createSlice?

Here is an example:
const user = createSlice({
name: 'user',
initialState: { name: '', age: 20 },
reducers: {
setUserName: (state, action) => {
state.name = action.payload // mutate the state all you want with immer
}
},
// "map object API"
extraReducers: {
[counter.actions.increment]: (state, action) => {
state.age += 1
}
}
})
Can I get access to the counter state?
Let's say I want to increment age only when the counter is 30. Otherwise, I would need to listen when the count is changing in useEffect hook and dispatch some action that will handle age increment (?).
In other words, what's the best way to compute the slice of state based on the current global state using redux-toolkit?
This is covered in the Redux FAQ entry on sharing state between reducers.
Pasting the key points:
If a reducer needs to know data from another slice of state, the state tree shape may need to be reorganized so that a single reducer is handling more of the data.
You may need to write some custom functions for handling some of these actions. This may require replacing combineReducers with your own top-level reducer function. You can also use a utility such as reduce-reducers to run combineReducers to handle most actions, but also run a more specialized reducer for specific actions that cross state slices.
Async action creators such as redux-thunk have access to the entire state through getState(). An action creator can retrieve additional data from the state and put it in an action, so that each reducer has enough information to update its own state slice.
I think you could use thunkAPI and extrareducers like below, assuming that you have an auth slice which has the current user object:
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import ordersService from "./ordersService";
const initialState = {
orders: [],
isError: false,
isSuccess: false,
isLoading: false,
message: "",
};
//get orders
export const getOrders = createAsyncThunk(
"orders/getOrders",
async (__, thunkAPI) => {
try {
const userId = thunkAPI.getState().auth.user.id
return await ordersService.getOrders(userId);
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
export const ordersSlice = createSlice({
name: "orders",
initialState,
reducers: {
reset: (state) => {
state.isError = false;
state.isSuccess = false;
state.isLoading = false;
state.message = "";
},
},
extraReducers: (builder) => {
builder
.addCase(getOrders.pending, (state) => {
state.isLoading = true;
})
.addCase(getOrders.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.orders = action.payload;
})
.addCase(getOrders.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
state.orders = [];
});
},
});
export const { reset, setOrder } = ordersSlice.actions;
export default ordersSlice.reducer;

Resources