next-redux-wrapper HYDRATION failed - next.js

I am trying to integrate next-redux-wrapper to Next.js RTK project. When invoking async action from getServerSideProps, I am getting state mismatch error (see the image below).
When I dispatch action from client side (increment/decrement), everything works well. I think issue is related to HYDRATION but so far all my efforts have failed.
I tried mapping redux state to props, storing props in component state, added if statements to check values but nothing seem to work. I've been stuck on this for 2 weeks. I'm not sure what else to try next.
"next": "12.3.1",
"next-redux-wrapper": "^8.0.0",
"react":
"18.2.0",
"react-redux": "^8.0.4"
store/store.js
import { configureStore, combineReducers } from "#reduxjs/toolkit";
import counterReducer from "./slices/counterSlice";
import { createWrapper, HYDRATE } from "next-redux-wrapper";
const combinedReducer = combineReducers({
counter: counterReducer,
});
const reducer = (state, action) => {
if (action.type === HYDRATE) {
const nextState = {
...state, // use previous state
...action.payload, // apply delta from hydration
};
return nextState;
} else {
return combinedReducer(state, action);
}
};
export const makeStore = () =>
configureStore({
reducer,
});
export const wrapper = createWrapper(makeStore, { debug: true });
store/slices/counterSlice.js
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import axios from "axios";
const initialState = {
value: 0,
data: { quote: "" },
pending: false,
error: false,
};
export const getKanyeQuote = createAsyncThunk(
"counter/kanyeQuote",
async () => {
const respons = await axios.get("https://api.kanye.rest/");
return respons.data;
}
);
export const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(getKanyeQuote.pending, (state) => {
state.pending = true;
})
.addCase(getKanyeQuote.fulfilled, (state, { payload }) => {
state.pending = false;
state.data = payload;
})
.addCase(getKanyeQuote.rejected, (state) => {
state.pending = false;
state.error = true;
});
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
pages/index.js
import React, { useState } from "react";
import { useSelector, useDispatch, connect } from "react-redux";
import {
decrement,
increment,
getKanyeQuote,
} from "../store/slices/counterSlice";
import { wrapper } from "../store/store";
function Home({ data }) {
const count = useSelector((state) => state.counter.value);
// const { data, pending, error } = useSelector((state) => state.counter);
const dispatch = useDispatch();
const [quote, setQuote] = useState(data.quote);
return (
<div style={{ display: "flex", flexDirection: "column" }}>
{/* <span>{pending && <p>Loading...</p>}</span>
<span>{error && <p>Oops, something went wrong</p>}</span> */}
<div>{quote}</div>
<span>Count: {count}</span>
<div>
<button
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
Increment
</button>
<button
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
Decrement
</button>
</div>
</div>
);
}
export const getServerSideProps = wrapper.getServerSideProps(
(store) =>
async ({ req, res, ...etc }) => {
console.log(
"2. Page.getServerSideProps uses the store to dispatch things"
);
await store.dispatch(getKanyeQuote());
}
);
function mapStateToProps(state) {
return {
data: state.counter.data,
};
}
export default connect(mapStateToProps)(Home);
Errors in console

This might stem from a known issue where next-redux-wrapper 8 hydrates too late. Please try downgrading to version 7 for now and see if that resolves the problem.

Related

Store State Issues - Next Redux Wrapper vs Redux ToolKit

FYI: Everything is working fine and all the flows are working as expected but logs are confusing.
I am using Redux ToolKit for managing redux state & using Next Redux Wrapper for Server Side rendering and caching queries.
CartSlice.js
import { createSlice } from '#reduxjs/toolkit';
export const cartSlice = createSlice({
name: 'cart',
initialState: { data: [] },
reducers: {
addToCart: (state, action) => {
const itemInCart = state.data.find((item) => item.id === action.payload.id);
if (itemInCart) {
itemInCart.quantity += action.payload.quantity;
} else {
state.data.push({ ...action.payload });
}
}
},
});
export const cartReducer = cartSlice.reducer;
export const { addToCart } = cartSlice.actions;
ServiceApi.js
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query/react'
import { HYDRATE } from 'next-redux-wrapper'
export const serviceApi = createApi({
reducerPath: 'serviceApi',
baseQuery: fetchBaseQuery({ baseUrl: '<base-url>' }),
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === HYDRATE) {
return action.payload[reducerPath]
}
},
endpoints: (builder) => ({
getItemById: builder.query({ query: (id) => `id/${id}` }),
getItems: builder.query({ query: () => `/` }),
}),
})
export const { getRunningQueriesThunk } = serviceApi.util
export const { useGetItemByIdQuery, useGetItemsQuery } = serviceApi
export const { getItemById, getItems } = serviceApi.endpoints;
store.js
import { configureStore } from '#reduxjs/toolkit'
import { serviceApi } from './serviceApi'
import { createWrapper } from "next-redux-wrapper";
import { cartSlice } from "./cartSlice";
export const store = configureStore({
reducer: {
[cartSlice.name]: cartSlice.reducer,
[serviceApi.reducerPath]: serviceApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(serviceApi.middleware),
})
const makeStore = () => store
export const wrapper = createWrapper(makeStore, {debug: true});
Using getServerSideProps
export const getServerSideProps = wrapper.getServerSideProps(
(store) => async (context) => {
const { id } = context.query;
store.dispatch(serviceApi.endpoints.getItemById.initiate(parseInt(id)));
await Promise.all(store.dispatch(serviceApi.util.getRunningQueriesThunk()));
return {
props: {},
};
}
);
And using wrappedStore
const { store, props } = wrapper.useWrappedStore(pageProps);
On the UI flows everything is working as expected and i am able to add items in the cart and i can see the store state is getting updated by looking the UI.
But the logs from next redux wrapper is confusing:

using dispatch crash the store state

i want to moving to #reduxjs/toolkit, actually my app use the following packages:
"#reduxjs/toolkit": "^1.8.3",
"react-redux": "^7.2.2",
"#types/react-redux": "^7.1.20",
actually i use this type of action for my redux store :
export function action<T extends string, P>(type: T, payload?: Partial<P>) {
return { type, ...payload };
}
i change my configure store like this :
const sagaMiddleware = createSagaMiddleware();
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) => [...getDefaultMiddleware(), sagaMiddleware],
});
sagaMiddleware.run(rootSaga);
export default store;
type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
i've created a simple button component like this :
interface FavoriteBtnProps {
className: string;
}
function FavoriteBtn({ className }: FavoriteBtnProps) {
const { windowDisplay } = useAppSelector(getFavoritePlacesState);
const dispatch = useDispatch();
const handleClick = () => {
dispatch(setWindowDisplay(!windowDisplay));
};
return (
<div className={className} onClick={handleClick}>
<FontAwesomeIcon icon={faBookmark} />
</div>
);
}
export default FavoriteBtn;
it work's fine but need to use generic redux dispatch (Dispatch()). the useAppDispatch created don't work with old actions
now i want to add in a parent component of FavoriteBtn
const { leftPanelDisplay } = useAppSelector(getPanelState);
const { windowDisplay } = useAppSelector(getFavoritePlacesState);
console.log(windowDisplay);
const dispatch = useDispatch();
const handleCollapseIcon = () => {
dispatch(
leftPanelDisplay
? panelsActions.leftPanel.collapse()
: panelsActions.leftPanel.uncollapse()
);
// dispatch(setWindowDisplay(!windowDisplay));
};
this code works fine the console log give me the good value, but if i uncomment
dispatch(setWindowDisplay(!windowDisplay));
i have this error :
Uncaught TypeError: Cannot destructure property 'windowDisplay' of
'Object(...)(...)' as it is undefined.
and i don't see favoriteAndHistoric state in the reduxDevTools
only the state created by the toolkit crashes.
here my slice
const favoritePlacesSlice = createSlice({
name: 'favoritePlaces',
initialState,
reducers: {
setWindowDisplay: (state, action: PayloadAction<boolean>) => {
state.windowDisplay = action.payload;
},
setFavorites: (state, action: PayloadAction<FavoriteSearch[]>) => {
state.favoritePlaces = action.payload;
},
},
extraReducers: (builder) => {
// GET ALL
builder.addCase(fetchFavoriteSearchs.pending, (state, action) => {
state.apiStatus = APIStatus.PENDING;
});
builder.addCase(fetchFavoriteSearchs.fulfilled, (state, action) => {
state.apiStatus = APIStatus.FULFILLED;
state.favoritePlaces = action.payload ?? [];
});
// ADD
builder.addCase(addFavoriteSearch.pending, (state, action) => {
state.apiStatus = APIStatus.PENDING;
});
builder.addCase(addFavoriteSearch.fulfilled, (state, action) => {
state.apiStatus = APIStatus.FULFILLED;
state.favoritePlaces = state.favoritePlaces.concat(action.payload);
});
// DELETE
builder.addCase(deleteFavoriteSearch.pending, (state, action) => {
state.apiStatus = APIStatus.PENDING;
});
builder.addCase(deleteFavoriteSearch.fulfilled, (state, action) => {
state.apiStatus = APIStatus.FULFILLED;
state.favoritePlaces = state.favoritePlaces.filter(
(f) => f.idIri !== action.payload
);
});
},
});
export const { setFavorites, setWindowDisplay } = favoritePlacesSlice.actions;
export default favoritePlacesSlice.reducer;
my rootReducer :
import { combineReducers } from 'redux';
...
import favoritePlacesReducer from '../features/favoritePlaces/favoritePlacesSlice';
const rootReducer = combineReducers({
...
favoritePlacesReducer,
});
export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;
i've allready tryed to reinstall #reduxjs/toolkit and react-redux from scratch but don't fix the problem and more, i have type errors on createAsyncThunk.
why it works on a componant and not on an other?
thx for help

useEffect hook problem, use redux to get data from backend to frontend

Hello i am new to programming, and i have learn to use MERN to make a sign up,i have no issue with backend but when i tried to use redux ,i have this problem in the frontend
enter image description here
This is the code for the SignInScreen.js
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { login } from '../actions/userAction'
export const SignInScreen = (props, history) => {
const navigate = useNavigate
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const submitHandler = (e) => {
e.preventDefault()
dispatch(login(email, password))
}
const { search } = useLocation()
const redirectInUrl = new URLSearchParams(search).get('redirect')
const redirect = redirectInUrl ? redirectInUrl : '/'
const userlogin = useSelector((state) => state.userlogin)
const { userInfo, loading, error } = userlogin
const dispatch = useDispatch()
useEffect(() => {
if (userInfo) {
navigate(redirect)
}
}, [navigate, userInfo, redirect])
I dont know what wrong with the code,but i do know that it connected with the redux store which have reducer,action and constant..this is for the redux store.js
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import {
userLoginReducer,
} from './reducers/userReducer'
const userInfo = localStorage.getItem('userInfo')
? JSON.parse(localStorage.getItem('userInfo'))
: null
const initialState = {
userLogin: { userInfo },
}
const reducer = combineReducers({
userLogin: userLoginReducer,
})
const middleware = [thunk]
const store = createStore(
reducer,
initialState,
compose(applyMiddleware(...middleware))
)
export default store
This is for constant
export const USER_LOGIN_REQUEST = 'USER_LOGIN_REQUEST'
export const USER_LOGIN_SUCCESS = 'USER_LOGIN_SUCCESS'
export const USER_LOGIN_FAIL = 'USER_LOGIN_FAIL'
export const USER_LOGOUT = 'USER_LOGOUT'
this is userReducer.js
import {
USER_LOGIN_REQUEST,
USER_LOGIN_SUCCESS,
USER_LOGIN_FAIL,
USER_LOGOUT,
} from '../constants/userConstant'
function userLoginReducer(state = {}, action) {
switch (action.type) {
case USER_LOGIN_REQUEST:
return { loading: true }
case USER_LOGIN_SUCCESS:
return { loading: false, userInfo: action.payload }
case USER_LOGIN_FAIL:
return { loading: false, error: action.payload }
case USER_LOGOUT:
return {}
default:
return state
}
}
export userLoginReducer
and lastly for user.js
import Axios from 'axios'
import {
USER_LOGIN_REQUEST,
USER_LOGIN_SUCCESS,
USER_LOGIN_FAIL,
} from '../constants/userConstant'
const login = (email, password) => async (dispatch) => {
try {
dispatch({ type: USER_LOGIN_REQUEST })
const config = { headers: { 'Content-Type': 'application/json' } }
const { data } = await Axios.post(
'/api/users/login',
{ email, password },
config
)
dispatch({ type: USER_LOGIN_SUCCESS, payload: data })
localStorage.setItem('userInfo', JSON.stringify(data))
} catch (error) {
dispatch({
type: USER_LOGIN_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
})
}
}
export login
I just want to get the data into the userInfo but it dont recognized them and said it is a TypeError..I hope u can help me with this..im using the redux latest version
The problem is in userLoginReducer. Each reducer should return a complete new copy of the store.
If you return just the changes, the object you return replaces the entire state.
For example in this code:
switch (action.type) {
case USER_LOGIN_REQUEST:
return { loading: true };
}
The state { userLogin: { userInfo } } will be replaced with { loading: true }. Then you will not have userLogin anymore in the state. That's why you get the error.
To overcome this problem, spread the previous state in returned object (for all actions):
switch (action.type) {
case USER_LOGIN_REQUEST:
return { ....state, loading: true }; // ...state copies exist state to the new copy of state
}
Note: To easily solve similar bugs in the future, I recommend to use redux devtools extension. It is a great extension for debugging and look at changes in redux store.
I have tried that but still cannot fix my problem,this is the rest of my signinScreen,js
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { login } from '../actions/userAction'
export const SignInScreen = (props, history) => {
const navigate = useNavigate
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const submitHandler = (e) => {
e.preventDefault()
dispatch(login(email, password))
}
const { search } = useLocation()
const redirectInUrl = new URLSearchParams(search).get('redirect')
const redirect = redirectInUrl ? redirectInUrl : '/'
const userlogin = useSelector((state) => state.userlogin)
const { userInfo, loading, error } = userlogin
const dispatch = useDispatch()
useEffect(() => {
if (userInfo) {
navigate(redirect)
}
}, [navigate, userInfo, redirect])
return (
<Container className="small-container">
<h1 className="my-3">Sign In</h1>
<Form onSubmit={submitHandler}>
<Form.Group className="mb-3" controlId="email">
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
required
onChange={(e) => setEmail(e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-3" controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
required
onChange={(e) => setPassword(e.target.value)}
/>
</Form.Group>
<div className="mb-3">
<Button type="submit">Sign In</Button>
</div>
<div className="mb-3">
New Customer?{' '}
<Link to={`/signup?redirect=${redirect}`}>Create new account</Link>
</div>
</Form>
</Container>
)
}
it still show the same error like before,i have no idea to solve this

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.

Why, while using useEffect() and .then() in Redux, I get an Error: Actions must be plain objects. Use custom middleware for async actions

using Redux and am now straggling with a signin and signout button while using oauth.
When I press on the button to logIn, the popup window appears and I can choose an account. But in the meantime the webpage throws an error.
I got the following error as stated in the title:
Error: Actions must be plain objects. Use custom middleware for async actions.
I am using hooks, in this case useEffect().then() to fetch the data.
1) Why?
2) Also do not know, why I am getting a warning: The 'onAuthChange' function makes the dependencies of useEffect Hook (at line 35) change on every render. Move it inside the useEffect callback. Alternatively, wrap the 'onAuthChange' definition into its own useCallback() Hook react-hooks/exhaustive-deps
Here is my code:
GoogleAuth.js
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { signIn, signOut } from "../actions";
const API_KEY = process.env.REACT_APP_API_KEY;
const GoogleAuth = () => {
const isSignedIn = useSelector((state) => state.auth.isSignedIn);
console.log("IsSignedIn useSelector: " + isSignedIn);
const dispatch = useDispatch();
const onAuthChange = () => {
if (isSignedIn) {
dispatch(signIn());
} else {
dispatch(signOut());
}
};
useEffect(
() => {
window.gapi.load("client:auth2", () => {
window.gapi.client
.init({
clientId: API_KEY,
scope: "email"
})
.then(() => {
onAuthChange(window.gapi.auth2.getAuthInstance().isSignedIn.get());
console.log("isSignedIn.get(): " + window.gapi.auth2.getAuthInstance().isSignedIn.get());
window.gapi.auth2.getAuthInstance().isSignedIn.listen(onAuthChange);
});
});
},
[ onAuthChange ]
);
const onSignInOnClick = () => {
dispatch(window.gapi.auth2.getAuthInstance().signIn());
};
const onSignOutOnClick = () => {
dispatch(window.gapi.auth2.getAuthInstance().signOut());
};
const renderAuthButton = () => {
if (isSignedIn === null) {
return null;
} else if (isSignedIn) {
return (
<button onClick={onSignOutOnClick} className="ui red google button">
<i className="google icon" />
Sign Out
</button>
);
} else {
return (
<button onClick={onSignInOnClick} className="ui red google button">
<i className="google icon" />
Sign In with Google
</button>
);
}
};
return <div>{renderAuthButton()}</div>;
};
export default GoogleAuth;
reducer/index.js
import { combineReducers } from "redux";
import authReducer from "./authReducer";
export default combineReducers({
auth: authReducer
});
reducers/authReducer.js
import { SIGN_IN, SIGN_OUT } from "../actions/types";
const INITIAL_STATE = {
isSignedIn: null
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case SIGN_IN:
return { ...state, isSignedIn: true };
case SIGN_OUT:
return { ...state, isSignedIn: false };
default:
return state;
}
};
actions/index.js
import { SIGN_IN, SIGN_OUT } from "./types";
export const signIn = () => {
return {
type: SIGN_IN
};
};
export const signOut = () => {
return {
type: SIGN_OUT
};
};
types.js
export const SIGN_IN = "SIGN_IN";
export const SIGN_OUT = "SIGN_OUT";
The reason of the first error is that, inside both onSignInOnClick and onSignInOnClick, dispatch() receives a Promise (since window.gapi.auth2.getAuthInstance().signIn() returns a Promise).
There are different solution to handle effects in redux, the simplest are redux promise or redux thunk.
Otherwise you can dispatch the { type: SIGN_IN } action, and write a custom middleware to handle it.
The reason of the second error, is that the onAuthChange is redefined on every render, as you can see here:
const f = () => () => 42
f() === f() // output: false
Here's a possible solution to fix the warning:
useEffect(() => {
const onAuthChange = () => {
if (isSignedIn) {
dispatch(signIn())
} else {
dispatch(signOut())
}
}
window.gapi.load('client:auth2', () => {
window.gapi.client
.init({
clientId: API_KEY,
scope: 'email',
})
.then(() => {
onAuthChange(window.gapi.auth2.getAuthInstance().isSignedIn.get())
console.log(
'isSignedIn.get(): ' +
window.gapi.auth2.getAuthInstance().isSignedIn.get(),
)
window.gapi.auth2.getAuthInstance().isSignedIn.listen(onAuthChange)
})
})
}, [isSignedIn])

Resources