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));
Related
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>
)
}
So hi guys, this is my first project where I try to use react-typescript with redux-toolkit and am getting an error:
Argument of type 'AppThunk' is not assignable to parameter of type 'AnyAction'. Property 'type' is missing in type 'AppThunk' but required in type 'AnyAction'.
Basically I just want to get data from cats public API, but it doesn't seem to work and I would really appriciate some help.
src/app/rootReducer.tsx
import { combineReducers } from "#reduxjs/toolkit";
import catsReducer from "../features/cat/catsSlice";
const rootReducer = combineReducers({
cats: catsReducer,
});
export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;
src/app/store.tsx
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,
});
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch();
export type AppThunk = ThunkAction<void, RootState, unknown, Action>;
export default store;
src/features/cat/catsSlice.tsx
import axios from "axios";
import { createSlice, PayloadAction } from "#reduxjs/toolkit";
import { AppThunk } from "../../app/store";
import { RootState } from "../../app/rootReducer";
export interface CatsState {
cats: Array<Cat>;
isLoading: boolean;
error: CatsError;
}
export interface Cat {
id: string;
categories?: Array<any>;
breeds: Array<any>;
url: string;
width: number;
height: number;
}
export interface CatsError {
message: string;
}
export const initialState: CatsState = {
cats: [],
isLoading: false,
error: { message: "" },
};
export const catsSlice = createSlice({
name: "cat",
initialState,
reducers: {
setLoading: (state, { payload }: PayloadAction<boolean>) => {
state.isLoading = payload;
},
setCatsSuccess: (state, { payload }: PayloadAction<Array<Cat>>) => {
state.cats = payload;
},
setCatsFailed: (state, { payload }: PayloadAction<CatsError>) => {
state.error = payload;
},
},
});
export const { setCatsSuccess, setCatsFailed, setLoading } = catsSlice.actions;
export const getCats = (): AppThunk => async (dispatch) => {
try {
dispatch(setLoading(true));
const catsResponse = await axios.get(
"https://api.thecatapi.com/v1/images/search?limit=8&size=small&order=random"
);
dispatch(setCatsSuccess(catsResponse.data));
} catch (error) {
dispatch(setCatsFailed({ message: "An Error occurred" }));
} finally {
dispatch(setLoading(false));
}
};
export const catsSelector = (state: RootState) => state.cats;
export default catsSlice.reducer;
src/components/game/GameView.tsx
import React, { useEffect } from "react";
import { getCats, catsSelector, Cat } from "../../features/cat/catsSlice";
import { useSelector, useDispatch } from "react-redux";
const GameView: React.FC = (): JSX.Element => {
const dispatch = useDispatch();
const { cats, isLoading, error } = useSelector(catsSelector);
useEffect(() => {
dispatch(getCats());
}, [dispatch]);
return <>GameView</>;
};
export default GameView;
So as you can see I call the getCats() function in the useEffect hook, but it gets underlined with an error.
Any help would be appriciated! Thanks in advance!
in 'store.tsx' useDispatch() doesn't take any
export const useAppDispatch = () => useDispatch();
argument but you passed one in 'GameView.tsx':
dispatch(getCats());
So you can fix this by adding an optional argument to useDispatch in 'store.tsx', so instead it will be :
export const useAppDispatch: (arg?:unknown) => AppDispatch = useDispatch
I am tried to use saga-toolkit package to create saga action but getting some error while dispatching, although I went to official documentation of saga-toolkit npm package but something is missing over there i think and I am not able to figure out what going wrong. This is official documentation link https://npm.io/package/saga-toolkit
And I wanted to pass some argument while dispatching the action but getting error in console like
"Actions must be plain objects. Use custom middleware for async actions."
saga-toolkit provide createSagaAction which is resposibe for creating saga action but I am still get the above during dispatching
dataSlice.js
import { createSlice } from "#reduxjs/toolkit";
import { createSagaAction } from "saga-toolkit";
export const fetchData = createSagaAction("data/fetchData");
const dataSlice = createSlice({
name: "data",
initialState: {
data: [],
error: null,
loading: false,
},
extraReducers: {
[fetchData.pending]: (state) => {
state.loading = true;
},
[fetchData.fulfilled]: (state, action) => {
state.loading = false;
state.data = action.payload;
},
[fetchData.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload;
},
},
});
export default dataSlice.reducer;
dataSagas.js
import { put, call, fork } from "redux-saga/effects";
import { takeLatestAsync, putAsync } from "saga-toolkit";
import * as actions from "./feature/dataSlice";
import { getDataApi } from "./api";
function* onLoadData({payload}) {
const result = yield call(getDataApi, payload);
return result;
}
function* onLoadData() {
yield takeLatestAsync(actions.fetchData.type, onLoadDataAsync);
}
export const dataSagas = [fork(onLoadData)];
rootSaga.js
import { all } from "redux-saga/effects";
import { dataSagas } from "./dataSagas";
export default function* rootSaga() {
yield all([...dataSagas]);
}
store.js
import { configureStore } from "#reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";
import DataReducer from "./feature/dataSlice";
import rootSaga from "./rootSaga";
const sagaMiddleware = createSagaMiddleware();
const store = configureStore({
reducer: {
data: DataReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ thunk: false }).concat(sagaMiddleware),
});
sagaMiddleware.run(rootSaga);
export default store;
App.js
import { useEffect } from "react";
import logo from "./logo.svg";
import "./App.css";
import { useDispatch } from "react-redux";
import { fetchData } from "./redux/feature/dataSlice";
function App() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchData());
}, []);
return (
<div className="App">
<h2>Calling API using Saga-toolkit</h2>
</div>
);
}
export default App;
I Have a component (simple) and a reducer to modify value in my state.
I increment my value in my state, but when I want to display this value updated in my component, it's not updated with the new state value.
I don't know if my usedispatch is incorrect.
In my dev tools I see my state updated and incremented, but my page don't change...
Here is my reducer:
import { data } from "jquery";
export const INCREMENT_COUNTER = "INCREMENT_COUNTER";
export const DECREMENT_COUNTER = "DECREMENT_COUNTER";
export function increment(amount) {
console.log('-------------------amount', amount);
return {
type: INCREMENT_COUNTER,
payload: amount
}
}
export function decrement(amount) {
return {
type: DECREMENT_COUNTER,
payload: amount,
};
}
const initialState = {
data: 42,
}
export default function testReducer(state = initialState, {type, payload}) {
Object.assign(state.data, data);
switch (type) {
case INCREMENT_COUNTER:
console.log('increment reducer', {...state});
return {
...state,
...state.data,
data: state.data + payload,
};
case DECREMENT_COUNTER:
console.log('decrement reducer');
return {
//...state,
data: state.data - payload,
};
default:
return state;
}
}
and there is my component :
import React, {useCallback, useEffect} from "react"
import { useStore } from "react-redux";
import {useDispatch, useSelector} from "react-redux"
import {decrement, increment} from "./testReducer.js"
export default function Sandbox() {
const dispatch = useDispatch();
const data = useSelector((state) => state.test);
const store= useStore();
const toto = store.getState().test.data;
console.log('------------ttititi--------', store.getState().test.data);
const inc = useCallback(() => {
console.log('-----------inc', data);
console.log('-----------inc2', toto);
dispatch(increment(2))
}, [dispatch, data, toto]
);
const dec = useCallback(() => {
dispatch(decrement(2))
}, [dispatch, data]);
return (
<>
{toto.test?.data}
<h1>Testin2g</h1>
<h3>The data is: {data.data} -- {toto}</h3>
<button onClick={inc} content="increment" color="green">increment</button>
<button onClick={dec} content="decrement" color="red">decrement</button>
</>
)
}
This is how I do in my rootReducer:
import {combineReducers} from 'redux'
import testReducer from "../sandbox/testReducer.js";
import authReducer from "../modules/auth/authReducer";
import businessesReducer from "../modules/business/businessesReducer";
// import eventReducer from '../../features/events/eventReducer'
const rootReducer = combineReducers({
test: testReducer,
auth: authReducer,
businesses: businessesReducer,
// event: eventReducer
})
export default rootReducer;
configureStore.js file :
import {applyMiddleware, compose, createStore} from "redux"
import rootReducer from "./rootReducer"
import thunk from "redux-thunk";
export function configureStore() {
const composeEnchancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
return createStore(rootReducer, composeEnchancer(applyMiddleware(thunk)))
}
I was trying to run my Redux app with redux-saga.
Basically on my store.js I have the following codes:
import { applyMiddleware, createStore } from "redux";
import createSagaMiddleware from "redux-saga";
import logger from "redux-logger";
import rootReducer from "./reducers/rootReducer";
import rootSaga from "./sagas/userSagas";
const sagaMiddleware = createSagaMiddleware();
const middleware = [sagaMiddleware];
if (process.env_NODE_ENV === "development") {
middleware.push(logger);
}
const store = createStore(rootReducer, applyMiddleware(...middleware));
sagaMiddleware.run(rootSaga);
export default store;
My usersApi.js looks something like this:
import axios from "axios";
export const loadUsersApi = async () => {
await axios.get("http://localhost:5000/users");
};
And here is my userSagas:
import * as type from "../actionType";
import {
take,
takeEvery,
takeLatest,
put,
all,
delay,
fork,
call,
} from "redux-saga/effects";
import { loadUsersSuccess, loadUsersError } from "../actions/userAction";
import { loadUsersApi } from "../services/userApi";
export function* onLoadUsersStartAsync() {
try {
const response = yield call(loadUsersApi);
if (response.status === 200) {
yield delay(500);
yield put(loadUsersSuccess(response.data));
}
} catch (error) {
yield put(loadUsersError(error.response.data));
}
}
export function* onLoadUsers() {
yield takeEvery(type.LOAD_USERS_START, onLoadUsersStartAsync);
}
const userSagas = [fork(onLoadUsers)];
export default function* rootSaga() {
yield all([...userSagas]);
}
When I run this on my HomePage.js file where I load and dispatch the data:
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { loadUsersStart } from "../redux/actions/userAction";
export default function HomePage() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(loadUsersStart());
}, [dispatch]);
return (
<div>
<h1>Home</h1>
</div>
);
}
It gave me this error:
[HMR] Waiting for update signal from WDS...
index.js:1 TypeError: Cannot read properties of undefined (reading 'data')
at onLoadUsersStartAsync (userSagas.js:25)
at onLoadUsersStartAsync.next (<anonymous>)
at next (redux-saga-core.esm.js:1157)
at currCb (redux-saga-core.esm.js:1251)
index.js:1 The above error occurred in task onLoadUsersStartAsync
created by takeEvery(LOAD_USERS_START, onLoadUsersStartAsync)
created by onLoadUsers
created by rootSaga
Tasks cancelled due to error:
takeEvery(LOAD_USERS_START, onLoadUsersStartAsync)
I am not sure what's causing this error, but even the logger of my app doesn't even show the actions being dispatched.
Any idea how can I fix this issue?
You need to return promise from
export const loadUsersApi = () => {
return axios.get("http://localhost:5000/users");
};
Maybe next time try to use typescript. It will prevent You from similar mistakes
Alternatively, you can build your redux without using redux-saga.
This is how I usually set them up:-
A. Reducer
/slices/auth.js
import { createSlice } from '#reduxjs/toolkit'
const initialState = {
users: [],
loading: false,
success: false,
error: false,
message: ''
}
export const usersSlice = createSlice({
name: 'users',
initialState,
// // The `reducers` field lets us define reducers and generate associated actions
reducers: {
setUsers: (state, action) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.users = action.payload
},
setLoading: (state, action) => {
state.loading = action.payload
},
setSuccess: (state, action) => {
state.success = action.payload.status
state.message = action.payload.message
},
setError: (state, action) => {
state.error = action.payload.status
state.message = action.payload.message
}
}
})
export const { setUsers, setLoading, setSuccess, setError, setMessage } = usersSlice.actions;
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
export const selectUsers = (state) => state.users.users
export const selectLoading = (state) => state.users.loading
export const selectSuccess = (state) => state.users.success
export const selectError = (state) => state.users.error
export const selectMessage = (state) => state.users.message
export default usersSlice.reducer;
B. Store
store.js
import { configureStore, getDefaultMiddleware } from '#reduxjs/toolkit'
import usersReducer from '../slices/users'
export const store = configureStore({
reducer: {
users: usersReducer
},
middleware: getDefaultMiddleware({
serializableCheck: false
})
})
C. App Component
import { Provider } from 'react-redux'
import { store } from '../app/store'
export default function App() {
return {
<>
<Provider store={store}>
{/* your content... */}
</Provider>
</>
}
}
D. Component (where you use the redux)
import { useContext, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { selectUsers, selectLoading, selectSuccess, selectError, selectMessage, setUsers, setLoading, setSuccess, setError } from '../slices/users'
import axios from 'axios'
export default function HomePage() {
const dispatch = useDispatch()
const users = useSelector(selectUser)
const loading = useSelector(selectLoading)
const success = useSelector(selectSuccess)
const error = useSelector(selectorError)
const message = useSelector(selectorMessage)
useEffect(() => {
async function init() {
dispatch(setLoading(true))
const response = await axios.get('http://localhost:5000/users')
if(response?.status == 200) {
dispatch(setUsers(response?.data?.data))
dispatch(setSuccess({ status: true, message: 'Successfully get users data.' }))
} else {
dispatch(setError({ status: true, message: 'Failed to get data.' }))
}
dispatch(setLoading(false))
}
return init()
}, [])
return {
<>
{user}
</>
}
}