I'm developing a webapp using TypeScript and redux-toolkit.
tl;dr
How to the set the type for dispatch parameter on a middleware?
I need to fetch some data from my server and save it on my store. So I wrote a code like bellow (simplified):
import { createSlice } from '#reduxjs/toolkit'
import { TNewOrder } from '../store'
const initialState: TNewOrder = {
prices: [],
// other stuffs...
}
const newOrderSlice = createSlice({
initialState,
name: 'new-order',
reducers: {
setPrices(state, action) {
const { payload } = action
return {
...state,
prices: payload.prices
}
},
updateFormData(state, action) {
// not important...
}
}
})
const { setPrices, updateFormData } = newOrderSlice.actions
const fetchPrices = () =>
async (dispatch: any) => {
const response = await fetch('https://jsonbox.io/box_4186ae80994ed3789969/prices/5de7e570de45ab001702a382')
const { prices } = await response.json()
dispatch(setPrices({ prices }))
}
export { fetchPrices, updateFormData }
export default newOrderSlice.reducer
Note that, because to get fetch a data is an async task, I can't put it directly on setPrices. So I created a middleware called fetchPrices to do the request task and call setPrices.
Despite it works, I'm unhappy with this solution because I set an any type. How is the correctly way to set a better type? I can't found a way to import ThunkDispatch from redux-toolkit.
Is there a better way to do it?
ThunkDispatch is not exported by #reduxjs/toolkit. You should import it from redux-thunk if you want to use it.
import { ThunkDispatch } from 'redux-thunk';
const fetchPrices = () =>
async (dispatch: ThunkDispatch) => {
const response = await fetch('https://jsonbox.io/box_4186ae80994ed3789969/prices/5de7e570de45ab001702a382')
const { prices } = await response.json()
dispatch(setPrices({ prices }))
}
The official suggestion is to use typeof store.dispatch, as Redux-Toolkit's store.dispatch already includes ThunkDispatch. (And you can extend store.dispatch if you use additional middlewares and have everything in a central location then.)
In addition, please also take a look at the TypeScript documentation of Redux-Toolkit. If you miss anything imporant, please open an issue and let us know :)
Related
So I have a movie app, and I have a page for a single movie. I have a section on that page where I display all of the videos from an API related to a certain movie.
So my Videos component looks like this:
const Videos = ({videos} :{videos:IVideos | null}) => {
return (
<div>{videos?.results.map((video, i) =>
<div key={i}>{video.name}</div>
)}</div>
)
}
It's just a basic component which gets props from a higher component. But the main thing is redux slice, which looks like this:
Initial state:
const initialState: IMovieVideosState = {
movieVideos: null,
fetchStatus: null,
}
export interface IMovieVideosState {
movieVideos: IVideos | null;
fetchStatus: FetchStatus | null;
}
And finally slice:
const videosSlice = createSlice({
name:'videos',
initialState,
reducers:{},
extraReducers(builder) {
builder
.addCase(fetchVideos.pending, (state, action) => {
state.fetchStatus = FetchStatus.PENDING
})
.addCase(fetchVideos.fulfilled, (state, action) => {
state.fetchStatus = FetchStatus.SUCCESS
state.movieVideos = action.payload
})
.addCase(fetchVideos.rejected, (state, action) => {
state.fetchStatus = FetchStatus.FAILURE
//state.error = action.error.message
})
}
})
As you see, these are basic reducers, where if promise is successful I assign payload to an existing array.
And also thunk function:
export const fetchVideos = createAsyncThunk('videos/fetchVideos', async (id: number) => {
const response = await axios.get<IVideos>(`${API_BASE}movie/${id}/videos?api_key=${TMDB_API_KEY}`);
console.log(response.data);
return response.data;
})
But in the browser I have the next error:
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
And also another one:
A non-serializable value was detected in an action, in the path: `<root>`. Value:
Promise { <state>: "pending" }
Take a look at the logic that dispatched this action:
Promise { <state>: "pending" }
I have no idea why I could have these errors, because my reducer is the same as another one in my project, but this one doesn't work for some reason.
UseEffect for dispatching all reducers:
useEffect(() =>{
dispatch(fetchDetail(Number(id)));
dispatch(fetchCredits(Number(id)));
dispatch(fetchPhotos(Number(id)));
dispatch(fetchRecommended(Number(id)));
dispatch(fetchSimilar(Number(id)));
dispatch(fetchVideos(Number(id))); //dispatching fetchVideos()
}, [dispatch, id])
So in my case, all of the other functions work fine besides fetchVideos().
Another example of a thunk for movie details:
export const fetchDetail = createAsyncThunk('detail/fetchDetail', async (id: number) => {
const response = await axios.get<IMovie>(`${API_BASE}movie/${id}?api_key=${TMDB_API_KEY}`);
console.log(response.data);
return response.data;
})
My store file:
import thunk from "redux-thunk";
export const store = configureStore({
reducer: {
popular,
top_rated,
playing,
upcoming,
detail,
credits,
videos,
photos,
recommended,
similar
},
middleware: [thunk]
})
export type RootState = ReturnType<typeof store.getState>;
instead of using create Async Thunk method add think malware where you create store of videos then you can pass Async actions into it without nothing.
import { applyMiddleware, combineReducers, createStore } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
// import your videos reducer here from file
export interface State {
videos: IVideos;
}
const rootReducer = combineReducers<State>({
videos: VideosReducer,
});
export const rootStore = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunk))
);
I'm struggling to understand how I can get around this issue?
I want to get data that I can share globally using redux**(using redux as I'm using it for other use cases in my app)**. my problem is I'm using getStaticProps to try and dispatch my ReduxThunk but I can't use Hooks inside getStaticProps and I have no idea what the workaround would be if anyone could point me to some docs I would appreciate it
Slice.js
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
export const fetchData = createAsyncThunk(
"fetchCoinData",
async (url, thunkApi) => {
const data = await fetch(url).then((res) => res.json());
return data;
}
);
const initialState = {
data: [],
status: null,
};
const getData = {};
export const dataSlice = createSlice({
name: "datafetch",
initialState,
extraReducers: {
[getData.pending]: (state) => {
state.status = "Loading!";
},
[getData.fulfilled]: (state, { payload }) => {
state.data = payload;
state.status = "Sucsess!";
},
[getData.rejected]: () => {
state.status = "Failed";
},
},
});
// Action creators are generated for each case reducer function
export const {} = dataSlice.actions;
export default dataSlice.reducer;
cardano.js
import React from "react";
import { useDispatch } from "react-redux";
import BasicCard from "../../Components/UI/Cards/BasicCard";
import { UsersIcon } from "#heroicons/react/outline";
import { fetchData } from "../../redux/slice/DataSlice";
const cardano = (props) => {
return (
<div>
<h1>Header</h1>
</div>
);
};
//PROBLEM IS HERE
export async function getStaticProps(context) {
const dispatch = useDispatch();
const priceQuery =
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin%2Ccardano%2Cethereum&vs_currencies=USD";
const res = await dispatch(fetchData(priceQuery));
return {
props: {
theData: res,
}, // will be passed to the page component as props
};
}
export default cardano;
To use Hooks, it has to be located at the highest level. Another alternative that you may try is the lifecycle of React. They are the same as Hooks.
For something like this, you probably want to use https://github.com/kirill-konshin/next-redux-wrapper .
Generally you have to be aware though: just putting something in the global state during SSR does not mean your client has it - your client will have it's own Redux state, and each page that is server-side-rendered also has their own isolated Redux state and you will think about what of that you will "hydrate" from the server to the client to make it available there.
The docs of next-redux-wrapper go more into this than I could possibly explain myself, so give those a read!
I am using Redux and call a async function using connect store.
Here is my view file code recordings.js where I write the below code:
fetchREcordingJson(file_name) {
const {
dispatch,
history
} = this.props;
dispatch(fetchRecordingJson(file_name))
console.log(dispatch(fetchRecordingJson(file_name)));
}
const mapStateToProps = ({
recordings
}) => {
return {
recordings
};
};
function mapDispatchToProps(dispatch) {
return {
dispatch,
...bindActionCreators({
getRecordingsList,
getRecordingsListById,
getRecordingsListByUserId,
getRecordingsSearchList,
getRecordingsSearchListListByUserId,
getRecordedListWithOrder,
getRecordedListWithOrderbyClient,
getRecordedListWithOrderbyUserId,
getRecordingsTags,
fetchRecordingJson,
}, dispatch)
}
}
export default injectIntl(
connect(mapStateToProps, mapDispatchToProps)(withRouter(RecordingsPage))
);
And below is my redux action.js code:
import axios from 'axios';
import FileDownload from 'react-file-download';
import {
RECEIVE_JSON,
}
from '../actions';
export function receiveJSON(json, file_name) {
return {
type: RECEIVE_JSON,
file_name,
data: json
}
}
export function fetchRecordingJson(file_name) {
return dispatch => {
return axios.get(API_URL + `fetchjson/${file_name}`)
.then(json => {
dispatch(receiveJSON(json.data, file_name))
})
}
}
And reducer.js code:
const INIT_STATE = {
info: {},
data: [],
count: 0,
annotations: [
[]
]
};
case RECEIVE_JSON:
let newState = {
data: action.data.data,
info: action.data.info,
count: state.count
};
newState.annotations = action.data.annotations.length === 0 ? [
[]
] : action.data.annotations || [
[]
];
newState.file_name = action.file_name;
return Object.assign({}, newState);
Either I use this.props.fetchRecordingJson(file_name)
or dispatch(fetchRecordingJson(file_name)) it returns the same error
Error: Actions must be plain objects. Use custom middleware for async actions
I am strugling from much time to resolve this but could not get success can anyone who worked on async calls using redux and dispatch can tell what would be the reason and how to resolve this
thanks
By itself, a Redux store doesn't know anything about async logic. You need to use middleware in order to make it work. It's easy. For instance, to add redux-thunk you need only:
// install: npm install redux-thunk
// configure your store
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '<path to your root reducer>';
const store = createStore(rootReducer, applyMiddleware(thunk));
And that's basically it.
These docs are worth to look at:
redux-thunk
Redux: Using Middleware to Enable Async Logic
Action on redux always must return an object. If you wanna implement asynchronous, you must use middleware to custom such as redux-thunk, redux-saga, observable...etc
My reducer mutate the state, which i dont want, please help
#Supplier Slice
import { createSlice,createAsyncThunk,createEntityAdapter } from '#reduxjs/toolkit'
import axios from 'axios'
export const getSupplierData = createAsyncThunk(
'supplier/getSupplierData', async () => {
const response = axios.get(
"http://127.0.0.1:8000/supplier/"
);
const data = await (await response).data
return data
})
const suppliersAdapter = createEntityAdapter({})
export const { selectAll: selectSuppliers, selectById: selectSupplierById} = suppliersAdapter.getSelectors(
state => state.suppliers
)
export const saveSupplier = createAsyncThunk('supplier/saveSupplier',
async supplier => {
const response = await axios.post('http://127.0.0.1:8000/supplier/', supplier)
const data = await response.data
return data
}
)
export const suppliersSlice = createSlice({
name: 'suppliers',
initialState:suppliersAdapter.getInitialState({}),
reducers: {
newSupplier:(state,action)=>action.payload
},
extraReducers: {
[getSupplierData.fulfilled]: suppliersAdapter.setAll,
[saveSupplier.fulfilled]:(state,action)=>state.push
}
})
export const { newSupplier } = suppliersSlice.actions
export default suppliersSlice.reducer
#supplier
const onSubmit=data=>dispatch(saveSupplier(data))
When i call this it replaced the list of supplier with the new data passed from the form
It mutates the state, which is not desireable
You need to remove [saveSupplier.fulfilled]:(state,action)=>state.push from your extraReducers. What you're doing now is trying to set the value to a function, which is why you'd also see a non-serializable error in your console with this code. If you just want to leave it as a placeholder and not update anything, just do [saveSupplier.fulfilled]: (state, action) => {}. Either way, I imagine what you actually want is [saveSupplier.fulfilled]: suppliersAdapter.addOne there?
Basically what I wanted to do was to stop making axios calls inside of my component. So I thought; “Why not just create an action for that?”
I googled around to find a good “guide” to use Redux and this is what I’m using:
Add a constant to the constants file. Something like const GREAT_COURSE = GREAT_COURSE
Add an action creator to the actions folder. Return an action JavaScript object with a type of the constant you created.
Add a reducer to the reducers folder that handles this action creator.
So I began to create my action creator:
import axios from 'axios'
import { CUSTOMER_FETCH } from './types'
import settings from '../settings'
axios.defaults.baseURL = settings.hostname
export const customers = () => {
return dispatch => {
return axios.get('http://hejhej/customers').then(res => {
dispatch({
type: CUSTOMER_FETCH,
data: res.data
})
})
}
}
And later to add a reducer that handles my action creator:
import { CUSTOMER_FETCH } from '../actions/types'
const initial = []
const customer = action => {
return {
data: action.data
}
}
const customers = (state = initial, action) => {
switch (action.type) {
case CUSTOMER_FETCH:
customers = [...state, customer(action)]
console.log('customers as state', customers)
return customers
default:
return state
}
}
export default customers
And inside of my component I'm importing it:
import { customers } from '../../actions/customersAction'
And later using connect: export default connect(null, { customers })(Events)
And finally I'm using it inside of my component:
customers() {
this.props.customers(this.state.data)
}
So I'm wondering what I'm doing wrong, because I can't see my console.log in my dev tools. Thanks a lot for reading!
Inside of my component atm:
axios.get('http://hejhej/customers').then(res => {
this.setState({
res,
customer: res.data
})
})