Usually in a thunk you'd wind up calling other actions:
const startRecipe = {type: "startRecipe"}
const reducer = (state, action) => {
if (action.type === "startRecipe") {
state.mode = AppMode.CookRecipe
}
}
const getRecipeFromUrl = () => async dispatch => {
const res = await Parser.getRecipeFromUrl(url)
dispatch(startRecipe)
}
With createAsyncThunk in redux toolkit, this isn't so straightforward. Indeed you can mutate the state from your resulting action in extraReducers:
export const getRecipeFromUrl = createAsyncThunk('getRecipeFromUrl',
async (url: string): Promise<RecipeJSON> => await Parser.getRecipeFromUrl(url)
)
const appStateSlice = createSlice({
name: 'app',
initialState: initialAppState,
reducers: {},
extraReducers: ({ addCase }) => {
addCase(getRecipeFromUrl.fulfilled, (state) => {
state.mode = AppMode.CookRecipe
})
}
})
But I also want to have non-async ways to start the recipe, which would entail a reducer in the slice:
reducers: {
startRecipe(state): state.mode = AppState.CookRecipe
},
To avoid writing the same code in two places I would love to be able to call the simple reducer function from the thunk handler. I tried simply startRecipe(state) and startRecipe (which had been destructured for ducks exporting so I’m fairly sure I was referring to the correct function) from the extraReducers case but it doesn't work.
My current solution is to define _startRecipe outside of the slice and just refer to that function in both cases
reducers: { startRecipe: _startRecipe },
extraReducers: builder => {
builder.addCase(getRecipeFromUrl.fulfilled, _startRecipe)
}
Is there a "better" way where you can define the simple action in your slice.reducers and refer to it from the thunk handler in extraReducers?
The second argument of the payloadCreator is thunkAPI (doc) from where you could dispatch the cookRecipe action.
interface ThunkApiConfig {
dispatch: AppDispatch,
state: IRootState,
}
export const getRecipeFromUrl = createAsyncThunk('getRecipeFromUrl',
async (url: string, thunkAPI: ThunkApiConfig): Promise<RecipeJSON> => {
await Parser.getRecipeFromUrl(url)
return thunkAPI.dispatch(cookRecipeActionCreator())
}
)
The idea of "calling a reducer" is the wrong approach, conceptually. Part of the design of Redux is that the only way to trigger a state update is by dispatching an action.
If you were writing the reducer using a switch statement, you could have multiple action types as cases that all are handled by the same block:
switch(action.type) {
case TypeA:
case TypeB: {
// common logic for A and B
}
case C: // logic for C
}
When using createSlice, you can mimic this pattern by defining a "case reducer" function outside of the call to createSlice, and pass it for each case you want to handle:
const caseReducerAB = (state) => {
// update logic here
}
const slice = createSlice({
name: "mySlice",
initialState,
reducers: {
typeA: caseReducerAB,
typeB: caseReducerAB,
}
extraReducers: builder => {
builder.addCase(someAction, caseReducerAB)
}
})
That sounds like what you described as your "current solution", so yes, that's what I would suggest.
Related
I'm trying to learn Redux, and i encountered this code:
reducers: {
loginStart: (state) => {
//...
},
loginSuccess: (state, action) => {
//...
},
loginFailure: (state) => {
//...
},
logout: (state) => {
//...
},
},
});
export const { loginStart, loginSuccess, loginFailure, logout } = userSlice.actions;
export default userSlice.reducer;
I can't understand well what are .actions, Slice, .reducer or reducers from different web sources.
So kindly can any expert in Redux here explain in a simplified way what are theses and their roles?
Every state of your app (which is global) lives in an object tree stored in a single store.
Actions are simply JavaScript objects that have a type with a payload of data illustrating exactly what is happening. what they do? they are the only way to manage our state tree. pay attention: no state has been mutated so far.
Reducers are just responses to our corresponding called action to perform on our immutable state and thus returning a new state. optionally you might also want to check Array.reduce() method to better understand reducers.
What is slice then? as it comes with redux-toolkit, slice contains all the reducer logics and actions for a single feature.
it auto generates your action creators and types which you have to define them as constants before redux-toolkit. check createSlice for the full explanation.
In your example the object called reducers goes into your createSlice with also an initial state and a name.
Based on all that being said, this is your final example of your question:
const initialState = {}
const authSlice = createSlice({
name: 'authentication',
initialState,
reducers: {
loginStart: (state) => {
//...
},
loginSuccess: (state, action) => {
//...
},
loginFailure: (state) => {
//...
},
logout: (state) => {
//...
},
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
Aight.. so im pretty new with redux toolkit and I want to catch ALL pending actions in one slice to basically show a loading modal. I know we can do this with redux-saga and probably redux-observable
Soooooo instead of
builder.addCase(fetchUsers.pending, (state) => {
state.loading = LoadingState.PENDING;
});
To Something like this
builder.addCase(allActions.pending, (state) => {
state.loading = LoadingState.PENDING;
});
I know allActions does not work there but is there anything that will.
You can use the matching utilities included in RTK:
import { createSlice, isPending} from "#reduxjs/toolkit";
const dataSlice = createSlice({
name: "data",
reducers: { /* */ },
extraReducers: builder => {
builder.addMatcher(isPending, (state, action) => {
// logic here
})
}
})
You can also combine the matching utilities in various ways to only handle the pending state for specific thunks, etc.
I'm getting this error message about mutating state, but the purpose of Redux Toolkit is mutating state, so I'm confused...
The error is coming from addNewEmail, where I'm adding new emails to the array calling prevEmails using useSelector and the second parameter is a regular string.
import { createSlice } from "#reduxjs/toolkit";
import { AppThunk } from "./store";
const initialState = {
emails: [],
};
export const emailSlice = createSlice({
name: "email",
initialState,
reducers: {
setEmails: (state, action: any) => {
state.emails = action.payload;
},
},
});
export const { setEmails } = emailSlice.actions;
export const addNewEmail = (prevEmails: any, email: string): AppThunk => (
dispatch
) => {
const allEmails = prevEmails.push(email);
dispatch(setEmails(allEmails));
};
export default emailSlice.reducer;
export const selectEmails = (state: any) => state.emailReducer.emails;
I was also getting the same error, this is not how you disptach the
action. All you have to pass this middlewares in your store.
const store = configureStore({
reducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
immutableCheck: false,
serializableCheck: false,
})
})
As #asaf-aviv said, the real problem is that you're attempting to mutate what is actually state.emails, outside of a reducer:
const allEmails = prevEmails.push(email);
dispatch(setEmails(allEmails));
The second problem is conceptual. You should model actions as "events", not "setters", and put as much logic as possible into reducers. If you follow those guidelines, this problem won't occur in the first place.
Also, this doesn't even need to be a thunk - just dispatch an action that contains the new email object.
The right way to handle this is:
export const emailSlice = createSlice({
name: "email",
initialState,
reducers: {
emailAdded: (state, action: PayloadAction<Email>) => {
state.emails.push(action.payload)
},
},
});
export const { emailAdded } = emailSlice.actions;
// later
dispatch(emailAdded(newEmail));
You are mutating the state before dispatching the action, you can do mutations inside the reducer but not outside of it.
You can change prevEmails.push(email) to prevEmails.concat(email) which will return a new array which you can then send as a payload.
I am using Redux with react and redux-thunk as a middleware.
When I make http requests I have to dispatch three actions in my thunks.
I will use my auth example.
here are my actions:
export const loginSuccess = () => ({
type: AUTH_LOGIN_SUCCESS,
})
export const loginFailure = (errorMessage) => ({
type: AUTH_LOGIN_FAILURE,
errorMessage,
})
export const loginRequest = () => ({
type: AUTH_LOGIN_REQUEST,
})
and here is the thunk which combines above three actions:
export const login = (credentials) => dispatch => {
dispatch(loginRequest())
const options = {
method: 'post',
url: `${ENDPOINT_LOGIN}?username=${credentials.username}&password=${credentials.password}`,
}
axiosInstance(options)
.then(response => {
dispatch(loginSuccess())
dispatch(loadUser(response.data)) // I have separate action for user and separate reducer.
window.localStorage.setItem(ACCESS_TOKEN_KEY, response.data.token)
})
.catch(error => {
return dispatch(loginFailure(error))
})
}
And here is my reducer:
const initialState = {
pending: false,
error: false,
errorMessage: null,
}
export const loginReducer = (state = initialState, action) => {
switch (action.type) {
case AUTH_LOGIN_SUCCESS:
return {
...state,
pending: false,
error: false,
errorMessage: null,
}
case AUTH_LOGIN_FAILURE:
const { errorMessage } = action
return {
...state,
pending: false,
error: true,
errorMessage,
}
case AUTH_LOGIN_REQUEST:
return {
...state,
pending: true,
}
default:
return state
}
}
I have to do almost exact same things when I am sending another request, for example in case of logout. I feel like I am repeating myself a lot and there must be a better way.
I need to know what is the best practice to handle this issue.
Any other corrections and recommendations will be appreciated.
If you are looking for "ready to use" solution take a look at:
https://redux-toolkit.js.org/api/createAsyncThunk
https://redux-resource.js.org/ (but it is written with js (not TS), and no #types definition for this library)
If you are looking for a custom solution you can create a few factories:
factory for reducer
factory for three actions
factory for thunk
const actions = createActions('My request name');
const reducer = createReducer(actions);
...
const thunk = createThunk(config);
or even you can combine them:
const { actions, reducer, thunk } = createRequestState('Name...', config);
... but this is just an idea.
In react-redux, I'm trying to create a generic reducer, meaning a reducer with common logic that writes (with that logic) each time to a different section in the store.
I read Reusing Reducer Logic over and over, I just can't wrap my head around it. Let's say I have this state:
{
a: { b: { c: {...} } } },
d: { c: {...} }
}
a and d are two reducers combined with combineReducers() to create the store. I want section c to be managed with common logic. I wrote the reducer logic for c, I wrapped it to create a higher-order reducer with a name.
How do I create the a reducer with the c reducer with reference to its location (and also d accordingly)? Maybe in other words, how do I create a reducer with a "store address", managing his slice of the state, agnostic to where it is?
I sure hope someone understands me, I'm new to redux and react.
Reducer are now simple function and can be reuse somewhere else
const getData = (state, action) => {
return {...state, data: state.data.concat(action.payload)};
};
const removeLast = (state) => {
return {...state, data: state.data.filter(x=>x !== state.data[state.data.length-1])};
}
Action type and reducer function are now declared in an array
const actions = [
{type: 'GET_DATA', reducer: getData},
{type: 'REMOVE_LAST', reducer: removeLast}
];
Initial state for the reducer
const initialState = {
data: []
}
actionGenerators creates an unique Id using Symbol and assign that Id to actions and reducer function.
const actionGenerators = (actions) => {
return actions.reduce((a,c)=>{
const id = Symbol(c.type);
a.actions = {...a.actions, [c.type]: id};
a.reducer = a.reducer ? a.reducer.concat({id, reducer: c.reducer}) : [{id, reducer: c.reducer}];
return a;
},{});
}
reducerGenerators is a generic reducer creator.
const reducerGenerators = (initialState, reducer) => {
return (state = initialState, action) => {
const found = reducer.find(x=>x.id === action.type);
return found ? found.reducer(state, action) : state;
}
}
Usage
const actionsReducerCreator = actionGenerators(actions);
const store = createStore(reducerGenerators(initialState, actionsReducerCreator.reducer));
const {GET_DATA} = actionsReducerCreator.actions;
store.dispatch({type: GET_DATA});
Checkout my github project where I have a working todo application utilizing this implementation.
Redux-Reducer-Generator