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.
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
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
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 redux app that, amongst other things, updates a single string, hence the state could be reflected as:
{
theDataString: "someString",
otherData: { ...some other data... },
someListItems: [ ...a data array... ]
}
Hence I have the following reducer:
function updateDataString(state = {}, action) {
switch (action.type) {
case UPDATE_DATA_STRING:
return Object.assign({}, ...state, {theDataString: action.theDataString});
default:
return state;
}
}
However, when dispatch the UPDATE_DATA_STRING action, the theDataString value in the state becomes double nested:
{
theDataString: {theDataString: "someString"},
otherData: {... some other data... },
someListItems: [ ... a data array ... ]
}
This same problem has already been encountered here. However, the solution for them was that they were calling combineReducers on a single reducer when that was unnecessary. In my case, I'm calling combineReducers on multiple reducers - so their answer doesn't work for me. Also note that the same nesting problem does not occur for other data, only the top-level string gets double nested.
What is going wrong here?
EDIT:
I'm connecting the component that updates theDataString like this:
import {connect} from 'react-redux';
import {updateDataString} from './actions/actions';
import SomeList from './components/someList';
const mapStateToProps = (state) => {
return {someListItems: state.someListItems}
};
const mapDispatchToProps = (dispatch, placeHolder) => {
return {
updateDataString: (aString) => dispatch(updateDataSting(aString))
}
};
export default SomeListConnected = connect(mapStateToProps, mapDispatchToProps)(SomeList)
The action is set up as follows:
export const UPDATE_DATA_STRING = 'UPDATE_DATA_STRING';
export function updateDataString(aString) {
return {type: UPDATE_DATA_STRING, theDataString: aString}
}
EDIT2: Changing the way the reducer updates the state is a natural place to look for answers. However I have tried various permutations here with little effect:
Object.assign({}, ...state, {theDataString: action.theDataString});
Object.assign({}, state, {theDataString: action.theDataString});
{...state, {theDataString: action.theDataString}};
None of the above fix the problem.
Assuming that you've used combineReducers, your updateDataString reducer should be treating the state as a string, not an object:
function updateDataString(state = "", action) {
switch(action.type) {
case UPDATE_DATA_STRING: return action.newString;
default; return state;
}
}
The slice reducer will only see the string value as its "state", so you need to treat it that way.
See Using combineReducers for some further information on the topic.
Try this
return { ...state, theDataString: action.newString};
instead of this
return Object.assign({}, ...state, {theDataString: action.newString});
in your reducer.
I think you are using ... spread operator in the wrong way. And no need to use Object.assign() here.
Read more about spread operator here