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
Related
Am having issues updating a redux store in NEXTJS. am building a CV platform with the feature to preview users' input almost immediately into a preview page. this cv platform has the experience, education etc that a normal cv platform should have and am using the react hook form package to manage forms and also to enhance dynamic forms.
so because the preview component will be another project on its own, I need the best way to pass data from my app into the preview app. Then I thought of passing every form input, cv styles, and data to a redux store so the preview component can just get the user's data from the store
as I said earlier, am using the react hooks form library to manage my form, so to update the store in real-time whenever the user inputs anything, I imported the useWatch hook from react hook form to watch my form in case of any data changes. so I set up a useEffect to listen for any useWatch change to dispatch the whole useWatch data to the store. NB: this data contains an array of objects
my challenge right now is that anytime I dispatch the data to store, redux toolkit or probably immer frowns at what am doing and will always break the app, returning back this error message
TypeError: Cannot assign to read only property 'jobTitle' of object '#<Object>'
at set (index.esm.mjs?b902:507:1)
at onChange (index.esm.mjs?b902:1749:1)
at HTMLUnknownElement.callCallback (react-dom.development.js?ac89:4164:1)
at Object.invokeGuardedCallbackDev (react-dom.development.js?ac89:4213:1)
at invokeGuardedCallback (react-dom.development.js?ac89:4277:1)
at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js?ac89:4291:1)
at executeDispatch (react-dom.development.js?ac89:9041:1)
at processDispatchQueueItemsInOrder (react-dom.development.js?ac89:9073:1)
at processDispatchQueue (react-dom.development.js?ac89:9086:1)
at dispatchEventsForPlugins (react-dom.development.js?ac89:9097:1)
at eval (react-dom.development.js?ac89:9288:1)
at batchedUpdates$1 (react-dom.development.js?ac89:26140:1)
at batchedUpdates (react-dom.development.js?ac89:3991:1)
at dispatchEventForPluginEventSystem (react-dom.development.js?ac89:9287:1)
at dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay (react-dom.development.js?ac89:6465:1)
at dispatchEvent (react-dom.development.js?ac89:6457:1)
at dispatchDiscreteEvent (react-dom.development.js?ac89:6430:1)
Below is the redux store and how am setting the experience
const initialState: IResume = {
templatePrimaryColor: "#335384",
top: 0,
showOverlay: false,
cv_object: {
templateId: 1,
personalInformation: {} as PersonalInformation,
experiences: [] as Experience[],
educations: [] as Education[],
skills: [] as Skill[],
awards: [] as Award[],
certificates: [] as Certificate[],
publications: [] as Publication[],
references: [] as Reference[],
},
};
export const resumeSlice = createSlice({
name: "resume",
initialState,
reducers: {
setExperience: (state, action: PayloadAction<Experience[]>) => {
// state.cv_object.experiences = [...action.payload];
state.cv_object.experiences = Object.assign(state.cv_object.experiences, action.payload);
},
},
});
Below is how am setting the forms and how am dispatching it
//React hooks form initialSetup
const { register, control, handleSubmit } = useForm<CvObject>({
defaultValues: {
experiences: [{ ...ExperienceDefaultValues }],
},
});
//usefieldArray for dynamic forms
const { append, fields, remove } = useFieldArray({ control, name: "experiences" });
//dispatch the entire form data to experience if any changes is being made
const formValues = useWatch({ control, name: "experiences" });
const [currentFormIndex, setCurrentFormIndex] = useState(0);
useEffect(() => {
if (!useAi) dispatch(hideOverlay());
else dispatch(showOverlay());
}, [useAi]);
useEffect(() => {
dispatch(setExperience(formValues));
}, [formValues]);
const handleAddAnotherExperience = () => {
setCurrentFormIndex((prev) => prev + 1);
append({ ...ExperienceDefaultValues });
};
const handleDelete = (index: number) => {
remove(index);
if (currentFormIndex > 0) setCurrentFormIndex((prev) => prev - 1);
};
const handleEdit = (index: number) => {
setCurrentFormIndex(index);
};
This is the sample object of the experience am passing but Array of Experience
export interface Experience {
companyName: string;
fromYear: string;
toYear: string;
fromMonth: string;
toMonth: string;
currentlyWorking: boolean;
achievements: string;
description: string;
city: string;
country: string;
index: number;
jobTitle: string;
}
So what am really expecting from this is how to change the store or how to replace the previous experience that is in the store with the incoming experience that is being dispatched.
React hook form is the guy handling new object, removing new object with their useFieldArray hooks.
First of all, you shouldn't directly mutate the data in the redux store, so you can use the object spread operator to create new objects and secondly you should always have a return statement in your slice. So your resumeslice should actually be like this
export const resumeSlice = createSlice({
name: "resume",
initialState,
reducers: {
setExperience: (state, action: PayloadAction<Experience[]>) => {
state = {
...state,
cv_object: {
...state.cv_object,
experiences: action.payload
}
}
return state
},
},
});
I believe this should work
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 have a general question about Reducers. In my application, I have many users (child, parent, and teacher). I have created a reducer to load tasks from the database for the parent/teacher and in my initial state I have an array "Tasks".
Do I have to create another reducer to load tasks of children from the database and I have to, can I use the same array name "Tasks"?
TasksReducer.js:
const initialState = {
tasks: [],
image: null
}
const tasks = (state = initialState, action) => {
switch (action.type) {
case 'LOAD_TASKS_FROM_SERVER':
return {
...state,
tasks: action.payload
};
case 'ADD_TASK':
return {
...state,
tasks: [action.payload, ...state.tasks]
}
default:
return state;
}
};
TasksChildReducer.js : (I only have in common the "LOAD_TASKS_FROM_SERVER")
const initialState = {
tasksChildren: [],
}
I think that the easiest way is to have different reducers mounted each in a different key of the root redux state.
So their own keys can be named the same, and you will access them like so
const getChildrenTasks = state => state.children.tasks
const getParentTasks = state => state.parent.tasks
etc.
To avoid code duplication, you could also abstract some of the reducer operation in common functions.
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.
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