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;
Related
I'm fetching data from a mongoDB database and then fetch that data from the server and finally render the data to the UI in a specified component. I'm using redux-toolkit for state management.
The problem is when fetching the data from the server it is not visible in the store. Why is the empty array in the initial state still empty after fetching the data? I'm using createSlice Api that generates the action creators and action types and createAsyncThunk Api for the asynchronous task of fetching the data from the server.
Slice reducer
import { createSlice, createAsyncThunk} from '#reduxjs/toolkit'
import axios from 'axios'
const initialState = {
realestate: [],
isSuccess: false,
isLoading: false,
message: '',
}
export const getRealEstates = createAsyncThunk(
'realestate/getRealEstates', async () => {
try {
const response = await axios.get('castles')
return response.data
} catch (error) {
console.log(error)
}
}
)
export const estateSlice = createSlice({
name: 'realestate',
initialState,
reducers: {
reset: (state) => initialState,
},
extrareducers: (builder) => {
builder.addCase(getRealEstates.pending, (state) => {
state.isLoading = true
})
builder.addCase(getRealEstates.fulfilled, (state, action) => {
state.isLoading = false
state.isSuccess = true
state.realestate = action.payload
})
builder.addCase(getRealEstates.rejected, (state, action) => {
state.isLoading = false
state.isError = true
state.message = action.payload
})
}
})
export const { reset } = estateSlice.actions
export default estateSlice.reducer
Store
export const store = configureStore({
reducer: {
realestate: realestateReducer,
registered: registerReducer,
},
});
Component
const realestates = useSelector(state => state.realestate)
const { isLoading, realestate, isError, message, isSuccess} = realestates
const dispatch = useDispatch()
useEffect(() => {
dispatch(getRealEstates())
if(realestate){
setShow(true)
}else{
console.log('No data retrieved')
}
}, [dispatch, isError, realestate, message])
It's extraReducers with an uppercase R, your code contains extrareducers.
I have an initial state like this.
const initialState = {
loading: false,
products: []
}
For the get products, I use thunk and I fetch datas from an API. loading field describes status of API call. If loading is true, i show progress in my component.
export const getProducts = createAsyncThunk('product/getProducts ',
async (payload, { rejectWithValue }) => {
try {
//fetch from somewhere
}
catch (e) {
return rejectWithValue(e.message)
}
}
)
const productSlice = createSlice({
name: "product",
initialState,
reducers: {
setLoading: (state, action) => {
state.loading = action.payload;
}
},
})
In a component first dispatch for loading and then getProducts.
// In some component
dispatch(setLoading(true))
dispatch(getProducts())
My question is, can I only call for getProducts and update loading state inside that function?
//In some component
dispatch(getProducts())
Yes you can. This is very simple
Update your createSlice section like this
const productSlice = createSlice({
name: "product",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getProducts.pending, (state) => {
state.loading = true;
});
builder.addCase(getProducts.fulfilled, (state) => {
state.loading = false;
});
},
})
You used createAsyncThunk and it is built in already to handle pending/fulfilled/rejected state with extraReducers in createSlice.
You can also handle the rejected state if fetch fail.
const productSlice = createSlice({
name: 'product',
initialState,
extraReducers: (builder) => {
builder.addCase(getProducts.pending, (state, action) => {
state.isLoading = true;
})
builder.addCase(getProducts.fulfilled, (state, action) => {
state.isLoading = true;
state.products = action.payload
})
builder.addCase(getProducts.rejected, (state, action) => {
state.isLoading = false;
})
},
})
To use it, will be enough to simple call dispatch(getProducts()) and extraReducers with thunk take care of the rest.
I want to dispatch in changeCategory reducer. how should I do it?
I am using create-react-app tool
Thanks
export const searchParamsSlice = createSlice({
name: 'searchParams',
initialState,
reducers: {
changeLocation: (state, action) => {
state.location = action.payload;
},
changeCategory: (state, action) => {
state.category = action.payload;
const dispatch = useDispatch()
dispatch(fetchResturantsAsync({ city: state.location, category: state.category, searchKey: state.seachText, page: 0, size: 10 }))
},
}
You cannot dispatch in a reducer - it is one of the three Redux core principles that reducers have to be side-effect-free.
If you want to react to another action by dispatching a new one, you could always use the listenerMiddleware provided by RTK Query, or write a thunk action creator that dispatches both of those actions after each other.
Here is how you should do it
create this Middleware
export const asyncDispatchMiddleware = store => next => action => {
let syncActivityFinished = false;
let actionQueue = [];
function flushQueue() {
actionQueue.forEach(a => store.dispatch(a)); // flush queue
actionQueue = [];
}
function asyncDispatch(asyncAction) {
actionQueue = actionQueue.concat([asyncAction]);
if (syncActivityFinished) {
flushQueue();
}
}
const actionWithAsyncDispatch =
Object.assign({}, action, { asyncDispatch });
const res = next(actionWithAsyncDispatch);
syncActivityFinished = true;
flushQueue();
return res;
};
Then add it to your store
export const store = configureStore({
reducer: {
counter: counterReducer,
//....
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(asyncDispatchMiddleware),
});
Then in your reducer do something like this
changeWeekday: (state, action) => {
state.weekName = action.payload;
action.asyncDispatch(fetchSomethingAsync({
weekName: state.weekName
}))
}
I had changeWeekday in my code, in your case it could be any reducer.
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.
I'm working on an app where I have multiple slices. I'm using createAsyncThunk for API calls and I like it cause it provides action creators for different state of API request, so that I can track loading state and errors within the reducer. But my question is, what if I want to have a separate reducer to track loading, error and success of my API calls how do I accomplish that with redux-toolkit
I know I can dispatch an action from within my createAsyncThunk function but it doesn't feel right and kinda defeats the purpose of the function itself. Also, side effects inside the reducer are considered to be a bad practice. So, I'm kinda confused at this point, I want to have just one Loader component in the root of the app that gets triggered when the loading state is true and it doesn't matter what exactly is loading
Here is an example of my current code:
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit'
import { AxiosError } from 'axios'
import { masterInstance } from 'api'
import { GetAccessCodeParams, RegistrationStateType } from 'store/slices/registration/types'
export const getAccessCodeRequest = createAsyncThunk<void, GetAccessCodeParams, { rejectValue: { message: string } }>(
'registration/getAccessCodeRequest',
async ({ email }, { rejectWithValue }) => {
try {
await masterInstance.post(`/authorization/getAccessCodeWc`, { email })
} catch (err) {
let error: AxiosError = err
if (error) {
return rejectWithValue({
message: `Error. Error code ${error.response?.status}`,
})
}
throw err
}
}
)
const initialState: RegistrationStateType = {
isLoading: false,
error: null,
}
const registrationSlice = createSlice({
name: 'registration',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getAccessCodeRequest.fulfilled, (state) => {
state.isLoading = false
state.error = null
})
builder.addCase(getAccessCodeRequest.pending, (state) => {
state.isLoading = true
state.error = null
})
builder.addCase(getAccessCodeRequest.rejected, (state, action) => {
if (action.payload) {
state.error = {
message: action.payload.message,
}
} else {
state.error = action.error
}
state.isLoading = false
})
},
})
export const registrationReducer = registrationSlice.reducer
I want isLoading and error to be in a separate reducer
You could have a shared reducer matcher function.
// mySharedStuff.js
export const handleLoading = (action, (state) => {
state.loading = action.type.endsWith('/pending'); // or smth similar
});
export const handleError = (action, (state) => {
state.error = action.type.endsWith('/rejected'); // or smth similar
});
// mySlice.js
const mySlice = createSlice({
name: 'FOO',
initialState: {},
reducers: {},
extraReducers: builder => {
builder.addMatcher(handleLoading),
builder.addMatcher(handleError),
...