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))
}
}
};
Related
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}))
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 a mern app and i'm using redux to maintain state of my posts, I want to fetch all data from my api at first run of the app (when the app component loads initially) but I can't achieve it. It only works when I post something and it fetches the post, but it doesn't fetch all the posts from db initially.
After struggling for a day I decided ask here.
This is my component tree:
In my PostsBody, I want to fetch all the posts from the database whenever the app loads initially (this is not happening) and then whenever there is a change in state like create, delete it should fetch the updated posts (this is happening).
This is my PostsBody component:
import React, { useEffect } from "react";
import { useDispatch, useSelector } from 'react-redux';
import Post from "./Post";
import { getPostsAction } from '../actions/posts';
const PostsBody = () => {
const dispatch = useDispatch();
// fetching posts
useEffect(() => {
dispatch(getPostsAction);
}, [dispatch]);
const posts = useSelector((globalState) => globalState.postsReducer);
console.log(posts); // intially empty when the app reloads/renders.
return (
// simply posts.map to display individual posts
);
}
export default PostsBody;
Action:
export const getPostsAction = () => async (dispatch) => {
try {
const { data } = await getAllPosts();
const action = {
type: 'GET_ALL',
payload: data,
}
dispatch(action);
} catch (error) {
console.log(error.message);
}
}
GET CALL:
import axios from 'axios';
const url = "http://localhost:5000/users";
export const getAllPosts = () => axios.get(url);
Reducer:
const postsReducer = (posts=[], action) => {
switch (action.type) {
case 'GET_ALL':
return action.payload;
case 'CREATE':
return [...posts, action.payload];
default: return posts;
}
}
export default postsReducer;
I repeat, the only problem is, it is not fetching all the posts from db initially when the app renders, after that when I create a new post it does fetch that post (not all from db).
Issues
It doesn't appear as though you are invoking the getPostsAction action creator correctly. Also, with only dispatch in the useEffect's dependency array the hook callback will only be invoked once when the component mounts.
Solution
Invoke the getPostsAction action.
useEffect(() => {
dispatch(getPostsAction()); // <-- invoke
}, [dispatch]);
Now this still only solves for fetching posts from the DB when the component mounts, but not when new posts are POST'd to your backend.
I've looked at your actions and state. Normally you would include another variable in the useEffect dependency array to trigger the effect callback to execute again, but I think a simpler way is possible. Instead of POST'ing the new post and dispatching the CREATE action you should POST the new "post" and immediately GET all posts and dispatch the GET_ALL action instead with that data.
export const createPostAction = (newPostData) => async (dispatch) => {
try {
await createPost(newPostData);
getAllPosts()(dispatch);
} catch (error) {
console.log(error.message);
}
}
I've basic familiarity with Thunks, but if the above doesn't work then you may need to duplicate some behavior, or factor it out into some common utility code used by both action creators.
export const createPostAction = (newPostData) => async (dispatch) => {
try {
await createPost(newPostData);
const { data } = await getAllPosts();
const action = {
type: 'GET_ALL',
payload: data,
}
dispatch(action);
} catch (error) {
console.log(error.message);
}
}
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