I have come across Redux Toolkit (RTK) and wanting to implement further functionality it provides. My application dispatches to reducers slices created via the createSlice({}) (see createSlice api docs)
This so far works brilliantly. I can easily use the built in dispatch(action) and useSelector(selector) to dispatch the actions and receive/react to the state changes well in my components.
I would like to use an async call from axios to fetch data from the API and update the store as the request is A) started B) completed.
I have seen redux-thunk and it seems as though it is designed entirely for this purpose, but the new RTK does not seem to support it within a createSlice() following general googling.
Is the above the current state of implementing thunk with slices?
I have seen in the docs that you can add extraReducers to the slice but unsure if this means I could create more traditional reducers that use thunk and have the slice implement them?
Overall, it is misleading as the RTK docs show you can use thunk, but doesn't seem to mention it not being accessible via the new slices api.
Example from Redux Tool Kit Middleware
const store = configureStore({
reducer: rootReducer,
middleware: [thunk, logger]
})
My code for a slice showing where an async call would fail and some other example reducers that do work.
import { getAxiosInstance } from '../../conf/index';
export const slice = createSlice({
name: 'bundles',
initialState: {
bundles: [],
selectedBundle: null,
page: {
page: 0,
totalElements: 0,
size: 20,
totalPages: 0
},
myAsyncResponse: null
},
reducers: {
//Update the state with the new bundles and the Spring Page object.
recievedBundlesFromAPI: (state, bundles) => {
console.log('Getting bundles...');
const springPage = bundles.payload.pageable;
state.bundles = bundles.payload.content;
state.page = {
page: springPage.pageNumber,
size: springPage.pageSize,
totalElements: bundles.payload.totalElements,
totalPages: bundles.payload.totalPages
};
},
//The Bundle selected by the user.
setSelectedBundle: (state, bundle) => {
console.log(`Selected ${bundle} `);
state.selectedBundle = bundle;
},
//I WANT TO USE / DO AN ASYNC FUNCTION HERE...THIS FAILS.
myAsyncInSlice: (state) => {
getAxiosInstance()
.get('/')
.then((ok) => {
state.myAsyncResponse = ok.data;
})
.catch((err) => {
state.myAsyncResponse = 'ERROR';
});
}
}
});
export const selectBundles = (state) => state.bundles.bundles;
export const selectedBundle = (state) => state.bundles.selectBundle;
export const selectPage = (state) => state.bundles.page;
export const { recievedBundlesFromAPI, setSelectedBundle, myAsyncInSlice } = slice.actions;
export default slice.reducer;
My store setup (store config).
import { configureStore } from '#reduxjs/toolkit';
import thunk from 'redux-thunk';
import bundlesReducer from '../slices/bundles-slice';
import servicesReducer from '../slices/services-slice';
import menuReducer from '../slices/menu-slice';
import mySliceReducer from '../slices/my-slice';
const store = configureStore({
reducer: {
bundles: bundlesReducer,
services: servicesReducer,
menu: menuReducer,
redirect: mySliceReducer
}
});
export default store;
I'm a Redux maintainer and creator of Redux Toolkit.
FWIW, nothing about making async calls with Redux changes with Redux Toolkit.
You'd still use an async middleware (typically redux-thunk), fetch data, and dispatch actions with the results.
As of Redux Toolkit 1.3, we do have a helper method called createAsyncThunk that generates the action creators and does request lifecycle action dispatching for you, but it's still the same standard process.
This sample code from the docs sums up the usage;
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit'
import { userAPI } from './userAPI'
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add user to the state array
state.entities.push(action.payload)
})
},
})
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))
See the Redux Toolkit "Usage Guide: Async Logic and Data Fetching" docs page for some additional info on this topic.
Hopefully that points you in the right direction!
You can use createAsyncThunk to create thunk action, which can be trigger using dispatch
teamSlice.ts
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
const axios = require("axios");
export const fetchPlayerList = createAsyncThunk(
"team/playerListLoading",
(teamId: string) =>
axios
.get(`https://api.opendota.com/api/teams/${teamId}/players`)
.then((response) => response.data)
.catch((error) => error)
);
const teamInitialState = {
playerList: {
status: "idle",
data: {},
error: {},
},
};
const teamSlice = createSlice({
name: "user",
initialState: teamInitialState,
reducers: {},
extraReducers: {
[fetchPlayerList.pending.type]: (state, action) => {
state.playerList = {
status: "loading",
data: {},
error: {},
};
},
[fetchPlayerList.fulfilled.type]: (state, action) => {
state.playerList = {
status: "idle",
data: action.payload,
error: {},
};
},
[fetchPlayerList.rejected.type]: (state, action) => {
state.playerList = {
status: "idle",
data: {},
error: action.payload,
};
},
},
});
export default teamSlice;
Team.tsx component
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchPlayerList } from "./teamSlice";
const Team = (props) => {
const dispatch = useDispatch();
const playerList = useSelector((state: any) => state.team.playerList);
return (
<div>
<button
onClick={() => {
dispatch(fetchPlayerList("1838315"));
}}
>
Fetch Team players
</button>
<p>API status {playerList.status}</p>
<div>
{playerList.status !== "loading" &&
playerList.data.length &&
playerList.data.map((player) => (
<div style={{ display: "flex" }}>
<p>Name: {player.name}</p>
<p>Games Played: {player.games_played}</p>
</div>
))}
</div>
</div>
);
};
export default Team;
Use redux-toolkit v1.3.0-alpha.8
Try this
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit';
export const myAsyncInSlice = createAsyncThunk('bundles/myAsyncInSlice', () =>
getAxiosInstance()
.get('/')
.then(ok => ok.data)
.catch(err => err),
);
const usersSlice = createSlice({
name: 'bundles',
initialState: {
bundles: [],
selectedBundle: null,
page: {
page: 0,
totalElements: 0,
size: 20,
totalPages: 0,
},
myAsyncResponse: null,
myAsyncResponseError: null,
},
reducers: {
// add your non-async reducers here
},
extraReducers: {
// you can mutate state directly, since it is using immer behind the scenes
[myAsyncInSlice.fulfilled]: (state, action) => {
state.myAsyncResponse = action.payload;
},
[myAsyncInSlice.rejected]: (state, action) => {
state.myAsyncResponseError = action.payload;
},
},
});
Related
I'm trying to convert an existing project to configureStore from createStore.
store.js :
export default configureStore({
reducer: {
articlesList: loadArticlesReducer
}
});
home.js :
const articlesList = useSelector((state) => state.articlesList);
const dispatch = useDispatch()
useEffect(() => {
dispatch(getArticles());
})
articleSlice.js :
const articleSlice = createSlice({
name: 'articles',
initialState : [],
reducers : {
loadArticlesReducer: (state, action) => {
console.log("WE NEVER REACH THIS CODE") <=== the problem is here
state = action.payload;
}
}
});
export const { loadArticlesReducer } = articleSlice.actions;
export const getArticles = () => dispatch => {
fetch("https://....")
.then(response => response.json())
.then(data => {
dispatch(loadArticlesReducer(data))
})
};
The problem, as stated in the comment, is that getArticles action never dispatches the data to loadArticlesReducer.
What am I missing here?
loadArticlesReducer is an action creator, not a reducer function. I suggest renaming the action creator so its purpose isn't confusing to future readers (including yourself) and actually exporting the reducer function.
Example:
const articleSlice = createSlice({
name: 'articles',
initialState : [],
reducers : {
loadArticlesSuccess: (state, action) => {
state = action.payload;
}
}
});
export const { loadArticlesSuccess } = articleSlice.actions;
export const getArticles = () => dispatch => {
fetch("https://....")
.then(response => response.json())
.then(data => {
dispatch(loadArticlesSuccess(data));
});
};
export default articleSlice.reducer; // <-- export reducer function
import articlesReducer from '../path/to/articles.slice';
export default configureStore({
reducer: {
articlesList: articlesReducer
}
});
You may also want to consider converting getArticles to a more idiomatic RTK thunk function using createAsyncThunk. You'd use the slice's extraReducers to handle the fulfilled Promise returned from the Thunk. Example:
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit';
export const getArticles = createAsyncThunk(
"articles/getArticles",
() => {
return fetch("https://....")
.then(response => response.json());
}
);
const articleSlice = createSlice({
name: 'articles',
initialState : [],
extraReducers: builder => {
builder.addCase(getArticles.fulfilled, (state, action) => {
state = action.payload;
});
},
});
export default articleSlice.reducer;
As per the documentation, you'll need to replace the following line:
export default configureStore({
reducer: {
articlesList: loadArticlesReducer // <= This currently points to the exported actions and not the reducer
}
});
With this one:
import articleReducer from './articleSlice.js'
export default configureStore({
reducer: {
articlesList: articleReducer
}
});
You'll need to export the reducer from the articleSlice.js of course:
export default articleSlice.reducer;
As a general tip, always reproduce the examples from the documentation using exactly the same setup and naming, and once it works, customize the code accordingly in a slow step by step manner. It's easy to miss something while replicating such complicated setups if there's no 1-to-1 correspendence with the original code.
This is my first react project and first JS project overall so I am incredibly new to the language though I have some prior experience with c#.
I got my app to a working version locally but my states and prop passing was getting out of hand so I wanted to implement redux before it gets worse.
I have a getClients Axios function that seems to work with a local state but something about the way its set up isn't working for redux.
I set up console logs and debuggers and it looks like the reducer is seeing the action.payload just fine and should be updating the state. but my clientList variable with useSelector is always showing an empty array.
export default function ClientDisplay() {
// const [clients, setClients] = useState([]);
const clientList = useSelector((state) => state.clientList);
const dispatch = useDispatch();
useEffect(() => {
getClients();
console.log("initial load");
}, []);
const getClients = () => {
Axios.get("http://localhost:3001/table/clients").then((response) => {
console.log("getClients trigered")
dispatch(setClientList(response.data));
});
};
import { createSlice } from "#reduxjs/toolkit"
export const clientListSlice = createSlice({
name: 'ClientList',
initialState: [],
reducers: {
setClientList(state, action) {
state = action.payload;
},
},
});
export const {setClientList} = clientListSlice.actions
export default clientListSlice.reducer;
const store = configureStore({
reducer : {
activeClient: activeClientReducer,
clientList: clientListReducer,
},
})
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
Try defining the initialState properly.
export const clientListSlice = createSlice({
name: 'clientList',
initialState: {clients:[]},
reducers: {
setClientList(state, action) {
state.clients = action.payload;
},
},
})
and then in useSelector
const {clients} = useSelector((state) => state.clientList);
I am starting a new next js project,we are migrating from a normal react app to a next app.
we intend to use redux toolkit for our global state management and we are using server side rendering.so we came across next-redux-wrapper npm package which looked like it solves most of our problems with redux and ssr but form some reason when ever we use server side rendering on one of the pages the HYDRATE action from next-redux-wrapper is getting called atleast twice sometimes even 4 times.What exactly is going wrong because the article i referred to seems to work fine,i have attached my Redux store details and the Redux slice details,and my getServerSideProps function details.
import { createStore, applyMiddleware, combineReducers } from "redux";
import count from "../ReduxSlices/CounterSlice";
import { createWrapper, HYDRATE } from "next-redux-wrapper";
import { configureStore } from "#reduxjs/toolkit";
const combinedReducer = combineReducers({
count,
});
const reducer = (state, action) => {
if (action.type === HYDRATE) {
const nextState = {
...state, // use previous state
...action.payload, // apply delta from hydration
count: {
count: state.count.count + action.payload.count.count,
},
};
return nextState;
} else {
return combinedReducer(state, action);
}
};
export const makeStore = () =>
configureStore({
reducer: reducer,
});
export const wrapper = createWrapper(makeStore, { debug: true });
and my Redux slice is
import { createSlice } from "#reduxjs/toolkit";
import { HYDRATE } from "next-redux-wrapper";
const initialState = {
count: 0,
};
const counterSlice = createSlice({
name: "counter",
initialState: initialState,
reducers: {
increment: (state) => {
state.count = state.count + 1;
},
},
});
export const { increment } = counterSlice.actions;
export default counterSlice.reducer;
and finally this is how i dispatch an action inside getServerSideProps
export const getServerSideProps = wrapper.getServerSideProps(
(store) => async () => {
store.dispatch(increment());
console.log("server", new Date());
}
);
The console log is logging only once but the HYDRATE action is getting dispatched atleast two times...any insight will be helpful,thank you.
I had the same issue. Disabling react strict mode worked for me.
const nextConfig = {
// reactStrictMode: true,
...
}
module.exports = nextConfig
im new to the redux toolkit here is my problem
im fetching data every time by creating own custom useFetch file for handling loading , success, error status
i used the same way with redux toolkit by creating createSlice method for accesing reducers and actions
this is working successfully and getting data from this way by dispaching actions and reducers
but i did't used the createAsyncThunk from redux toolkit
my confusion is is this currect way to fetch data from custom useFetch or should i use createAsyncthunk
Im not sure how to use createAsyncThunk in custom useFetch
if anyone knows the answer that is so appreciatable
posted my all files below
if i get answer with createAsyncThunk in custom useFetch that is soo appreciable
thanks advance
App.js
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import useFetchData from "./Components/useFetchData";
import { actions } from "./Slices/CounterSlice";
const App = () => {
useFetchData("https://jsonplaceholder.typicode.com/todos");
const apiData = useSelector((s) => s);
console.log(apiData.api);
return (
<>
{apiData.api.status !== "success" && <h1>hello</h1>}
{apiData.api.status === "success" &&
apiData.api.apiData.map((el) => {
return <h6 key={el.id}>{el.title}</h6>;
})}
</>
);
};
export default App;
custom useFetchData.js file
import React, { useCallback } from "react";
import { useDispatch } from "react-redux";
import { ApiActions } from "../Slices/ApiSlice";
const useFetchData = (link) => {
const dispatch = useDispatch();
const fetchData = useCallback(async () => {
try {
dispatch(ApiActions.loadingTime());
const getData = await fetch(link);
const toJson = await getData.json();
dispatch(ApiActions.successTime(toJson));
} catch {
dispatch(ApiActions.errorTime());
}
}, [link]);
React.useEffect(() => {
fetchData();
}, [fetchData]);
};
export default useFetchData;
this is createSlice file for creating actions and reducers
import { createSlice } from "#reduxjs/toolkit";
const apiSlice = createSlice({
name: "api",
initialState: {
status: "idle",
apiData: [],
error: false,
},
reducers: {
loadingTime: (state, action) => {
state.status = "Loading";
},
successTime: (state, action) => {
state.apiData = action.payload;
state.status = "success";
},
errorTime: (state, action) => {
state.apiData = [];
state.status = "error";
},
},
});
export const ApiActions = apiSlice.actions;
export const apiReducers = apiSlice.reducer;
You can use one way or the other. Example with createAsyncThunk:
export const checkIfAuthenticated = createAsyncThunk(
"auth/checkIsAuth",
async (_, thunkAPI) => {
const response = await axios.post(
"http://127.0.0.1:8080/dj-rest-auth/token/verify/",
{ token: localStorage.getItem("access") }
);
thunkAPI.dispatch(getUserInfo());
return response.data;
}
);
Then you handle loading, success, error status as before:
extraReducers: (builder) => {
builder
.addCase(checkIfAuthenticated.fulfilled, (state, action) => {
state.isAuthenticated = true;
})
.addCase(checkIfAuthenticated.rejected, (state, action) => {
state.isAuthenticated = false;
snackbar({ error: "Nie jesteÅ› zalogowany" });
})
},
However, if you have a lot of API queries, the optimal solution is to use redux toolkit query or react-query. These two libraries make queries a lot better.
If you care about code cleanliness and ease, check them out.
I'm using Redux-Toolkit for the first time, followed the Quick-start in their documentation and am met with this error
Store does not have a valid reducer. Make sure the argument passed to combineReducers is an object whose values are reducers.
I logged the reducers and the store objects to console and found that the store object from configure store doesn't have a reducer property at all.
If any of you can help out, I'd really appreciate it.
P.S : I already looked at all the other stackoverflow question on this. Nothing is relevant.
Here's my code
store.js
import { configureStore } from "#reduxjs/toolkit";
// Redux Reducers
import { authSlice } from "./slices/authSlice";
import { projectSlice } from "./slices/projectSlice";
export const store = configureStore({
reducer: {
auth: authSlice.reducer,
project: projectSlice.reducer,
},
});
authSlice.js
import { createSlice } from "#reduxjs/toolkit";
export const authSlice = createSlice({
name: "auth",
initialState: {
isUserLoggedIn: false,
userDetails: {},
},
reducers: {
login: (state) => {
state.auth.isUserLoggedIn = true;
},
logout: (state) => {
state.auth.isUserLoggedIn = false;
},
register: (state) => {
state.auth.isUserLoggedIn = true;
},
},
});
export const { login, logout, register } = authSlice.actions;