Redux middleware with configureStore: Next is not a function - redux

I'm trying to add a custom middleware to my Redux (I'm using it with configureStore)
const reducer = {
layout: layout,
login: login,
companies: companies,
services: services,
platforms: platforms,
report: report,
users: users,
stats: stats,
version: version,
}
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
const store = configureStore({
reducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger())
});
But I get Uncaught TypeError: next is not a function
Cannot find a working example with configureStore.
The packages that I'm using are:
"react-redux": "^8.0.2",
"redux": "^4.2.0",
"redux-thunk": "^2.4.1",
Update #1
I followed hint of
as following
const reducer = {
layout: layout,
login: login,
companies: companies,
services: services,
platforms: platforms,
report: report,
users: users,
stats: stats,
version: version,
wizard: wizard,
}
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
const middlewareLogger = logger(store)(next)(action);
const store = configureStore({
reducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middlewareLogger())
});
export default store;
but i get
src/reducers/index.js
Line 33:40: 'next' is not defined no-undef
Line 33:46: 'action' is not defined no-undef

One minute after, I found solution, thanks to this post: Why does it says that 'next' is not defined in redux middleware code. Is next method of middleware deprecated?
Error was I'm calling logger() instead of logger.
So, I did't pass any arguments to logger.
Final working code:
const store = configureStore({
reducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger)
});

Related

How to update or change the entire propery of a particular redux object

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

changing state with RTK Query

I'm learning about RTK Query and really confused. I'd be happy if someone could point me towards the right direction. My question is how one can manipulate the state of the application store the same way as it is done when using createAsyncThunk and setting up extraReducers.
export const asyncApiCall = createAsyncThunk("api/get_data", async (data) => {
const config = {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
}
};
const res = await axios.get( "http://apiserver/path_to_api",data,config );
return res['data']
} )
export const mySlice = createSlice({
name:"mySliceName",
initialState:{
data: [],
loadInProgress: false,
loadError: null,
extraData: {
// something derived based on data received from the api
}
},
extraReducers: {
[asyncApiCall .pending]: (state) => {
state.loadInProgress = true;
},
[asyncApiCall .fulfilled]: (state,action) => {
state.loadInProgress = false;
state.data = action.payload;
state.extraData = someUtilFunc(state.data)
},
[asyncApiCall.rejected]: (state) => {
state.loadInProgress = false;
state.loadError= true;
},
}
})
Now I'm replacing it with RTK Query. My current understanding is that RTK Query automatically generates hooks for exposing data received from the api and all the query-related info like if it's pending, if an error occurred etc.
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query/react'
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: builder => ({
getData: builder.query({
query: () => '/get_data'
}),
setData: builder.mutation({
query: info => ({
url: '/set_data',
method: 'POST',
body: info
})
})
})
})
export const { useSendDataMutation, useGetDataQuery } = apiSlice
If I want to store some additional data that may be affected by the api calls should I create another slice that will somehow interact with the apiSlice, or is it possible to incorporate everything in this existing code? I'm sorry for possible naivety of this question.
The short answer is that RTK Query is focused on purely caching data fetched from the server. So, by default, it stores exactly what came back in an API call response, and that's it.
There are caveats to this: you can use transformResponse to modify the data that came back and rearrange it before the data gets stored in the cache slice, and you can use updateQueryData to manually modify the cached data from other parts of the app.
The other thing to note is that RTK Query is built on top of standard Redux patterns: thunks and dispatched actions. Every time an API call returns, a fulfilled action gets dispatched containing the data. That means you can also apply another suggested Redux pattern: listening for that action in other reducers and updating more than one slice of state in response to the same action.
So, you've got three main options here:
If the "extra data" is derived solely from the server response values, you could use transformResponse and return something like {originalData, derivedData}
You could just keep the original data in the cache as usual, but use memoized selector functions to derive the extra values as needed
If you might need to update the extra values, then it's probably worth looking at listening to a query fulfilled action in another slice and doing something with it, like this silly example:
import { api } from "./api";
const someExtraDataSlice = createSlice({
name: "extraData",
initialState,
reducers: {/* some reducers here maybe? */},
extraReducers: (builder) => {
builder.addMatcher(api.endpoints.getPokemon.matchFulfilled, (state, action) => {
// pretend this field and this payload data exist for sake of example
state.lastPokemonReceived = action.payload.name;
}
}
})

Why redux store doesn't receive an update from immer

Combining reducers
export default (injectedReducers = {}) => {
return combineReducers({
...injectedReducers,
memoizedStamps: memoizedStampsReducer, // <-- need to write here
});
};
Writing an action
const mapDispatch = (dispatch) => ({
addStamp: (payload) =>
dispatch({
type: ADD_STAMP,
payload,
}),
});
Writing the reducer
export const initialState = [];
const memoizedStampsReducer = produce((draft, action) => {
const { type, payload } = action;
switch (type) {
case ADD_STAMP:
draft.push(payload);
}
}, initialState);
export default memoizedStampsReducer;
Using in a react hook
const useMemoizedStamps = () => {
const [memStamps, dispatch] = useImmerReducer(reducer, initialState);
const { addStamp } = mapDispatch(dispatch);
useEffect(() => {
addStamp({ body: 'body', coords: 'coords' });
}, []);
console.log(memStamps); // <-- gives [{ body: 'body', coords: 'coords' }] all good here
return null;
};
export default useMemoizedStamps;
But it gets never saved into injected reducer "memoizedStamps". The array is always empty. It works perfectly will with connect(null, mapDispatchToProps), but can't use connect() in my custom hook.
What do I do wrong? What is the answer here?
--- UPD 1 ---
#phry, like so?
const useMemoizedStamps = (response = null, error = null) => {
const dispatch = useDispatch();
const [memStamps] = useImmerReducer(reducer, initialState);
const { addStamp } = mapDispatch(dispatch);
useEffect(() => {
addStamp({ body: 'body', coords: 'coords' });
}, []);
console.log(memStamps);
return null;
};
And now if I need them to be local, I need to use immer's dispatcher? Any way to merge these two dispatchers? P.S. this dispatcher really saved it to global state.
Rereading this, I think you just have a misconception.
Stuff is never "saved into a reducer". A reducer only manages how state changes.
In Redux, it would be "saved into the store", but for that you would have to actually use a store. useImmerReducer has nothing to do with Redux though - it is just a version of useReducer, which like useState just manages isolated component-local state with a reducer. This state will not be shared with other components.
If you want to use Redux (and use it with immer), please look into the official Redux Toolkit, which already comes with immer integrated. It is taught by the official Redux tutorial.

Error: Invariant failed: A state mutation was detected between dispatches, in the path 'emailReducer.emails.0'

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.

What is the second argument for?

I find this code in a tutorial
...
import configureMockStore from 'redux-mock-store';
const middleware = [thunk];
const mockStore = configureMockStore(middleware);
...
it('should create BEGIN_AJAX_CALL & LOAD_COURSES_SUCCESS', (done) => {
const expectedActions = [
{type: types.BEGIN_AJAX_CALL},
{type: types.LOAD_COURSES_SUCCESS, body: {
courses: [{id:'clean-code', title:'Clean Code'}]
}}
];
const store = mockStore({courses:[]}, expectedActions);
store
.dispatch(courseActions.loadCourses())
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(types.BEGIN_AJAX_CALL);
expect(actions[1].type).toEqual(types.LOAD_COURSES_SUCCESS);
done();
});
});
and the whole bit with expectedActions doesn't make sense.
The docs say that if there is a second argument to store, it should be a function; (no explanation telling what that function would do though).
At first I thought it was forcing some actions into the store for some reason, but a quick console.log told me that wasn't the case.
Because only dispatch causes actions to accumulate.
So is it a mistake in the text or some wisdom to explore further?
This feature was removed in version 1, but you can find the example in the pre 1 docs.
The parameter expectedActions is used for testing. You can create a mock store with an array of actions, and then dispatch an the 1st action. This action will cause the other other actions to forwarded (dispatch / next) via thunks/api middleware/etc... The test will check if all of the actions in the expectedActions array have acted on the store:
import configureStore from 'redux-mock-store';
const middlewares = []; // add your middlewares like `redux-thunk`
const mockStore = configureStore(middlewares);
// Test in mocha
it('should dispatch action', (done) => {
const getState = {}; // initial state of the store
const action = { type: 'ADD_TODO' };
const expectedActions = [action];
const store = mockStore(getState, expectedActions, done);
store.dispatch(action);
})

Resources