is it a bad idea to dispatch several (synchronous) actions one after the other in a handler?
lets assume I have 3 buttons: buttonA, buttonB and buttonC.
for my first reducer it is important to know which button was clicked so it might look like this:
const firstReducer = function(state = [], action){
switch(action.type) {
case types.BUTTON_A_CLICKED:
console.log("button a was clicked");
return state.concat("a");
case types.BUTTON_B_CLICKED:
console.log("button b was clicked");
return state.concat("b");
case types.BUTTON_C_CLICKED:
console.log("button c was clicked");
return state.concat("c");
default:
return state;
}
}
but my second reducer just wants to know when a button was clicked (doesn't care which button). I know I can make it like this:
const secondReducer = function(state = [], action){
switch(action.type) {
case types.BUTTON_A_CLICKED:
case types.BUTTON_B_CLICKED
case types.BUTTON_B_CLICKED
console.log("some button was clicked");
return state.concat("button");
default:
return state;
}
}
but, what if I have 1000 buttons? can I just make my second reducer look like this?:
const secondReducer = function(state = [], action){
switch(action.type) {
case types.BUTTON_CLICKED:
console.log("some button was clicked");
return state.concat("button");
default:
return state;
}
}
and from the handler dispatch 2 actions for every button click,
one for the specific button (BUTTON_A_CLICKED)
and one general (BUTTON_CLICKED)
onClickHandler(){
dispatch({
type: ACTION_TYPES.BUTTON_A_CLICKED,
});
dispatch({
type: ACTION_TYPES.BUTTON_CLICKED,
});
of course this is a silly example and I can just dispatch BUTTON_CLICKED and in the action data send which button was clicked and if any reducer is interested in that info - it can take it from the action obj..
every googling I performed says to look at redux thunk, or saga, but I am curious to understand why (and if) it is a bad idea to do what I suggested.
Thanks.
In my opinion there is nothing wrong with dispatching multiple actions in the way you describe.
However, when I want to do something similar, the pattern I follow is to only dispatch a single action from the handler and then use redux-thunk to allow this primary action to dispatch subsequent actions.
Here is an example, taken from live code, of me dispatching multiple actions from within a single action. The actions.modelSelect function returns a thunk:
actions.modelSelect = modelId => {
return (dispatch, getState) => {
const state = getState()
const model = getModelById(state, modelId)
dispatch({
types,
type: types.modelSelect,
local: true,
data: model
})
dispatch(actionHub.USAGE_LIST_FILE_SERVER())
dispatch(actionHub.USAGE_LIST_FILE_MSAPI(modelId))
dispatch(actionHub.BUILD_LIST(modelId))
}
}
Related
I managed to write reducer using createSlice but the action seems to be confusing.
My old reducer :
function listPeopleReducer(state = {
getPeople:{}
}, action){
switch (action.type) {
case D.LIST_PEOPLE: {
return {
...state
, getPeople:action.payload
}
}
default:{}
}
return state
}
By using createSlice from the redux toolkit, I migrated the reducer to this,
const listPeopleReducer = createSlice({
initialState:{getPeople:{}},
name:"listPeople",
reducers:{
listPeople(state,action){
return {
...state,
getPeople : action.payload
}
}
}
})
My old action, makes an api call inside it, with the help of a helper function makeApiRequest (which takes in parameters and returns the response of the api),
export function listPeople(config: any) {
return function (dispatch: any) {
makeApiRequest(config)
.then((resp) => {
dispatch({
type : D.LIST_PEOPLE,
payload : resp.data
})
})
.catch((error) => {
dispatch({
type : D.LIST_PEOPLE,
payload : error
})
})
}
}
With reduxtool kit, we could do something like,
const listPeople = listPeopleReducer.actions.listPeople;
But, how will I write my custom action that contains the helper function makeApiRequest ?
i.e The old Action should be migrated to reduxtoolkit type.
It's definitely tricky when migrating, since there are some major conceptual changes that you must eventually wrap your head around. I had to do it a couple of times before it clicked.
First, when you are creating const listPeopleReducer with createSlice(), that is not actually what you are creating. A slice is a higher level object that can generate action creators and action types for you, and allows you to export reducers and actions FROM it.
Here are the changes I would make to your code:
const peopleSlice = createSlice({
initialState:{getPeople:{}},
name:"people",
reducers:{
listPeople(state,action){
// uses immer under the hood so you can
// safely mutate state here
state.getPeople = action.payload
}
},
extraReducers:
// each thunk you create with `createAsyncThunk()` will
// automatically have: pending/fulfilled/rejected action types
// and you can listen for them here
builder =>
builder.addCase(listPeople.pending, (state,action) => {
// e.g. state.isFetching = true
})
builder.addCase(listPeople.fulfilled, (state,action) => {
// e.g. state.isFetching = false
// result will be in action.payload
})
builder.addCase(listPeople.rejected, (state,action) => {
// e.g. state.isFetching = false
// error will be in action.payload
})
}
})
Then, outside of your slice definition, you can create actions by using createAsyncThunk(), and do like:
export const listPeople = createAsyncThunk(
`people/list`,
async (config, thunkAPI) => {
try {
return makeApiRequest(config)
} catch(error) {
return thunkAPI.rejectWithError(error)
// thunkAPI has access to state and includes
// helper functions like this one
}
}
}
The "Modern Redux with Redux Toolkit" page in the Redux Fundamentals docs tutorial shows how to migrate from hand-written Redux logic to Redux Toolkit.
Your makeApiRequest function would likely be used with Redux Toolkit's createAsyncThunk, except that you should return the result and let createAsyncThunk dispatch the right actions instead of dispatching actions yourself.
hey guys im learning the useReducer hook and for the most part it seems to be quite similar to redux (minus the action being sent to the store etc)
the thing i seem to ALWAYS have problems with when i get more complex state management situations is trying to alter my state in the ways i would like to. in my code i am essentially trying to have a user select a track and add it to a list of favorite songs. my code seems to be replacing the state and not adding to it
here is my initial state and my useReducer and then lastly my add function (which when a button is pressed down below it sends in a track to be added to the list
const initialState = {
name:'',
duration:''
};
const [favorites, dispatch]=useReducer(reducer, initialState)
const add=(song)=>{
dispatch({type:'ADD', payload:song})
console.log(favorites)
}
THIS is the part that is confusing me. in my reducer i have this
export const reducer=(song, action)=>{
switch(action.type){
case 'ADD':
return {...song, name:action.payload}
}
WHICH is essentially adding a new object everytime called name: trackname BUT i do not want to overwrite the last item. i feel like i am using spread wrong and also returning the incorrect payload maybe?
my final state keeps looking like this
{name: "deep end", duration: ""}
when i want it to look something like this
``
[{name: "deep end", duration: ""}
{name: "ok", duration: ""}
{name: "testtrack", duration: ""}
]`
i have tried setting the initial state to somethingm like this
const initialState = {
favorites:{
[ADD TRACKS HERE]
}
};
BUT CANT seem to overwrite the state correctly so that it ADDS to the array. it keeps overwritting the last one
Redux's guide to Immutable Update Patterns is a great resource on how to update nested data in a way that doesn't mutate your state.
With an array there are two main ways to immutably add an element.
With spread syntax:
const newArray = [...songs, newSong];
With .concat(), which returns a new array that contains the additional items (that is different from .push() which mutates the array and just returns the length).
const newArray = songs.concat(newSong);
You can decide what you want the shape of your state to be. Storing the array to a property favorites is fine, but adds another layer of complexity to your updates.
export const reducer = (state, action) => {
switch (action.type) {
case "ADD":
return {
...state,
favorites: [...state.favorites, action.payload]
};
default:
return state;
}
};
const initialState = {
favorites: [] // initial state has an empty array
};
const [state, dispatch] = useReducer(reducer, initialState);
// here favorites is just a property of state, not the whole state
const favorites = state.favorites;
I would recommend that that state should just be the array of favorites itself.
export const reducer = (favorites, action) => {
switch (action.type) {
case "ADD":
return [...favorites, action.payload]
default:
return favorites;
}
};
// favorites is the whole state
// initial state is an empty array
const [favorites, dispatch] = useReducer(reducer, []);
In either case, we are expecting the action.payload to be a complete song object, not just the name.
dispatch({ type: "ADD", payload: { name: "Deep End", duration: "3:22" } });
You could extract that into a helper function. In Redux terms we call this function an Action Creator.
const addFavorite = (name, duration) => ({
type: "ADD",
payload: { name, duration }
});
dispatch(addFavorite("Deep End", "3:22"));
I have a use case like this:
eventListReducer: will get a list of events based on date range
eventDetailReducer: will get the event details based on one event id
I know how to do the two above, my question:
When my page loads initially, I will get a list of events based on default date range and load the first event details, I can certainly create an
EventListAndDetailReducer to duplicate eventListReducer and eventDetailReducer. Is there any better way I can reuse the logic?
What I want to achieve is to have another action, that will first call getEvents and update the eventLists state, and then grab the first event and call setEvent and update the eventDetail state.
This is my eventDetailReducer:
const initialState = {
eventDetails: "",
}
const eventReducer = (state = initialState, action) => {
switch (action.type) {
case "SET_EVENT":
state = {
...state,
eventDetails: action.payload
};
break;
}
return state;
}
export default eventReducer;
This is my eventsReducer:
const initialState = {
eventsList: [],
}
//getEventsReducer
const getEventsReducer = (state = initialState, action) => {
switch (action.type) {
case "GET_EVENTS":
state = {
...state,
eventList: ["Joe", "Tom", "Marry"] //assuming this from some other endpoint
};
break;
}
return state;
}
export default getEventsReducer;
What about using EventListAndDetailReducer?
const initialState = {
eventsList: [],
eventDetails: ""
}
export function eventListAndDetailReducer(state, action) {
switch(action.type) {
case GET_EVENTS:
return {...state, eventList: eventsReducer(state.eventsList, action)}
case "SET_EVENT":
return {...state, eventDetails: eventDetailsReducer(state.eventDetails, action)}
default:
return state
}
}
and then somewhen start using combineReducers?
Why not just have the eventDetails reducer also update on the GET_EVENTS action?
const eventReducer = (state = initialState, action) => {
switch (action.type) {
case "SET_EVENT":
state = {
...state,
eventDetails: action.payload
};
break;
case "GET_EVENTS":
state = {
...state,
eventDetails: action.payload[0] // assuming payload is an array
};
break;
}
return state;
}
Remember, all reducers receive all actions, so it does not need to be a 1-1 mapping.
What I understand from you question is that you want another action to do both actions sequentially and be dependent on each. I assume you have some middle ware such as redux-thunk that allows actions to be more than plaIn functions!
export function combinedAction() {
return (dispatch, getState) => {
// Write fetch() request to get events list from anywhere.
// Following should be within .then() if you're using fetch.
// Here events are just hardcoded in reducer!
dispatch(return { type: GET_EVENTS, payload: events }).then( () => {
let event = getState().eventsList[0]
dispatch(return { type: SET_EVENT, payload: event })
})
};
}
This will fire up GET_EVENTS action first and it'll set events array in state.eventsList. Then next action just uses this state information to dispatch next action SET_EVENT. Refer here to learn about chaining actions. How to chain async actions?
Using redux, I have a bunch of actions and a bunch of reducers that coincide with each of the action types.
Each of the actions map to a different part of the state being updated (all the action creators are primarily for fetching data from an API, for example, which maps to some part of the state). Our reducers currently look rather silly (simplified to A, B, C, for the sake of example):
export const rootReducer = combineReducers({
somePartOfStateA: someAReducer,
somePartOfStateB: someBReducer,
somePartOfStateC: someCReducer,
... (and many other reducers)
});
function someAReducer(state = [], action) {
switch (action.type) {
case FETCH_SOME_A:
return action.payload;
default:
return state;
}
}
function someBReducer(state = [], action) {
switch (action.type) {
case FETCH_SOME_B:
return action.payload;
default:
return state;
}
}
function someCReducer(state = [], action) {
switch (action.type) {
case FETCH_SOME_C:
return action.payload;
default:
return state;
}
}
// and a bunch of pretty much identical reducers continue below
From my understanding, the purpose of having them split is so that each reducer handles a part of the state's namespaces. The resulting reducers are simple, but pretty much the same thing over and over. Is there a recommended way to consolidate all of these reducers per piece of state?
Reducer is just a function. You could use higher order function to make reducer for you.
const makeSingleActionReducer = type => (state, action) =>
action.type === type ? action.payload : state
export const rootReducer = combineReducers({
somePartOfStateA: makeSingleActionReducer(FETCH_SOME_B)
...
})
Also you could go further by creating a config {stateKey: actionType, ...} and loop over it.
I have a a pair of Redux actions that I want to dispatch when a button is pressed. One of these should take a list of services and perform reducer logic on it, and the other should just trigger some logic in the reducer:
function retrieveServices(services) {
return {
type: RETRIEVE_SERVICES,
services,
};
}
function toggleSubmitState() {
return {
type: TOGGLE_SUBMIT_STATE,
};
}
I have the following action that I assumed would do the trick:
export function submitGameplan(services) {
return (dispatch => {
dispatch(toggleSubmitState());
dispatch(retrieveServices(services));
};
}
This is how I call it in my component:
const { submitter, dispatch, services } = this.props;
const submitWithServices = () => {
dispatch(submitter(services));
};
return (
<div>
<button onClick={submitWithServices}>
<div>Submit</div>
</button>
</div>
);
where the submitter action being passed in is submitGameplan.
Although redux-thunk seems to be picking up the action and firing it (I'm getting console.log output), it's not dispatching the actions.
Aside from the (likely) possibility that I am mis-calling the function in my action, perhaps there is some issue with the fact that I have done this in my main App-level component:
const boundActions = bindActionCreators(actions, dispatch);
and then passed all actions as boundActions, so i.e. my component would get boundActions.submitGameplan.
Still, I'm not certain why this action wouldn't dispatch either of the two actions as written.
Turns out that this worked after a recompile, no idea why it didn't work before.