redux-toolkit use an actionCreater in the same slice from another thunk reducer - redux

I have a redux-thunk reducer generated with createSlice from redux-toolkit called getOne.
getOne fetches from an API and dispatches actions for the loading status, (startedLoading, finishedLoading, errorLoading).
I would like to also call another actionCreater created in the same slice called insert with the resulting data. or directly update the state from the getOne reducer.
import { createSlice } from "#reduxjs/toolkit"
import { startedLoading, finishedLoading, errorLoading } from '../slices/loadingSlice'
const apiEndpoint = "/api/v1"
const fetchOptions = { headers: { "Content-Type": "application/json" } }
const createModelSlice = function (modelName) {
return createSlice({
name: modelName,
initialState: {byId: {}},
reducers: {
insert: (state, action) => {
// ...
},
getOne: (state, action) => async (dispatch) => {
// ...
try {
const response = await fetch(/* */)
// How would I update the store here?
// 1. reference the insert actionCreater somehow.
dispatch(insert({id: id, data: response))
// 2. construct the action manually
dispatch({action: `${modelName}/insert`, payload: {id: id, data: response))
// 3. Mutate the state here and rely immer. (I'm not sure exactly how that works)
state[modelName].byId[id] = response
dispatch(finishedLoading({ key: /* */ }))
} catch (error) {
dispatch(errorLoading({ key: /* */ }))
}
},
// ...
}
})
}

I missed the part of docs about slices not being able to use thunk. either way, it wouldn't work as the thunk actions don't map to a reducer but rather dispatch other actions multiple other reducers/actions.
I added thunk actions to the slice actions after creating the slice. this way I can reference the other actions
import { createSlice } from "#reduxjs/toolkit"
const slice = createSlice({
name: name,
initialState: { byId: {} },
reducers: { /* */ }
}
slice.actions.myThunkAction = payload => async (dispatch, state) => {
// ...
slice.actions.nonThunkAction({ id: id, data: data})
slice.actions.anotherNonThunkAction({ index payload.index, data: data.map( /* */ )})
}
import { createSlice } from "#reduxjs/toolkit"
import { startedLoading, finishedLoading, errorLoading } from '../slices/loadingSlice'
import encodeURLParams from '../tools/encodeURLParams'
const apiEndpoint = "/api/v1"
const fetchOptions = { headers: { "Content-Type": "application/json" } }
const createModelSlice = function (modelName) {
const slice = createSlice({
name: modelName,
initialState: { byId: {} },
reducers: {
insert: (state, action) => {
// ...
},
bulkInsert: (state, action) => {
// ...
},
}
})
slice.actions.loadMany = payload => async (dispatch, state) => {
dispatch(startedLoading({ key: /* */ }))
try {
const response = await fetch(/* */)
dispatch(slice.actions.insert(response))
dispatch(finishedLoading({ key: /* */ }))
} catch (error) {
dispatch(errorLoading({ key: /* */ }))
}
}
slice.actions.loadOne = payload => async (dispatch, state) => {
dispatch(startedLoading({ key: /* */ }))
try {
const response = await fetch(/* */)
dispatch(slice.actions.bulkInsert(response))
dispatch(finishedLoading({ key: /* */ }))
} catch (error) {
dispatch(errorLoading({ key: /* */ }))
}
}
return slice
}
export default createModelSlice

Related

How to add cases in ExtraReducer to match the actions created in currentReducer using createSlice() from #reduxjs/toolkit

Here below I have mentioned a redux slice. A fetchAllApps thunk function is created with createAsyncThunk for action 'allApps/allappsAdded/' which I dynamically got by allAppsAdded.type. When the fetchAllapps is dispatched it generated actions of type 'allApps/allappsAdded/pending', 'allApps/allappsAdded/fulfilled', 'allApps/allappsAdded/rejected' which I need to add in extraReducers to handle it by doing hardcode.Is there any way to add these action types like allAppsAdded.type programatically?. so that in future It makes easy for me to change these without redundant..
import {
configureStore,
createAsyncThunk,
createSlice
} from "#reduxjs/toolkit";
const initialState = {
apps: [],
categories: [],
loading: {
apps: false
}
};
const allappsSlice = createSlice({
name: "allapps",
initialState,
reducers: {
allappsAdded: (state, action) => {
state["apps"] = action.payload.apps;
state["categories"] = action.payload.categories;
}
},
extraReducers: {
}
});
export default () =>
configureStore({
reducer: allappsSlice.reducer
});
const { allappsAdded } = allappsSlice.actions;
const fetchAllApps = createAsyncThunk(allappsAdded.type, async () => {
console.log("ss");
setTimeout(() => ({ apps: [], categories: [] }), 2000);
});
export { allappsAdded, fetchAllApps };

TypeError: Cannot perform 'get' on a proxy that has been revoked, redux-toolkit and nextJS

I'm get the error in tittle when a action is dispatched to redux in a next application and i can't find the solution: the first action is correctly dispatched but others raises the error: TypeError: Cannot perform 'get' on a proxy that has been revoked, redux-toolkit and nextJS, you can see the project in the follow link : https://github.com/cpereiramt/BACKBONE-TEST
Below I share the mainly snippets of code and the configuration in general :
configuring store:
import {
configureStore,
EnhancedStore,
getDefaultMiddleware
} from "#reduxjs/toolkit"
import { MakeStore } from "next-redux-wrapper"
import { Env } from "../constants"
import { rootReducer, RootState } from "./reducers"
import { createWrapper } from 'next-redux-wrapper';
/**
* #see https://redux-toolkit.js.org/usage/usage-with-typescript#correct-typings-for-the-dispatch-type
*/
const middlewares = [...getDefaultMiddleware<RootState>()]
const store = configureStore({
reducer: rootReducer,
middleware: middlewares,
devTools: Env.NODE_ENV === "development",
})
const makeStore: MakeStore = (_?: RootState): EnhancedStore => store
export const wrapper = createWrapper(makeStore);
combineReducers
import { combineReducers } from "redux"
import { contactReducer } from "./contact"
/**
* Combine reducers
* #see https://redux-toolkit.js.org/usage/usage-with-typescript
*/
export const rootReducer = combineReducers({
contacts: contactReducer,
})
export type RootState = ReturnType<typeof rootReducer>
actions
import { createAsyncThunk } from "#reduxjs/toolkit";
import { Contact } from "../../model";
import { FeatureKey } from "../featureKey";
/**
* Fetch all contact action
*/
export const fetchAllContactsAction = createAsyncThunk(
`${FeatureKey.CONTACT}/fetchAll`,
async (arg: { offset: number; limit: number }) => {
const { offset, limit } = arg
const url = `/api/contact?offset=${offset}&limit=${limit}`
const result: Contact[] = await fetch(url, {
method: "get",
}).then((response: Response) => response.json())
return { contacts: result }
}
)
/**
* Fetch contact action
*/
export const fetchContactAction = createAsyncThunk(
`${FeatureKey.CONTACT}/fetch`,
async (arg: { id: number }) => {
const url = `/api/contact/${arg}`
const result: Contact = await fetch(url, {
method: "get",
}).then((response: Response) => response.json())
return { contacts: result }
}
)
/**
* Add contact action
*/
export const addContactAction = createAsyncThunk(
`${FeatureKey.CONTACT}/add`,
async (arg: { contact: Contact }) => {
const url = `/api/contact`
const result: Contact = await fetch(url, {
method: "post",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(arg),
}).then((response: Response) => response.json())
return { contacts: result }
}
)
/**
* Edit contact action
*/
export const editContactAction = createAsyncThunk(
`${FeatureKey.CONTACT}/edit`,
(arg: { contact: Contact }) => {
const { contact } = arg
const url = `/api/contact/${arg.id}`
const result: Contact = fetch(url, {
method: "put",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(contact),
}).then((response: Response) => response.json())
return { contacts: result }
}
)
/**
* Delete contact action
*/
export const deleteContactAction = createAsyncThunk(
`${FeatureKey.CONTACT}/delete`,
async (arg: { id: number }) => {
const url = `/api/contact/${arg}`
await fetch(url, {
method: "delete",
})
}
)
reducers
import { ActionReducerMapBuilder, createReducer } from "#reduxjs/toolkit"
import {
addContactAction,
deleteContactAction,
editContactAction,
fetchAllContactsAction,
fetchContactAction
} from "./action"
import { adapter, ContactState, initialState } from "./state"
/**
* CONTACT reducer
*/
export const contactReducer = createReducer(
initialState,
(builder: ActionReducerMapBuilder<ContactState>) =>
builder
.addCase(fetchAllContactsAction.pending, (state) => {
return { ...state, isFetching: true }
})
.addCase(fetchAllContactsAction.fulfilled, (state, action) => {
const { contacts } = action.payload
return adapter.setAll({ ...state, isFetching: false }, contacts)
})
.addCase(fetchAllContactsAction.rejected, (state) => {
return { ...state, isFetching: false }
})
//-------------------------------------------------------------------------------
.addCase(fetchContactAction.pending, (state, action) => {
const { id } = action.meta.arg
return { ...state, isFetching: true, selectedId: id }
})
.addCase(fetchContactAction.fulfilled, (state, action) => {
const { contacts } = action.payload
return adapter.upsertOne({ ...state, isFetching: false }, contacts)
})
.addCase(fetchContactAction.rejected, (state) => {
return { ...state, isFetching: false }
})
//-------------------------------------------------------------------------------
.addCase(addContactAction.pending, (state, action) => {
const { contact } = action.meta.arg
return { ...state, isFetching: true, selectedId: contact?.id }
})
.addCase(addContactAction.fulfilled, (state, action) => {
const { contacts } = action.payload
return adapter.addOne({ ...state, isFetching: false }, contacts)
})
.addCase(addContactAction.rejected, (state) => {
return { ...state, isFetching: false }
})
//-------------------------------------------------------------------------------
.addCase(editContactAction.pending, (state, action) => {
const { contact } = action.meta.arg
return { ...state, isFetching: true, selectedId: contact?.id }
})
.addCase(editContactAction.fulfilled, (state, action) => {
const { contacts } = action.payload
return adapter.updateOne(
{ ...state, isFetching: false },
{
id: contacts.id,
changes: contacts,
}
)
})
.addCase(editContactAction.rejected, (state) => {
return { ...state, isFetching: false }
})
//-------------------------------------------------------------------------------
.addCase(deleteContactAction.pending, (state, action) => {
const { id } = action.meta.arg
return { ...state, isFetching: true, selectedId: id }
})
.addCase(deleteContactAction.fulfilled, (state, action) => {
const { id } = action.meta.arg
return adapter.removeOne({ ...state, isFetching: false }, id)
})
.addCase(deleteContactAction.rejected, (state) => {
return { ...state, isFetching: false }
})
)
selectors
import { createSelector } from "#reduxjs/toolkit"
import { RootState } from "../reducers"
import { adapter, ContactState } from "./state"
const { selectAll, selectEntities } = adapter.getSelectors()
const featureStateSelector = (state: RootState) => state.contacts
const entitiesSelector = createSelector(featureStateSelector, selectEntities)
/**
* isFetching selector
*/
export const isFetchingSelector = createSelector(
featureStateSelector,
(state: ContactState) => state?.isFetching
)
/**
* selectedId selector
*/
export const selectedIdSelector = createSelector(
featureStateSelector,
(state: ContactState) => state?.selectedId
)
/**
* all contact selector
*/
export const allContactSelector = createSelector(featureStateSelector, selectAll)
/**
* contact selector
*/
export const contactSelector = createSelector(
entitiesSelector,
selectedIdSelector,
(entities, id) => (id ? entities[id] || null : null)
)
states
import { createEntityAdapter, EntityState } from "#reduxjs/toolkit"
import { Contact } from "../../model"
export interface ContactState extends EntityState<Contact> {
isFetching: boolean
selectedId: number | null
}
export const adapter = createEntityAdapter<Contact>({
selectId: (contacts: Contact) => contacts.id,
})
export const initialState: ContactState = adapter.getInitialState({
isFetching: false,
selectedId: null,
})
And the _app file
import CssBaseline from "#material-ui/core/CssBaseline";
import { ThemeProvider } from "#material-ui/styles";
import { NextPageContext } from 'next';
import App from "next/app";
import React from "react";
import { MuiTheme } from "../components/MuiTheme";
import { Store } from '../redux/store';
import { wrapper } from "../store/configureStore";
import "../styles/main.css";
interface AppContext extends NextPageContext {
store: Store;
}
class MyApp extends App<AppContext> {
componentDidMount() {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector("#jss-server-side")
jssStyles?.parentNode?.removeChild(jssStyles)
}
render() {
const { Component, ...props } = this.props;
return (
<ThemeProvider theme={MuiTheme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<Component {...props} />
</ThemeProvider>
)
}
}
export default wrapper.withRedux(MyApp);
And in index file the action fetchAllContacts() work without problems.
import React, { useEffect } from "react";
import ContactTable from "../components/Table/";
import { useContact } from "../hooks";
import { Contact } from '../model/Contact';
import appStyles from "./indexStyles";
type Props = {}
function Index(props: Props) {
const { fetchAllContacts } = useContact();
const [contacts, setContacts] = React.useState<Contact[]>([])
useEffect(() => {
const results = fetchAllContacts();
results.then(data => console.log(data));
results.then(data => setContacts(data.contacts));
}, [])
const classes = appStyles(props)
return (
<div className={classes.indexBackground}>
<div className={classes.indexTabletDiv}>
<ContactTable contacts={contacts} />
</div>
</div>
);
}
export default Index
But when I try to use the action addContact() in another component the error is raised
page add
import { Button, createStyles, InputLabel, makeStyles, TextField, Theme } from "#material-ui/core";
import router from "next/router";
import React from "react";
import { useContact } from "../../hooks";
const useStyles = makeStyles((_: Theme) =>
createStyles({
root: {},
})
)
type Props = { }
const AddContact = (props: Props) => {
const { addContact } = useContact();
const newContact = {
id:'455666gghghttttytyty',
firstName: 'clayton',
lastName: 'pereira',
email: 'cpereiramt#gmail.com',
phone: '5565992188269',
}
const handleCreateContact = () => {
addContact(newContact);
}
const { } = props
return (
<div style={{margin: '10px', display: 'flex', justifyContent: 'space-between', wrap: 'wrap', flexDirection:'column'}}>
<>
<InputLabel>Name </InputLabel><TextField />
<InputLabel>Last Name </InputLabel><TextField />
<InputLabel>Email </InputLabel><TextField />
<div>
<Button variant="outlined" color="primary" onClick={() => handleCreateContact(newContact)} >
Create Contact</Button>
<Button variant="outlined" color="primary" onClick={() => router.push('/')} >
Back</Button>
</div>
</>
</div>
)
}
export default AddContact;
I think I see the issue.
First, the actual error message is Immer telling you that something is trying to modify the Proxy-wrapped state in a reducer, but long after the reducer has actually finished running. Normally that's impossible, because reducers are synchronous. So, there has to be some kind of async behavior going on.
The case reducers themselves seem basically okay, and mostly look like this:
.addCase(fetchAllContactsAction.pending, (state) => {
return { ...state, isFetching: true }
})
I'll point out that Immer lets you write state.isFetching = true instead, so you don't have to do object spreads :) But this code should run fine, and it's synchronous. So, what's the problem?
You didn't actually describe which actions are causing errors, so I'm having to guess. But, I think it's in one of the async thunks, and specifically, here:
export const editContactAction = createAsyncThunk(
`${FeatureKey.CONTACT}/edit`,
(arg: { contact: Contact }) => {
const { contact } = arg
const url = `/api/contact/${arg.id}`
// PROBLEM 1
const result: Contact = fetch(url, {
method: "put",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(contact),
}).then((response: Response) => response.json())
// PROBLEM 2
return { contacts: result }
}
Notice the line const result: Contact = fetch(). This doesn't have any await in there So, this is going to end up returning a Promise and saving it as result, and then that Promise ends up being returned as the contacts field.
I think that the object with the promise is being put into Redux state, wrapped by an Immer proxy, and then modified sometime later, and that's what's causing the error. But I'm not 100% sure because I don't know which actions are actually involved.

How to avoid infinite loop in saga?

I cant understand why the code above runs infinite loop. My actions are different. Could u please take a look?
import { call, put, takeLatest ,delay} from 'redux-saga/effects'
import { saveFetchedDevices,fetchDevicesRequest } from './devicesRedux'
import axios from "axios"
const getDevices= () => {
return axios.get("http://localhost:3131/devices")
}
function* fetchDevicesHandler(action) {
try {
const response = yield call(getDevices);
yield delay(3000);
yield put(saveFetchedDevices(response.data));
} catch (e) {
console.log(e)
}
}
function* mySaga() {
yield takeLatest(fetchDevicesRequest,fetchDevicesHandler);
}
export default mySaga;
fetchDevicesRequest is dispatched from button. Then loop starts
delay is just to slow down infinite loop
this is my redux file
const createActionName = function(name) {
return `app/devices/${name}`
}
const ADD_DEVICE = createActionName("ADD_DEVICE");
const UPDATE_DEVICE = createActionName("UPDATE_DEVICE");
const REMOVE_DEVICE = createActionName("REMOVE_DEVICE");
const SAVE_FETCHED_DEVICES = createActionName("SAVE_FETCHED_DEVICES");
const FETCH_DEVICES_REQUEST= createActionName("FETCH_DEVICES_REQUEST");
//action creators
export const addDevice = payload => ({ type: ADD_DEVICE, payload })
export const updateDevice = payload => ({ type: UPDATE_DEVICE, payload })
export const removeDevice = payload => ({ type: REMOVE_DEVICE, payload })
export const saveFetchedDevices = payload => ({ type: SAVE_FETCHED_DEVICES, payload })
export const fetchDevicesRequest = payload => ({ type: FETCH_DEVICES_REQUEST })
//selectors
export const getAllDevices = state => state.devices.data;
const reducer = function(statePart = [], action = {}) {
switch(action.type) {
case SAVE_FETCHED_DEVICES:
return { data: action.payload }
case ADD_DEVICE:
return { ...statePart, data: [ ...statePart.data, action.payload ] }
case UPDATE_DEVICE:
return { ...statePart, data: [ ...statePart.data.map((device)=>device.id===action.payload.id?action.payload:device)] }
case REMOVE_DEVICE:
return { ...statePart, data: [ ...statePart.data.filter((device)=>device.id!==action.payload.id)] }
default:
return statePart
}
}
export default reducer

Dispatch in Redux ToolKit not Changing state

I am trying to pass data to my redux store with the help of redux toolkit, but I'm unable to do so. I double checked that there is no problem with my data. Here is my code:
//USER DETAILS
export const userDetailsSlice = createSlice({
name: 'userDetails',
initialState: { loading: 'idle', user: {} },
reducers: {
userDetailsLoading(state) {
if (state.loading === 'idle') {
state.loading = 'pending';
}
},
userDetailsReceived(state, action) {
if (state.loading === 'pending') {
state.loading = 'idle';
state.user = action.payload;
}
},
userDetailsError(state, action) {
if (state.loading === 'pending') {
state.loading = 'idle';
state.error = action.payload;
}
},
},
});
export const {
userDetailsLoading,
userDetailsReceived,
userDetailsError,
} = userDetailsSlice.actions;
export const getUserDetails = (id) => async (dispatch, getState) => {
dispatch(userRegisterLoading());
try {
const {
userLogin: { userInfo },
} = getState();
const config = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${userInfo.token}`,
},
};
const { data } = await axios.get(`/api/users/${id}`, config);
console.log(data);
dispatch(userDetailsReceived(data));
} catch (error) {
dispatch(userDetailsError(error.toString()));
}
};
I console logged my data right before the dispatch action: so the data is definitely there. Therefore I think there's something wrong with my code. Here is my code for my redux store:
import { configureStore } from '#reduxjs/toolkit';
import { photoListSlice, photoCreateSlice } from './photo';
import { userRegisterSlice, userLoginSlice, userDetailsSlice } from './user';
const reducer = {
userRegister: userRegisterSlice.reducer,
userLogin: userLoginSlice.reducer,
userDetails: userDetailsSlice.reducer,
};
const store = configureStore({ reducer });
export default store;
As seen from this photo: the action userDetailsReceived was called but the state for user did not change.
Any help or hint would be greatly appreciated!!
I made a typo on the first dispatch call of getUserDetails function, I dispatched userRegisterLoading instead of userDetailsLoading... Now it works fine after the change.
export const getUserDetails = (id) => async (dispatch, getState) => {
dispatch(userDetailsLoading());
try {
const {
userLogin: { userInfo },
} = getState();
const config = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${userInfo.token}`,
},
};
const { data } = await axios.get(`/api/users/${id}`, config);
console.log(data);
dispatch(userDetailsReceived(data));
} catch (error) {
dispatch(userDetailsError(error.toString()));
}
};

useEffect infinite loop with dependency array on redux dispatch

Running into an infinite loop when I try to dispatch an action which grabs all recent posts from state.
I have tried the following in useEffect dependency array
Object.values(statePosts)
useDeepCompare(statePosts)
passing dispatch
omitting dispatch
omitting statePosts
passing statePosts
doing the same thing in useCallback
a lot of the suggestions came from here
I have verified that data correctly updates in my redux store.
I have no idea why this is still happening
my component
const dispatch = useDispatch()
const { user } = useSelector((state) => state.user)
const { logs: statePosts } = useSelector((state) => state.actionPosts)
const useDeepCompare = (value) => {
const ref = useRef()
if (!_.isEqual(ref.current, value)) {
ref.current = value
}
return ref.current
}
useEffect(() => {
dispatch(getActionLogsRest(user.email))
}, [user, dispatch, useDeepCompare(stateLogs)])
actionPosts createSlice
const slice = createSlice({
name: 'actionPosts',
initialState: {
posts: [],
},
reducers: {
postsLoading: (state, { payload }) => {
if (state.loading === 'idle') {
state.loading = 'pending'
}
},
postsReceived: (state, { payload }) => {
state.posts = payload
},
},
})
export default slice.reducer
const { postsReceived, postsLoading } = slice.actions
export const getActionPostsRest = (email) => async (dispatch) => {
try {
dispatch(postsLoading())
const { data } = await getUserActionPostsByUser({ email })
dispatch(postsReceived(data.userActionPostsByUser))
return data.userActionPostsByUser
} catch (error) {
throw new Error(error.message)
}
}
Remove dispatch from dependencies.
useEffect(() => {
dispatch(getActionLogsRest(user.email))
}, [user, dispatch, useDeepCompare(stateLogs)])
you cannot use hook as dependency and by the way, ref.current, is always undefined here
const useDeepCompare = (value) => {
const ref = useRef()
if (!_.isEqual(ref.current, value)) {
ref.current = value
}
return ref.current
}
because useDeepCompare essentially is just a function that you initiate (together with ref) on each call, all it does is just returns value. That's where the loop starts.

Resources