Why redux store doesn't receive an update from immer - redux

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.

Related

Difficulty implementing redux thunk

This is my first time working with redux hooks and I keep receiving the error: "Error: Actions must be plain objects. Use custom middleware for async actions."
I have added the middleware thunk. Following the other peoples questions, I am not sure where I am making the mistake. I'm looking for an explanation on what I am doing wrong and what I should be reading in order to fix it.
Actions:
export const fetchNewsData = () => {
return (dispatch) => {
axios.get('http://localhost:3001/getnews')
.then(response => {
console.log(response.data);
const data = response.data;
dispatch(loadNews(data));
})
.catch(error => {
console.log(error);
dispatch(errorOnNews(error));
});
}
}
export const loadNews = (fetchedData) => {
return {
type: LOAD_NEWS,
payload: fetchedData
}
}
export const errorOnNews = (errorMessage) => {
return {
type: ERROR_ON_NEWS,
payload: errorMessage
}
}
Reducer:
const initialState = {
fetched: false,
data: [],
input: '',
filtered: [],
error: ''
}
const newsReducer = (state = initialState, action) => {
switch(action.type) {
case LOAD_NEWS:
return {
...state,
fetched: true,
data: action.payload
}
case FILTER_NEWS:
return {
...state
}
case ERROR_ON_NEWS:
return {
...state,
error: action.payload
}
default: return state;
}
}
Store:
import { createStore, applyMiddleware } from 'redux';
import ReduxThunk from 'redux-thunk';
import rootReducer from './rootReducer';
const store = createStore(rootReducer, applyMiddleware(thunk));
Component:
const fetch = useDispatch(fetchNewsData());
useEffect(() => {
if(hasFetched){
// work on true condition
} else {
fetch(); // fails on this line.
}
}, []);
useDispatch does not work like this, as it ignores all arguments and just returns you a dispatch function. So you have called dispatch() there, which essentially equals dispatch(undefined) - and the store doesn't know what to make of that action.
Do this instead:
const dispatch = useDispatch();
useEffect(() => {
if(hasFetched){
// work on true condition
} else {
dispatch(fetchNewsData()); // fails on this line.
}
}, []);
Also, generally you are writing a very outdated style of redux here that we do not really recommend to learn or use in new applications any more.
You might have been following an outdated tutorial - as this style requires you to write multiple times the necessary code and is much more error prone.
For up-to-date tutorials featuring modern redux with the official redux toolkit please see the official redux tutorials
Was following a tutorial and creating additional work that was unnessessary. Answer is to:
Cut: const fetch = useDispatch(fetchNewsData());
Change: fetch(); to fetchNewsData();
In this case, I am calling a handling function that will execute the dispatches when required.

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.

Redux Dispatch Infinite Loop

I am using axios.get in my useeffect and then I am passing the data from response to the dispatcher. It retrieves data and dispatches and then I get the state to show data an console it but data shows infinite loop. Here is my useEffect, local state, mapStateToProps and mapDispatchToProps.
const [users, setUsers] = useState()
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/users').then(response => {
// console.log(response.data)
__storeUsers(response.data)
})
setUsers(showUsers)
console.log(users)
}, [__storeUsers, showUsers, users, setUsers])
const mapStateToProps = state => ({
showUsers: state.getUsers.users
})
const mapDispatchToProps = dispatch => ({
__storeUsers: (data) => dispatch({type: types.STORE_USERS, payload: data}),
})
This is my reducer for users
import * as types from "./types";
const initialState = {
users: []
}
const usersState = (state = initialState, action) => {
switch (action.type) {
case types.STORE_USERS:
return {
...state,
users: action.payload
}
default:
return state
}
}
export default usersState
This is for practice purpose. i am not using actionCreators right now. After this I will move the axios call to the action creator. The data that I get from above goes in loop in console. Please help.
Also if I create action creator for this, that also goes in loop. Action creator is like below:
export const UserActions = () => async (dispatch) => {
const response = await axios.get('https://jsonplaceholder.typicode.com/users')
if (response.data) {
// console.log(response.data)
dispatch({
type: types.STORE_USERS,
payload: response.data
})
} else {
// console.log("no data")
}
return response
}
And then I use it like below
const mapDispatchToProps = dispatch => ({
__storeUsers: () => dispatch(UserActions())
})
Both methods are firing loop in console.
Notice that your useEffect here is where the infinite loop occurs:
const [users, setUsers] = useState()
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/users').then(response => {
// console.log(response.data)
__storeUsers(response.data)
})
setUsers(showUsers)
console.log(users)
}, [__storeUsers, showUsers, users, setUsers])
The useEffect has been told that users is one of its dependencies and that it should re-run when this variable changes. The useEffect then changes the value of users via setUsers and it sees this update so runs again.
It looks like you're only depending on users for this console.log. Consider taking it out of the dependency list.

Redux request statuses. Reduce boilerplate

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.

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