RTK Query endpoints and RTK actions happening out of order - reactjs

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.

Related

How to make a redux dispatch action async using redux toolkit

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}))

How to test state update with dispatch function of useReducer after get request?

I am trying to understand the logic of how to write unit tests. But i am stuck in here. I have a simple product listing page and it sends requests based on filtering options and returns related products. getProducts function pulls related products and updates my global state. How can i test this? How should be the logic?
getProducts Function
const getProducts = async () => {
const { color, brand, sort, search, page } = state;
const URL = `/api/v1/products?page=${page}&color=${color}&brand=${brand}&sort=${sort}&search=${search}`;
dispatch({ type: SETUP_PRODUCTS_BEGIN });
try {
const { data } = await axios(URL);
dispatch({
type: SETUP_PRODUCTS_SUCCESS,
payload: {
products: data.products,
colorOptions: data.colorOptions,
brandOptions: data.brandOptions,
numOfPages: data.numOfPages,
},
});
} catch (error) {
console.log(error);
}
};
Reducer
if (action.type === SETUP_PRODUCTS_BEGIN) {
return { ...state, isLoading: true };
}
if (action.type === SETUP_PRODUCTS_SUCCESS) {
return {
...state,
isLoading: false,
products: action.payload.products,
colorOptions: action.payload.colorOptions,
brandOptions: action.payload.brandOptions,
numOfPages: action.payload.numOfPages,
};
}
initialState
const initialState = {
products: [],
isLoading: false,
colorOptions: null,
brandOptions: null,
numOfPages: null,
};
Your question is too general.
You have mentioned two components in your question:
Product list React Component
A redux state with some reducers
In order to unit-test components, you need to be able to control all the external dependencies. All your code component/modules should have only one responsibility.
Let's analyze what you have:
Your UI component has too much knowledge - it knows how to get data from the back end, and it even knows the path of the API. Your component has to know how to render based on props/redux state and what actions to dispatch on certain events.
Unfortunately, if you use useDispathc and useElector redux hooks which menans you always have to test your component along with a redux state.
Here are my recomendations. (examples are using jest)
You need to unit test your reducer(s). Since they are pure javascript functions - we do not need to mock anything
import reducer from "/path-to-the-reducer";
describe("Action SETUP_PRODUCTS_BEGIN", () => {
it("Should set isLoading to true nomatter what", () => {
//setup
const initialState = { abc: "Germany", isLoading: undefined };
//act
cons resultState = reducer(initialState, { type: SETUP_PRODUCTS_BEGIN });
//assert
expect(resultState.isLoading).toBe(true);
});
})
In order to test your component you need two things:
2.1 Separate the rest api call into a separate module. In this case you will allow other UI components to reuse same logic for the API calls. This is very basic example:
RestApi.js
const listProducts = async (input) => {
const { color, brand, sort, search, page } = input;
const URL = `/api/v1/products?page=${page}&color=${color}&brand=${brand}&sort=${sort}&search=${search}`;
const axiosResult = await axios(URL);
return axios.data;
}
2.2 You need to use a library to test you react component. As of writing this post react recommends React testing library React testing.
So you need to do a few things:
mock the RestApi methods you need
mock the redux state and "listen for dispatches"
ensure your component renders correct things based on the props/reduxState
I will post some pseudo javascript code to how it should look
import { listProducts } from './restAPI'
//mockign the listProducts to return empty array each time
jest.mock('./restAPI', () => ({
listProducts : () => Promise.resolve([]),
}))
describe("Component initialization ", async () => {
it("Should call SETUP_PRODUCTS_BEGIN and SETUP_PRODUCTS_SUCCESS on componend load", () => {
/// configureStore from the #reduxjs/toolkit npm package
const store = configureStore({ reducer: {}, { isLoading: true, products: [] } })
const renderResult = await render(<Provider store={store}><ProductListComponent /></Provider>);
//assert
const actions = store.getActions();
expect(actions).toHaveLength(2);
expect(actions[0].type).toEqual("SETUP_PRODUCTS_BEGIN");
expect(actions[1].type).toEqual("SETUP_PRODUCTS_SUCCESS");
});
});
Redux in unit testing

'useEffect' not run only once

Thanks everyone, especially Mr.Drew Reese. If you are newbie as me, see his answer.
I don't know why but when I console log state data if I use useEffect, it always rerender although state generalInfo not change :/ so someone can help me to fix it and explain my wrong?
I want the result which is the data will be updated when generalInfo changes.
Thanks so much!
This is my useEffect
======================== Problem in here:
const {onGetGeneralInfo, generalInfo} = props;
const [data, setData] = useState(generalInfo);
useEffect(() => {
onGetGeneralInfo();
setData(generalInfo);
}, [generalInfo]);
======================== fix:
useEffect(() => {
onGetGeneralInfo();
}, []);
useEffect(() => {
setData(generalInfo);
}, [generalInfo, setData]);
this is mapStateToProps
const mapStateToProps = state => {
const {general} = state;
return {
generalInfo: general.generalInfo,
};
};
this is mapDispatchToProps
const mapDispatchToProps = dispatch => {
return {
onGetGeneralInfo: bindActionCreators(getGeneralInfo, dispatch),
};
};
this is reducer
case GET_GENERAL_INFO_SUCCESS: {
const {payload} = action;
return {
...state,
generalInfo: payload,
};
}
this is action
export function getGeneralInfo(data) {
return {
type: GET_GENERAL_INFO,
payload: data,
};
}
export function getGeneralInfoSuccess(data) {
return {
type: GET_GENERAL_INFO_SUCCESS,
payload: data,
};
}
export function getGeneralInfoFail(data) {
return {
type: GET_GENERAL_INFO_FAIL,
payload: data,
};
}
and this is saga
export function* getGeneralInfoSaga() {
try {
const tokenKey = yield AsyncStorage.getItem('tokenKey');
const userId = yield AsyncStorage.getItem('userId');
const params = {
method: 'GET',
headers: {
Authorization: `Bearer ${tokenKey}`,
},
};
const response = yield call(
fetch,
`${API_GET_GENERAL_INFO}?id=${userId}`,
params,
);
const body = yield call([response, response.json]);
if (response.status === 200) {
yield put(getGeneralInfoSuccess(body));
} else {
yield put(getGeneralInfoFail());
throw new Error(response);
}
} catch (error) {
yield put(getGeneralInfoFail());
console.log(error);
}
}
the initial state in redux and state in component is an empty array.
so I want to GET data from API. and I push it to redux's state. then I
useState it. I want to use useEffect because I want to update state
when I PUT the data and update local state after update.
Ok, so I've gathered that you want fetch the data when the component mounts, and then store the fetched data into local state when it is populated. For this you will want to separate out the concerns into individual effect hooks. One to dispatch the data fetch once when the component mounts, the other to "listen" for changes to the redux state to update the local state. Note that it is generally considered anti-pattern to store passed props in local state.
const {onGetGeneralInfo, generalInfo} = props;
const [data, setData] = useState(generalInfo);
// fetch data on mount
useEffect(() => {
onGetGeneralInfo();
}, []);
// Update local state when `generalInfo` updates.
useEffect(() => {
setData(generalInfo);
}, [generalInfo, setData]);
in your useEfect you are setting generalInfo and it causes change in the dependency array of useEffect. So, it runs over and over:
useEffect(() => {
onGetGeneralInfo();
setData(generalInfo);
}, [generalInfo]);
try this instead:
useEffect(() => {
onGetGeneralInfo();
setData(generalInfo); // or try to remove it if it is unnecessary based on below question.
}, []);
However, I don't understand why you have used setData(generalInfo); in useEffect when you have set it before. does it change in onGetGeneralInfo(); function?
Yow hook has or uses things that are not listed in the dependencies list
useEffect(() => {
onGetGeneralInfo();
setData(generalInfo);
}, [ onGetGeneralInfo, setData, generalInfo]);
Also let's remember that useEffect is call before the component mounts and after it mounts, so if you add a log it will be printed

dispatching to react store outside of component

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))
}
}
};

Redux ToolKit: is it possible to dispatch other actions from the same slice in one action created by createAsyncThunk

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
})

Resources