How to use redux-persist with toolkit and next-redux-wrapper? - reactjs

I'm having trouble with redux-toolkit, redux-persist, and next-redux-wrapper configuration. I've tried to make persist for redux state but it doesn't run redux actions which should save state to local storage.
My store.ts file.
import {
Action,
combineReducers,
configureStore,
ThunkAction,
getDefaultMiddleware,
} from '#reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { createWrapper } from 'next-redux-wrapper';
import taskReducer from './reducers/taskReducer';
import projectReducer from './reducers/projectReducer';
import workplaceReducer from './reducers/workplaceReducer';
import userReducer from './reducers/userReducer';
import trackTaskReducer from './reducers/trackTaskReducer';
import chatRoomReducer from './reducers/chatRoomReducer';
import messageReducer from './reducers/messageReducer';
const rootReducer = combineReducers({
taskReducer,
projectReducer,
workplaceReducer,
userReducer,
trackTaskReducer,
chatRoomReducer,
messageReducer,
});
const persistConfig = {
key: 'root',
storage,
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
const customizedMiddleware = getDefaultMiddleware({
serializableCheck: false,
});
export const setupStore = () => {
return configureStore({
reducer: persistedReducer,
middleware: customizedMiddleware,
});
};
export type AppStore = ReturnType<typeof setupStore>;
export type AppState = ReturnType<AppStore['getState']>;
export type AppDispatch = AppStore['dispatch'];
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
AppState,
unknown,
Action
>;
export const wrapper = createWrapper<AppStore>(setupStore);
My app.tsx file
import React from 'react';
import AdapterDateFns from '#mui/lab/AdapterDateFns';
import { PersistGate } from 'redux-persist/integration/react';
import { persistStore } from 'redux-persist';
import { useRouter } from 'next/router';
import type { AppProps } from 'next/app';
import { setupStore, wrapper } from '../store/store';
export default wrapper.withRedux(function MyApp({
Component,
pageProps,
}: AppProps) {
const persistor = persistStore(setupStore());
return (
<PersistGate persistor={persistor} loading={<div>Loading</div>}>
<Component {...pageProps} />
</PersistGate>
);
});
It's saving the initial state to local storage but it isn't saving future changes to the state.
What am I doing wrong?

So i found solution for me:
import {
Action,
combineReducers,
configureStore,
ThunkAction,
getDefaultMiddleware,
} from '#reduxjs/toolkit';
import { persistReducer, persistStore } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
const rootReducer = combineReducers({
// your reducers
});
const persistConfig = {
key: 'root',
storage,
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
const customizedMiddleware = getDefaultMiddleware({
serializableCheck: false,
});
export const setupStore = () => {
return configureStore({
reducer: persistedReducer,
middleware: customizedMiddleware,
});
};
export const persistedStore = () => persistStore(setupStore());
export type AppStore = ReturnType<typeof setupStore>;
export type AppState = ReturnType<AppStore['getState']>;
export type AppDispatch = AppStore['dispatch'];
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
AppState,
unknown,
Action
>;
export const store = setupStore();
Warning: your reducers must have this extra reducer like that:
extraReducers: {
[HYDRATE]: (state, action) => {
return {
...state,
...action.payload.chatRoom,
};
},
Also i don't know if it will work with SSR because i don't need that feature in my case

Related

How to unwrapResult for all dispatched actions with redux-toolkit

I'm using redux-toolkit and would like to cut down on code duplication by not having to call unwrapResult after every dispatched asynchronous action. I am handling errors for all dispatched actions via displaying a toast message to the user so I'd need to unwrap according to redux-toolkit docs.
Ideally, I'd like to wrap the dispatch function returned from the useDispatch() function from react-redux using a hook something like the following...
// Example of wrapped dispatch function
function SomeComponent() {
const dispatch = useWrappedAppDispatch()
return (
<button
onClick={() =>
dispatch(myAsyncAction())
.catch((_err) =>
toast("Error processing action")
)
}
></button>
)
}
// configureStore.ts
import { configureStore } from "#reduxjs/toolkit"
import { useDispatch } from "react-redux"
import { persistReducer } from "redux-persist"
import storage from "redux-persist/lib/storage"
import { logger } from "../middleware/logger"
import rootReducer from "./reducer"
const persistConfig = {
key: "root",
storage,
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
immutableCheck: false,
}).concat(logger),
})
export { store }
export type AppDispatch = typeof store.dispatch
export const useAppDispatch = () => useDispatch<AppDispatch>()
export type RootState = ReturnType<typeof rootReducer>
export interface GetState {
getState: () => RootState
}
This is possible. To do this we will create a custom hook useAppDispatchUnwrap and return a wrapped redux-toolkit function which will call unwrapResult on all dispatched actions. Just note that by unwrapping all dispatched actions you have to handle any exceptions that are thrown otherwise the app will crash due to uncaught exceptions.
// useAppDispatchUnwrap.ts
import { AsyncThunkAction, unwrapResult } from "#reduxjs/toolkit"
import { AppDispatch, useAppDispatch } from "../store/config/configureStore"
export default function useAppDispatchUnwrap() {
const dispatch = useAppDispatch()
async function dispatchUnwrapped<R extends any>(
action: AsyncThunkAction<R, any, any>
): Promise<R> {
return dispatch(action).then(unwrapResult)
}
return dispatchUnwrapped
}
// configureStore.ts
import { configureStore } from "#reduxjs/toolkit"
import { useDispatch } from "react-redux"
import { persistReducer } from "redux-persist"
import storage from "redux-persist/lib/storage"
import { logger } from "../middleware/logger"
import rootReducer from "./reducer"
const persistConfig = {
key: "root",
storage,
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
immutableCheck: false,
}).concat(logger),
})
export { store }
export type AppDispatch = typeof store.dispatch
export const useAppDispatch = () => useDispatch<AppDispatch>()
export type RootState = ReturnType<typeof rootReducer>
export interface GetState {
getState: () => RootState
}
// Example of using wrapped dispatch function
import useAppDispatchUnwrap from "./useAppDispatchUnwrap";
function SomeComponent() {
const dispatch = useAppDispatchUnwrap();
// All dispatched actions are now automatically unwrapped using redux-toolkit "unwrapResult" function.
return (
<button
onClick={() =>
dispatch(myAsyncAction())
.catch((_err) =>
toast("Error processing action")
)
}
></button>
)
}

Issue using selector after converting to configureStore Redux Toolkit

I've just converted my reducer from createStore to configure store and now my selectors that I think should be working, don't.
configureStore below
import { Dispatch } from 'react';
import { Action, configureStore } from '#reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import logger from 'redux-logger';
import {
testReducer,
} from './reducers';
const rootReducer = () => ({
testReducer
});
export const store = configureStore({
reducer: rootReducer,
devTools: process.env.NODE_ENV !== 'production',
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(logger),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = (): Dispatch<Action<any>> => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
selector
export const getTest = (state: RootState) => state.testReducer.test;
error
Property 'test' does not exist on type 'ReducerWithInitialState<TestState>'.ts(2339)
The error seems to go away if I add getInitialState() after testReducer. in the selector but that seems incorrect.
That rootReducer you have there is a function that returns an object with reducers - that's wrong. Instead just make it an object.
const rootReducer = {
testReducer
};

Redux useSelector does not infer state properly --- Typescript

Would love to know how to infer my state on the useSelector hook
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
import { reducers } from './reducers';
export const store = createStore(reducers, {}, applyMiddleware(thunk));
export type RootState = ReturnType<typeof reducers>;
Want to use it in my component like so
const { LogoutUser } = bindActionCreators(actionCreators, dispatch);
const appState = useSelector((state: RootState) => state.auth);
{appState.isAuthenticated && (
<Button
onClick={handleUserLogout}
size={'sm'}
colorScheme={'red'}
>
Logout
</Button>
)}
Typescript is annoyed
Error Property 'isAuthenticated' does not exist on type 'never'.
My State = Istate
export interface IState {
user: IUser | null;
isAuthenticated: false;
}
My Reducer
import { IState} from '../../interfaces';
import { AuthAction } from '../actionsTypes';
import { initialState } from '../state';
import { AuthActionTypes } from '../types';
export const AuthReducer = (
state: IState| undefined = initialState,
action: AuthAction
) => {
switch (action.type) {
case AuthActionTypes.LOGIN:
return {
...state,
isAuthenticated: true,
user: action.payload,
};
case AuthActionTypes.LOGOUT:
return {
...state,
isAuthenticated: false,
user: action.payload,
};
default:
return state;
}
};
My root reducer
import { combineReducers } from 'redux';
import { AuthReducer } from './AuthReducer';
export const reducers = combineReducers({
auth: AuthReducer,
});
export type State = ReturnType<typeof reducers>;
Have you try to create your own useSelector and useDispatch types ?
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
Have you try to generate RootState from store.getState ?
export type RootState = ReturnType<typeof store.getState>
Have you try to configure your store with configureStore ?
import { configureStore } from '#reduxjs/toolkit'
export const store = configureStore({
reducer: reducers
});
When your app is loaded, appState is null.
So in this case isAuthenticated property doesn't exist.
Therefore, you need to use ? before . to determine null object.
const { LogoutUser } = bindActionCreators(actionCreators, dispatch);
const appState = useSelector((state: RootState) => state.auth);
{appState?.isAuthenticated && (
<Button
onClick={handleUserLogout}
size={'sm'}
colorScheme={'red'}
>
Logout
</Button>
)}

Persist-reducer function giving type error to my reducer in typescript

I am trying to configure react-persist in my typescript react application. The persistReducer function is giving a type error to my reducer that Argument of type '(state: IState | undefined, action: Action) => IState' is not assignable to parameter of type 'Reducer<unknown, Action>'. Here is my store.ts code.
const persistConfig = {
key: "root",
storage,
stateReconciler: autoMergeLevel2,
whiteList: ["reducer"],
};
const persistedReducer = persistReducer(persistConfig, reducer);//the type error
This the code I am using for my reducers
export const reducer= (state:IState=initialState, action:Action):IState=> {
const {type, payload}=action;
switch(type){
case ActionType.CONNECT_META_MASK:
return {
...state,
address:payload.address,
connection:payload.connection
}
case ActionType.HOUR_PASSED:
return {
...state,
hourPassed:payload
}
default:
return state;
}
}
IState
export interface IState{
address:string,
connection:boolean
hourPassed:number
}
export const initialState:IState={
address: '',
connection: false,
hourPassed:0
}
Action
import {ActionType} from "../types/types"
interface IMetaMaskConnection{
type:typeof ActionType.CONNECT_META_MASK,
payload:{
connection:boolean,
address:string
}
}
interface IHourPassed{
type:typeof ActionType.HOUR_PASSED,
payload:number
}
export type Action = IMetaMaskConnection | IHourPassed
export const connectMetaMaskAction = (data:IMetaMaskConnection['payload']):Action => ({
type: ActionType.CONNECT_META_MASK,
payload:data
});
export const setHourPassed = (data:IHourPassed['payload']):Action => ({
type: ActionType.HOUR_PASSED,
payload:data
});
Instead of Action if I use AnyAction (exported from redux) then it works fine but I lose type declarations for my action payload.
I have looked online but I wasn't able to find any solution.
I had a similar issue while trying to add the persister to my root level reducer. And it took me some time to find a solution so help this directs people to the solution: https://github.com/rt2zz/redux-persist/issues/1140 for this issue. For me to keep type declarations I had to move the persister off of the root level and onto the specific reducer. Here is my implementation of the working persister with working type declarations:
import { configureStore, ThunkAction } from "#reduxjs/toolkit";
import { AnyAction, combineReducers } from "redux";
import thunk from "redux-thunk";
import storage from "redux-persist/lib/storage";
import autoMergeLevel2 from "redux-persist/lib/stateReconciler/autoMergeLevel2";
import persistReducer from "redux-persist/es/persistReducer";
import persistStore from "redux-persist/es/persistStore";
import uiReducer, { UiState } from "./reducers/uiReducer";
import apiReducer from "./reducers/apiReducer";
const persistConfig = {
key: "ui",
storage,
stateReconciler: autoMergeLevel2,
};
//Couldn't get state typings if persisting root reducer. Persisted by reducer works.
const reducers = combineReducers({
ui: persistReducer<UiState, any>(persistConfig, uiReducer),
api: apiReducer,
});
export const store = configureStore({
reducer: reducers,
middleware: [thunk],
});
export const persister = persistStore(store);
// Infer the `RootState` from the store itself to set up useAppDispatch and useAppSelector hook: https://react-redux.js.org/using-react-redux/usage-with-typescript
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
AnyAction
>;
UiState is the interface for the initial uiReducer state. This is what kept the type declarations for my setup. I left in some extra stuff to help see the full setup.
In your root reducer, export its type:
import { combineReducers } from "#reduxjs/toolkit";
export const rootReducer = combineReducers({ ... });
export type RootReducer = ReturnType<typeof rootReducer>;
Then import it into your store and set persistReducer as below:
import { rootReducer, RootReducer } from "./reducers";
const persistedReducer = persistReducer<RootReducer>(
persistConfig,
rootReducer
);
Just set the type. i.e:
reducer : persistReducer(persistConfig, rootReducer) as typeof rootReducer
This should give you the autocomplete hints as well.

Store does not have a valid reducer. Make sure the argument passed to combineReducers is an object

I'm having a little trouble with ReactJS and Redux-Toolkit
I want to have a global state for a Theme Switcher. So that i can wrap a Material-UI Theme around my whole project in App.tsx. But i always get the error: Store does not have a valid reducer. Make sure the argument passed to combineReducers is an object whose values are reducers.
My Store is configured like this:
import { combineReducers, configureStore, getDefaultMiddleware } from '#reduxjs/toolkit';
import { TypedUseSelectorHook, useSelector } from 'react-redux';
import { applyMiddleware } from 'redux';
import { themeUiReducer } from './ui/themeSlice';
export const rootReducer = combineReducers({
data: combineReducers({
}),
ui: combineReducers({
theme: themeUiReducer
}),
});
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) => [...getDefaultMiddleware()],
});
export type RootState = ReturnType<typeof store.getState>
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
And my themeUIReducer looks like this:
import { createEntityAdapter, createSlice, PayloadAction } from "#reduxjs/toolkit";
export interface IThemes {
themeMode: boolean | undefined,
useOs: boolean | undefined
}
const initialState: IThemes = {
themeMode: false,
useOs: false
}
const themeSlice = createSlice({
name: 'themeUI',
initialState: initialState,
reducers: {
setThemeMode: (state, action: PayloadAction<boolean | undefined>) => {
state.themeMode = action.payload;
},
setUseOs: (state, action: PayloadAction<boolean | undefined>) => {
state.useOs = action.payload;
},
},
extraReducers: {
}
})
export const { setThemeMode, setUseOs } = themeSlice.actions;
export const themeUiReducer = themeSlice.reducer;
The App.tsx file where i dispatch the reducer is configurered like this:
import React, { useCallback, useState } from 'react';
import './App.css';
import Layout from './components/Layout';
import { Route, Switch, Redirect, withRouter } from 'react-router-dom';
import { ThemeProvider, createTheme, useMediaQuery, CssBaseline } from '#mui/material';
import ThemeSwitcher from './theme/themeSwitcher';
import { useDispatch } from 'react-redux';
import { setThemeMode, setUseOs } from './store/ui/themeSlice';
function App() {
const dispatch = useDispatch();
// Get OS-level preference for dark mode
const prefersDarkMode: boolean | undefined = useMediaQuery("(prefers-color-scheme: dark)");
const [darkMode, setDarkMode] = useState<boolean | undefined>(prefersDarkMode);
const themeOption = (b: boolean) => (b ? "dark" : "light");
const theme = React.useMemo(
() =>
createTheme({
palette: {
mode: themeOption(darkMode!)
}
}),
[darkMode]
);
const toggleDarkMode = (checked: boolean | undefined) => {
if (checked === null)
setDarkMode(prefersDarkMode);
else setDarkMode(checked);
};
const dispatchTheme = useCallback(
(themeMode: boolean | undefined) => {
if(themeMode === null)
dispatch(setThemeMode(prefersDarkMode));
else
dispatch(setThemeMode(themeMode));
},
[dispatch]
);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<ThemeSwitcher useOs={false} themeChanger={dispatchTheme} />
<Layout>
<Switch>
<Route />
<Route />
<Route />
</Switch>
</Layout>
</ThemeProvider>
);
}
export default App;
So i'll use the ThemeSwitcher later in my Navigation/Footer and via global State i'll watch the selection of the user
Your error message tells you where to look:
Store does not have a valid reducer. Make sure the argument passed
to combineReducers is an object whose values are reducers.
The issue here is calling combineReducers with an empty object for your data state.
export const rootReducer = combineReducers({
data: combineReducers({
// Here is the problem. It needs a reducer.
}),
ui: combineReducers({
theme: themeUiReducer
}),
});
Since you aren't using the data property at all right now, the simplest thing is to remove it from your state.
export const rootReducer = combineReducers({
ui: combineReducers({
theme: themeUiReducer
}),
});
If you want to have a placeholder for a future reducer, you can provide a "no-op" reducer function which doesn't do anything.
export const rootReducer = combineReducers({
data: (state = null, action) => state,
ui: combineReducers({
theme: themeUiReducer
}),
});

Resources