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
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
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.
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.
I'm giving redux-orm a try and from what I can gather, redux-orm creates an object in store, whose key is the value specified via Model.name, for every registered model.
Well,to create my "entities" reducer, I'm using combineReducers, passing in the reducers for all my entities, like this:
import { combineReducers } from 'redux';
import City from './cityReducer';
import Poi from './pointOfInterestReducer';
const entitiesReducer = combineReducers({
City,
Poi
});
export default entitiesReducer;
And finally this is how I create my rootReducer, also using combineReducers:
The problem is that,I think that, by using combineReducers I'm duplicating the keys in my store like it's shown here:
Do you have any idea how I can avoid this, and have all my models be direct descendants of "entities"?
Something like entities>City and not entities>City>City
Edit: cityReducer.js
import orm from './../../orm/orm';
import * as types from '../../actions/actionTypes';
const loadCities = (state, action) => {
// Create a Redux-ORM session from our entities "tables"
const session = orm.session(state);
// Get a reference to the correct version of model classes for this Session
const { City } = session;
const { cities } = action.payload;
// Clear out any existing models from state so that we can avoid
// conflicts from the new data coming in if data is reloaded
City.all().toModelArray().forEach(city => city.delete());
// Immutably update the session state as we insert items
cities.forEach(city => City.parse(city));
return session.state;
};
const updateCity = (state, payload) => {
const { id, newItemAttributes } = payload;
const session = orm.session(state);
const { City } = session;
if (City.hasId(id)) {
const modelInstance = City.withId(id);
modelInstance.update(newItemAttributes);
}
return session.state;
};
const deleteCity = (state, payload) => {
const { id } = payload;
const session = orm.session(state);
const { City } = session;
if (City.hasId(id)) {
const modelInstance = City.withId(id);
// The session will immutably update its state reference
modelInstance.delete();
}
return session.state;
};
const createCity = (state, payload) => {
const { city } = payload;
const session = orm.session(state);
const { City } = session;
City.parse(city);
return session.state;
};
const citiesReducer = (dbState, action) => {
const session = orm.session(dbState);
switch (action.type) {
case types.LOAD_CITIES_SUCCESS: return loadCities(dbState, action);
case types.CREATE_CITY_SUCCESS: return createCity(dbState, action);
case types.UPDATE_CITY_SUCCESS: return updateCity(dbState, action);
case types.DELETE_CITY_SUCCESS: return deleteCity(dbState, action);
default: return session.state;
}
};
export default citiesReducer;
There's two ways you can use Redux-ORM to define its "tables" structure, and write reducer logic to use those tables. I gave examples of both approaches in my blog posts Practical Redux, Part 1: Redux-ORM Basics, Practical Redux, Part 2: Redux-ORM Concepts and Techniques, and Practical Redux, Part 5: Loading and Displaying Data. (Note that those posts cover use of Redux-ORM 0.8, and version 0.9 has some API changes. I listed those changes in Practical Redux, Part 9.)
The first approach is to write your reducer logic attached to your model classes, and use the Redux-ORM ORM instance to generate a reducer that creates the "tables" and manages them for you. Per the example on the front of the Redux-ORM repo:
import {createReducer} from "redux-orm";
import {orm} from "./models";
const rootReducer = combineReducers({
entities: createReducer(orm)
});
The other approach is to write your own function that creates the initial state, and possibly other functions that handle the updating:
import {orm} from "./models";
const initialState = orm.getEmptyState();
export default function entitiesReducer(state = initialState, action) {
// handle other actions here if desired
return state;
}
In other words, don't define the separate per-model-type "tables" yourself. Let Redux-ORM define those, either automatically in its own reducer, or by using it to generate the initial state for your entities reducer.