I'm trying to make redux dispatch action async using redux toolkit. I'm doing this to handle UI freeze caused due to redux state update (using setFilterInfo reducer).
While searching for a way to do it, I came across createAsyncThunk. However, all the examples showed its usage in respect of fetching data. I ended up with the following code. The problem is still not solved, UI is still freezing while updating the redux state.
// This is the Thunk
export const filterInfoSetterThunk = createAsyncThunk(
"screenSlice/setFilterInfo",
async (filter, thunkAPI) =\> {
return filter;
}
);
// Slice
const screenSlice = createSlice({
name: "screenSlice",
initialState: {
filter_info: []
},
reducers: {
setFilterInfo(state, action) {
state.filter_info = action.payload.filter_info;
},
},
extraReducers: (builder) => {
builder.addCase(filterInfoSetterThunk.fulfilled, (state, action) => {
console.log("Inside extra reducer", action.payload.filter_info);
state.filter_info = action.payload.filter_info;
});
},
});
// calling filterInfoSetterThunk inside a function
updateFilterInfoInReduxStore = async (data) => {
await filterInfoSetterThunk({filter_info: data})
You are calling await filterInfoSetterThunk({filter_info: data}) incorrectly here.
Instead of this dispatch(filterInfoSetterThunk({filter_info: data}))
Related
I'm having this weird issue where my RTK Query requests are happening in a strange order.
We've got the RTK sports slice, and in the same file I've defined the useLoadSports hook
const sportsSlice = createSlice({
name: 'sports', initialState: {},
reducers: {
setSports: (state, action) => action.payload,
},
});
export const sports = sportsSlice.reducer;
export const { setSports } = sportsSlice.actions;
export const useLoadSports = () => {
const dispatch = useDispatch();
const { data: sports, ...result } = useGetSportsQuery();
useEffect(() => { console.log('useLoadSports'); }, []);
useEffect(() => {
if (sports) {
console.log('SETTING SPORTS');
dispatch(setSports(sports));
}
}, [sports]);
return result;
};
The Application component uses this hook as it loads some data needed throughout the app.
const useInitialLoad = () => {
useEffect(() => {
console.log('useInitialLoad');
}, []);
const { isLoading: sportsLoading } = useLoadSports(); // below
const ready = !sportsLoading;
return { ready };
};
const Application: React.FC<Props> = () => {
const { ready } = useInitialLoad();
if (!ready) return <h1>Loading app data</h1>;
return (
<S.Wrapper>
<AppRouter />
</S.Wrapper>
);
};
The AppRouter actually iterates over a config object to create Routes. I'm assuming that's not our issue here.
Anyway, then the PlayerPropsPage component calls useGetPropsDashboardQuery.
const PlayerPropsPage = () => {
const { data, isLoading, isError, error } = useGetPropsDashboardQuery();
useEffect(() => {
console.log('LOADING PlayerPropsPage');
}, [])
return /* markup */
}
The query's queryFn uses the sports that were saved into the store by useLoadSports
export const { useGetPropsDashboardQuery, ...extendedApi } = adminApi.injectEndpoints({
endpoints: build => ({
getPropsDashboard: build.query<PropAdminUIDashBoard, void>({
queryFn: async (_args, { getState }, _extraOptions, baseQuery) => {
console.log('PROPS ENDPOINT');
const result = await baseQuery({ url });
const dashboard = result.data as PropAdminDashBoard;
const { sports } = getState() as RootState;
if (!Object.entries(sports).length) {
throw new Error('No sports found');
}
// use the sports, etc.
},
}),
}),
});
I'd think it would use the setSports action before even rendering the router (and hence calling the props endpoint or loading the page, and I'd really think it would render the PlayerPropsPage before calling the props query, but here's the log:
useInitialLoad
useLoadSports
LOADING PlayerPropsPage
PROPS ENDPOINT
SETTING SPORTS
Another crazy thing is if I move the getState() call in the endpoint above the call to baseQuery, the sports haven't been stored yet, and the error is thrown.
Why is this happening this way?
A bunch of random observations:
you should really not dispatch that setSports action in a useEffect here. If you really want to have a slice with the result of your useGetSportsQuery, then add an extraReducers for api.endpoints.getSports.fulfilled. See this example:
// from the example
extraReducers: (builder) => {
builder.addMatcher(
api.endpoints.login.matchFulfilled,
(state, { payload }) => {
state.token = payload.token
state.user = payload.user
}
)
},
I don't see why you even copy that data into the state just to use a complicated queryFn instead of just passing it down as props, using a query and passing it in as useGetPropsDashboardQuery(sports). That way that getPropsDashboard will update if the sports argument changes - which will never happen if you take the extra logic with the getState() and all the other magic.
you could even simplify this further:
const { data: sports } = useGetSportsQuery()
const result = useGetPropsDashboardQuery( sports ? sports : skipToken )
No need for a slice, no need to have that logic spread over multiple components, no need for a queryFn. queryFn is an escape hatch and it really doesn't seem like you need it.
The current behaviour is normal even if it's not what you expect.
you have a main hook who do a query
the first query start
when the query is finish it does "rerender" and does this
dispatch the result of the sports
the second query start
So there is no guarantee that you have sports in the store when you start the second query. As all is done with hooks (you can technically do it with hooks but that's another topic)
How to wait a thunk result to trigger another one ?
You have multiple ways to do it. It depends also if you need to wait thoses two queries or not.
Listener middleware
If you want to run some logic when a thunk is finish, having a listener can help you.
listenerMiddleware.startListening({
matcher: sportsApi.endpoints.getSports.fulfilled,
effect: async (action, listenerApi) => {
listenerApi.dispatch(dashboardApi.endpoints.getPropsDashboard.initiate())
}
},
})
In addition, instead of setting sports in the store with a dispatch inside the useEffect. You can plug your query into the extraReducers. here is an example:
createSlice({
name: 'sports',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addMatcher(
sportsApi.endpoints.getSports.fulfilled,
(state, action) => {
state.sports = action.payload.sports
}
)
},
})
Injecting thunk arguments with a selector
If you use directly pass sports as a variable to the query when they change they'll re-trigger the query. here is an example:
const PlayerPropsPage = () => {
const { data: sports, ...result } = useGetSportsQuery();
const { data, isLoading, isError, error } = useGetPropsDashboardQuery(sports);
}
Doing the two query inside a single queryFn
If inside a single queryFn you can chain theses query by awaiting them
queryFn: async (_args, { getState }, _extraOptions, baseQuery) => {
const resultFirstQuery = await baseQuery({ url: firstQueryUrl });
const resultSecondQuery = await baseQuery({ url: secondQueryUrl });
// Do stuff
},
Note:
When you use getState() inside a thunk, if the store update this will not trigger your thunk "automatically"
I do not know if you need the sport to do the second query or to group the result of the two queries together.
I have been using Redux on some of my projects by following some tutorials, in order to make API calls(write action creators that return a function instead of an action) we use redux-thunk.
I don't understand why inside this action creators functions I can run dispatch() without having to use an instance of the store store.dispatch()?
Example on the redux documentation:
import { createStore } from 'redux'
const store = createStore(todos, ['Use Redux'])
function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}
store.dispatch(addTodo('Read the docs'))
store.dispatch(addTodo('Read about the middleware'))
Tutorial Code:
const loadRockets = () => async (dispatch) => {
const res = await fetch(URL);
const data = await res.json();
const state = data.map((rocket) => ({
id: rocket.id,
name: rocket.rocket_name,
image: rocket.flickr_images[0],
type: rocket.rocket_type,
description: rocket.description,
reserved: false,
}));
dispatch({ type: LOAD, state });
};
You can do that because the redux-thunk middleware is designed to work that way. If you dispatch a function, redux-thunk will call that function and pass the dispatch function in to you. If you're curious, here is the code where they implement that (in particular, the return action(dispatch, getState, extraArgument) part):
function createThunkMiddleware<
State = any,
BasicAction extends Action = AnyAction,
ExtraThunkArg = undefined
>(extraArgument?: ExtraThunkArg) {
// Standard Redux middleware definition pattern:
// See: https://redux.js.org/tutorials/fundamentals/part-4-store#writing-custom-middleware
const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> =
({ dispatch, getState }) =>
next =>
action => {
// The thunk middleware looks for any functions that were passed to `store.dispatch`.
// If this "action" is really a function, call it and return the result.
if (typeof action === 'function') {
// Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"
return action(dispatch, getState, extraArgument)
}
// Otherwise, pass the action down the middleware chain as usual
return next(action)
}
return middleware
}
https://github.com/reduxjs/redux-thunk/blob/master/src/index.ts#L30
I am having difficulty updating my store after calling an API. I am using reduxjs/toolkit. Here is the structure of the project:
react/
store/
api/
dataconsumer/
dataSlice.js
notifications/
notificationsSlice.js
app.js
Here, api contains non-component API calls to the server. They are bound to thunk functions within dataSlice and a successful query updates data just fine.
The following are relevant parts to my reducers.
notificationSlice.js
const slice = createSlice({
...,
reducers: {
// need to call this from api
setNotifications: (state, action) => state.notification = action.payload
}
})
dataSlice.js
export const fetchInitialData = createAsyncThunk(
'chart/fetchInitialData',
async (data) => {
return API.candles.initialData({
...data
})
}
const slice = createSlice({
...
extraReducers: {
...
[fetchInitialData.success]: (state, action) => state.action = action.payload
}
})
And the api
const fetchInitialData = () => {
return fetch(url, {
...
}).then(data => data.json())
.then(data => {
if(data.status === 200) { return data } // works great!
else {
// doesn't work, but functionally what I'm looking for
store.dispatch(setNotifications(data.status))
}
})
}
The problem is when the response is other than 200, I need to update notifications, but I don't know how to get the data to that reducer.
I can't useDispatch because it is outside a component, and if I import the store to my api files it is outside the context provider and my state is uninitialized.
I'm sure I could use localStorage to solve the problem or some other hack, but I feel I shouldn't have to and I'm wondering if there is a key principle I'm missing when organizing my react-redux project? or if there are standard solutions to this problem.
Thanks - I'm new to redux.
Well, if you are using thunk, then the best solution will be to use it in order to dispatch your action after you get the error.
You do it like this:
export const fetchInitialData = () => {
return dispatch => {
...your logic
else {
// now you can dispatch like this
dispatch(setNotifications(data.status))
}
}
};
I am using redux-toolkit with createAsyncThunk to handle async requests.
I have two kinds of async operations:
get the data from the API server
update the data on the API server
export const updateData = createAsyncThunk('data/update', async (params) => {
return await sdkClient.update({ params })
})
export const getData = createAsyncThunk('data/request', async () => {
const { data } = await sdkClient.request()
return data
})
And I add them in extraReducers in one slice
const slice = createSlice({
name: 'data',
initialState,
reducers: {},
extraReducers: (builder: any) => {
builder.addCase(getData.pending, (state) => {
//...
})
builder.addCase(getData.rejected, (state) => {
//...
})
builder.addCase(
getData.fulfilled,
(state, { payload }: PayloadAction<{ data: any }>) => {
state.data = payload.data
}
)
builder.addCase(updateData.pending, (state) => {
//...
})
builder.addCase(updateData.rejected, (state) => {
//...
})
builder.addCase(updateData.fulfilled, (state) => {
//<--- here I want to dispatch `getData` action to pull the updated data
})
},
})
In my component, I have a button that triggers dispatching of the update action. However I found after clicking on the button, despite the fact that the data is getting updated on the server, the data on the page is not getting updated simultaneously.
function MyComponent() {
const dispatch = useDispatch()
const data = useSelector((state) => state.data)
useEffect(() => {
dispatch(getData())
}, [dispatch])
const handleUpdate = () => {
dispatch(updateData())
}
return (
<div>
<ul>
// data goes in here
</ul>
<button onClick={handleUpdate}>update</button>
</div>
)
}
I tried to add dispatch(getData()) in handleUpdate after updating the data. However it doesn't work because of the async thunk. I wonder if I can dispatch the getData action in the lifecycle action of updateData i.e.
builder.addCase(updateData.fulfilled, (state) => {
dispatch(getData())//<--- here I want to dispatch `getData` action to pull the updated data
})
Possibly it's not actual and the question is outdated, but there is thunkAPI as second parameter in payload creator of createAsyncThunk, so it can be used like so
export const updateData = createAsyncThunk('data/update', async (params, {dispatch}) => {
const result = await sdkClient.update({ params })
dispatch(getData())
return result
})
First of all: please note that reducers always need to be pure functions without side effects. So you can never dispatch anything there, as that would be a side effect. Even if you would somehow manage to do that, redux would warn you about it.
Now on to the problem at hand.
You could create a thunk that dispatches & awaits completion of your updateData call and then dispatches your getData call:
export const updateAndThenGet = (params) => async (dispatch) => {
await dispatch(updateData(params))
return await dispatch(getData())
}
//use it like this
dispatch(updateAndThenGet(params))
Or if both steps always get dispatched together anyways, you could just consider combining them:
export const updateDataAndGet = createAsyncThunk('data/update', async (params) => {
await sdkClient.update({ params })
const { data } = await sdkClient.request()
return data
})
I'm going to save in localstorage some data, but only after call UPDATE_POST action. Now i'm apply localstorage in index.js via:
store.subscribe(throttle(() => {
post: saveState(store.getState().post);
}, 1000))
and it save data in localstorage for every second. But my goal is to save it only after updatePost action. Can I achieve it using middleware, and how to write it?
My reducer:
const Post = (state = {}, action) => {
switch (action.type) {
case 'INIT_POST':
..... some code
case 'UPDATE_POST':
... some code
default:
return state
}
};
My action:
export const updatePost = (...items) => ({
type: 'UPDATE_POST',
items
});
I use Redux-thunk for this (https://github.com/gaearon/redux-thunk) - it lets you write action creators that return a function instead of an action - allowing you to perform async tasks in the action, then hit the reducer.
With redux-thunk you can call an async function (performSomeAsyncFunction() in example below), get the response, deal with it (such as the saveDataToLocalStorage() dummy function below), then hit the reducer to update your state:
export const startUpdatePost = (...items) => {
return (dispatch) => {
return performSomeAsyncFunction(...items).then((response) => {
dispatch(updatePost(...items));
saveDataToLocalStorage()
});
};
};
Don't forget to also handle the failure of the async function above