How to unwrapResult for all dispatched actions with redux-toolkit - reactjs

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>
)
}

Related

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>
)}

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

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

reducer/action not updating slice

I am clearly missing something about how this should work. I have a slice, it has reducers I can bring those in and I can see console.log firing as expected... buuut Redux Dev Tools says I have not changed my state - the default null still is still the listed value.
Slice
import { User } from "#firebase/auth";
import { createSlice, PayloadAction } from "#reduxjs/toolkit";
//Slice definition
// define state
interface UserState {
currentUser: User | null;
}
const initialState: UserState = {
currentUser: null,
};
//define action creators
const userSlice = createSlice({
name: "user",
initialState,
reducers: {
setUser(state, action: PayloadAction<User>) {
state.currentUser = action.payload;
},
setNoUser(state) {
state.currentUser = null;
},
},
});
//export all action creators
export const { setUser, setNoUser } = userSlice.actions;
//export reducer that handles all actions
export default userSlice.reducer;
Store
import { configureStore } from "#reduxjs/toolkit";
import userReducer from "../features/user/user-slice";
export const store = configureStore({
reducer: { user: userReducer },
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
App snippet
import React, { useEffect, useState } from "react";
...
import { setUser, setNoUser } from "./features/user/user-slice";
function App() {
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
auth.onAuthStateChanged((user) => {
if (user) {
console.log("useEffect");
console.log(user);
setUser(user);
} else {
setNoUser();
console.log("useEffect, no user");
}
setLoading(false);
});
});
if (loading) return <Spinner animation="border" color="dark" />;
return <App/>;
}
export default App;
the reason for this is because setUser is an action and the action needs to be dispatched.
Per Mark Erikson's Video from may it is advisable to create a typed version of the useSelector and useDispatch hooks when working with typescript.
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { RootState, AppDispatch } from "./store";
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const userAppSelector: TypedUseSelectorHook<RootState> = useSelector;
you then import the hooks from the hooks file not react-redux and dispatch or select from there:
const dispatch = useAppDispatch();
dispatch(setUser(auth.currentUser));

Property 'then' does not exist on type 'ThunkAction<Promise<string>

I am getting the error
Property 'then' does not exist on type 'ThunkAction<Promise<string>
I am trying to return a promise from a AppThunk in my slice.
below is the code I have setup and I have went through several same questions and followed few suggestions given there, but still could not figure out what is wrong.
store.ts
import { configureStore, Action } from '#reduxjs/toolkit'
import { useDispatch } from 'react-redux'
import { ThunkAction } from 'redux-thunk'
import rootReducer, { RootState } from './rootReducer'
const store = configureStore({
reducer: rootReducer
})
if (process.env.NODE_ENV === 'development' && module.hot) {
module.hot.accept('./rootReducer', () => {
const newRootReducer = require('./rootReducer').default
store.replaceReducer(newRootReducer)
})
}
export type AppDispatch = typeof store.dispatch
export const useAppDispatch = () => useDispatch<AppDispatch>()
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>
export default store
mySlice.ts
import { AppThunk } from 'app/store'
// ...
// my reducer logic is here
// ...
export const fetchIssuePromise = (): AppThunk<Promise<string>> => async (dispatch) => {
return Promise.resolve("string")
}
and I am using this in my component.
MyComponent.tsx
import React, { useEffect } from 'react'
import { fetchIssue, fetchIssuePromise } from 'features/mySlice.ts'
import { useDispatch } from 'react-redux'
export const MyComponent = ({
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchIssuePromise()).then(() => {
// My logic here.
})
}, [dispatch])
})
I know I can do this by storing in redux state and using selector and work on that.
But I do not want to store some data especially validation errors from server in redux.
I am not sure if this approach is correct, please let me know how can I handle this or any better ways to achieve this.
Don’t use the default "useDispatch" hook.
As mentionned in this github issue: https://github.com/reduxjs/redux-toolkit/issues/678
The linked doc: https://redux-toolkit.js.org/usage/usage-with-typescript#getting-the-dispatch-type
In your store.ts:
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export const useAppDispatch = () => useDispatch<AppDispatch>()
In your Component.tsx:
const dispatch = useAppDispatch() // hook exported from store

Resources