Sequential execution / queue of endpoint of createApi - redux-toolkit

One of my endpoints should be called one by one, since the backend doesnt support multiple parallel requests. Ive tried two ways to do it:
Using onQueryStarted:
const sequenceMutex = new Mutex()
async onQueryStarted(id, { dispatch, queryFulfilled }) {
// wait until the sequenceMutex is available
await sequenceMutex.waitForUnlock()
const releaseSequence = await sequenceMutex.acquire()
await queryFulfilled // the endpoint already started here, cant manage the execution
releaseSequence()
}
Using a different baseQuery. But I dont see any way to use a different baseQuery for an especific endpoint:
const sequenceMutex = new Mutex()
export const sequentialBaseQueryWithReauth = async (
args,
api,
extraOptions
) => {
// wait until the sequenceMutex is available
await sequenceMutex.waitForUnlock()
const releaseSequence = await sequenceMutex.acquire()
const result = await baseQueryWithAuthentication(args, api, extraOptions)
releaseSequence()
return result
}

You could iterate on your answer and kick that yourEndpointSequentialBaseQuery.
const apiSlice = api.injectEndpoints({
endpoints: (builder) => ({
yourEndpoint: builder.query({
queryFn: (args, api, extraOptions, baseQuery) => {
await mutex.waitForUnlock()
const releaseSequence = await mutex.acquire()
try {
return baseQuery({
url: '/your-route',
method: 'POST',
body: args,
})
} finally {
releaseSequence()
}
},
}),
}),
})

Finally I created a generic seuqnetial baseQuery function to reuse it across the app.
First, create a baseQuery that accepts a mutex instance as argument:
export const sequentialBaseQueryFactory = (mutex: InstanceType<typeof Mutex>) => {
const sequentialBaseQuery: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError> = async (
args,
api,
extraOptions,
) => {
await mutex.waitForUnlock()
const releaseSequence = await mutex.acquire()
const result = await yourBaseQuery(args, api, extraOptions) // use your createApi baseQuery
releaseSequence()
return result
}
return sequentialBaseQuery
}
Then, create the custom baseQuery for your endpoint using a mutex instance :
import { Mutex } from 'async-mutex'
// mutex for sequential calls of your endpoint
const yourEndpointSequenceMutex = new Mutex()
const yourEndpointSequentialBaseQuery = sequentialBaseQueryFactory(yourEndpointSequenceMutex)
Use the custom baseQuery in your endoint:
const apiSlice = api.injectEndpoints({
endpoints: (builder) => ({
yourEndpoint: builder.query({
queryFn: (args, api, extraOptions) => {
return yourEndpointSequentialBaseQuery({
url: '/your-route',
method: 'POST',
body: args,
}, api, extraOptions)
},
}),
}),
})

Related

use Effect not working to bring up my product, using axios use params,

This code is not working for me i am trying to pull data from my mongodb
const ProductScreen = ({ match }) => {
const [product, setProduct] = useState({});
const { id } = useParams();
useEffect(() => {
const fetchProduct = async () => {
const { data } = await axios.get(
`/api/product/${encodeURIComponent(id)}`
);
setProduct(data);
};
fetchProduct();
}, []);
};
pull data from server of mongo db
It is possible when the component first mounts, id is null and useParams() doesn't get it till the second render. So add an if statement in your useEffect to make sure the id is present. Also add id to the dependency array, so if the id changes, you will refetch the data for it. Otherwise, with an empty dependency array, the useEffect will only run on first mount.
const ProductScreen = ({ match }) => {
const [product, setProduct] = useState({});
const { id } = useParams();
useEffect(() => {
const fetchProduct = async () => {
const { data } = await axios.get(
`/api/product/${encodeURIComponent(id)}`
);
setProduct(data);
};
if (id) {
fetchProduct();
}
}, [id]);
};

React-query: invalidateQueries doesn't work

I'm trying to invalidate queries when I create new comment.
const { data: comments } = useQuery("getComments", () => getComments({ originalKind: "NOTICE", originalSeq: id }));
const createCommentMutation = useMutation(postComment, {
onSuccess: async () => {
const queryClient = new QueryClient();
await queryClient.invalidateQueries("getComments");
},
});
The comment is created successfully, but invalidateQueries dose not working.
There is no default options...
every time i create comment, the query will invalidated
If you create a new QueryClient, it will have a new QueryCache, which is not associated with the cached data of your query. That's not how it works, and that's also not what any of the official examples / docs are showing.
What you have to do is get access to the client with useQueryClient() - another hook exported from react-query. This will give you the singleton QueryClient that you have put into the QueryClientProvider:
import { useQueryClient } from '#tanstack/react-query'
const queryClient = useQueryClient()
const createCommentMutation = useMutation(postComment, {
onSuccess: async () => {
await queryClient.invalidateQueries("getComments");
},
});

Custom queryFn reusing other endpoints

Using code from https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#performing-multiple-requests-with-a-single-query
Note that I added the endpoint getRandomUser:
import {
createApi,
fetchBaseQuery,
FetchBaseQueryError,
} from '#reduxjs/toolkit/query'
import { Post, User } from './types'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/ ' }),
endpoints: (build) => ({
getRandomUser: builder.query<User, void>({
query: () => ({
url: `users/random`,
}),
}),
getRandomUserPosts: build.query<Post, void>({
async queryFn(_arg, _queryApi, _extraOptions, fetchWithBQ) {
// get a random user
const randomResult = await fetchWithBQ('users/random') // avoid repetition
if (randomResult.error) throw randomResult.error
const user = randomResult.data as User
const result = await fetchWithBQ(`user/${user.id}/posts`)
return result.data
? { data: result.data as Post }
: { error: result.error as FetchBaseQueryError }
},
}),
}),
})
Since in my example code I already have a getRandomUser endpoint defined, I would like to avoid repetition in getRandomUserPosts await fetchWithBQ('users/random') and directly call the endpoint getRandomUser.
I tried to access it with _queryApi.endpoints but it doesn't seem to be defined (I don't know if it can point to a key in the very same object endpoints) and even if it would I didn't know how to use it to replace the repetition.
How to approach these situations?

How to use axios to `fetch all()`

I don't understand how to use axios to fetch data from an array of urls. But I can do it with fetch. The following code works perfectly:
const url = 'https://vimeo.com/api/oembed.json?url='
async index(videoUrls = []) {
try {
const response = await Promise.all(
// videoUrls.map(videoUrl => axios.$get(`${url}${encodeURIComponent(videoUrl)}`))
videoUrls.map(videoUrl => fetch(`${url}${encodeURIComponent(videoUrl)}`))
)
const results = await Promise.all(response.map(r => r.json()));
return results;
} catch (e) {
console.error(e)
}
}
When I make a call like index(["https://vimeo.com/216850224", "https://vimeo.com/642263700"]), my console shows an array with all the video meta details vimeo has to give me. This is perfect.
But the moment I comment out the line that uses fetch and use axios, I get a CORS error.
What is the idiomatic way to fetch data from a bunch of urls in axios?
EDIT
I also tried this, but the .all() function doesn't seem to exist
async index(videoUrls = []) {
try {
const response = await axios.all(videoUrls.map(videoUrl => `${url}${encodeURIComponent(videoUrl)}`));
return response;
} catch (e) {
console.error(e)
}
}
You can easily do it like below:
(async function getAll() {
const axiosrequest1 = axios.get('https://jsonplaceholder.typicode.com/posts');
const axiosrequest2 = axios.get('https://jsonplaceholder.typicode.com/posts');
const axiosrequest3 = axios.get('https://jsonplaceholder.typicode.com/posts');
const [res1, res2, res3] = await Promise.all([axiosrequest1, axiosrequest2, axiosrequest3]);
console.log('request1', res1.data);
console.log('request2', res2.data);
console.log('request3', res3.data);
})();
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
The Axios version would be slightly different because it automatically decodes and embeds the response body into the response.data property (no need for res.json())
const baseUrl = "https://vimeo.com/api/oembed.json"
const index = async (videoUrls = []) => {
// create an array of responses and wait for them to resolve
const responses = await Promise.all(
videoUrls.map(url => axios.get(baseUrl, { params: { url } })
)
// extract the `data` properties and return them as an array
return responses.map(({ data }) => data)
}
Exactly when you extract response.data is totally up to you. It could also look like this
const index = (videoUrls = []) => Promise.all(
videoUrls.map(async (url) => (
await axios.get(baseUrl, { params: { url } })
).data)
)
FYI, your fetch() version could be a little cleaner too...
const baseUrl = "https://vimeo.com/api/oembed.json"
const index = (videoUrls = []) => Promise.all(
videoUrls.map(async (url) => {
const params = new URLSearchParams({ url })
const res = await fetch(`${baseUrl}?${params}`)
if (!res.ok) { // check for bad response
throw new Error(`${res.status}: ${await res.text()}`)
}
return res.json()
})
)

Best practice for using React hooks and Context API to update global state and fetch/provide data from multiple endpoints

I am new to React hooks/Context API. I have read the React hook/context docs, and I am still having trouble with the following:
My attempts to update global state by multiple consumer components
currently causes frequent overwriting of context state due to
rerendering (e.g., activity or details state is sometimes
null/undefined). This probably is why...
... I am getting 400 (bad request) and/or 500 (server) errors on random refreshes of the page (~30% of the time content loads as
expected, ~70% errors are thrown. I believe this is happening
because we have various context states that are being called
asynchronously).
I am not sure how to implement Axios Cancellation, given that our useEffect hooks are calling dispatch functions (e.g.,
getActivities()) in different files. The examples I've seen
involve fetching data within the component (rather than in context).
I am seeking assistance for #1 specifically. I would love guidance on how to accurately fetch data and store in context as global state, and then provide that context to child components, allowing them to consume/update context state without unnecessary rerendering.
Tried to only provide relevant code snippets below:
ActivityState.js -- should fetch activity data
...
const ActivityState = props => {
const initialState = {
activities: [],
isLoading: false,
isError: false
};
const HEADERS = {
'Content-Type': 'application/json',
'user_id': 1
}
const [state, dispatch] = useReducer(ActivityReducer, initialState);
const userContext = useContext(UserContext);
const getActivities = async () => {
const { loggedIn } = contactContext;
let didCancel = false; // attempts to start implementing axios cancellation
try {
const res = await axios.get(url);
dispatch({ type: GET_ACTIVITIES, payload: res.data.data.activities });
} catch (err) {
if (!didCancel) {
dispatch({ type: 'FETCH_FAILURE' });
}
}
}
const updateActivity = (path, data) => { //update context state
dispatch({ type: UPDATE_ACTIVITY, payload: { path: path, data: data } });
};
const saveActivity = () => { //send new activity data to the backend
const postData = {
actions: [{"293939": []}],
activities: state.activities
};
try {
const res = axios.post(url,{ data: postData }, { headers: HEADERS });
} catch (err) {
console.log(err);
}
}
return (
<ActivityContext.Provider
value={{
activities: state.activities,
data: state.data,
backup_data: state.backup_data,
getActivities,
updateActivity,
saveActivity,
}}
>
{props.children}
</ActivityContext.Provider>
);
};
export default ActivityState;
ActivityReducer.js -- switch statements to be dispatched by ActivityState.js
...
export default (state, action) => {
switch (action.type) {
case GET_ACTIVITIES:
return {
...state,
activities: action.payload,
isLoading: true
};
case FETCH_FAILURE:
return {
...state,
isLoading: false,
isError: true
};
case UPDATE_ACTIVITY:
const { payload: { path }, payload } = action;
const data = state;
if (!data.activities)
return { data };
const index = data.activities.findIndex(e => e.socium_tracking_number == path.id);
if(index === -1)
return { data };
_.set(data, `activities[${index}].${path.field}`, payload.data);
return {
data,
};
...
DetailsState.js -- dispatch functions to fetch details
const DetailsState = props => {
const initialState = {
details: null,
};
const [state, dispatch] = useReducer(DetailsReducer, initialState);
const getDetails = async () => {
try {
const res = await axios.get(url);
dispatch({ type: GET_DETAILS, payload: res.data.data[0].details});
}catch(err) {
console.log(err)
}
};
return (
<DetailsContext.Provider
value={{ details: state.details, getDetails }}
>
{ props.children }
</DetailsContext.Provider>
);
}
export default SchemaState;
DetailsReducer.js -- switch statement
export default (state, action) => {
switch (action.type) {
case GET_DETAILS:
return {
...state,
details: action.payload,
};
default:
return state;
}
};
ActivityTable.js -- component that consumes Activity Info
...
const ActivityTable = ({ activity }) => {
const activityContext = useContext(ActivityContext);
const { activities, filtered, getActivities } = activityContext;
const [order, setOrder] = React.useState('asc');
const [orderBy, setOrderBy] = React.useState(activities.wait_time);
// Get activity data on mount
useEffect(() => {
async function fetchData() {
await getActivities()
}
fetchData();
}, []);
...
CreateActivity.js -- component that consumes Activity and Details data
...
const CreateActivity = props => {
const activityContext = useContext(ActivityContext);
const { activities, filtered, getActivities, addActivity } = activityContext;
const detailsContext = useContext(DetailsContext);
const { details, getDetails } = detailsContext;
// Get activity and details data on mount
useEffect(() => {
async function fetchData() {
await getActivities();
await getSchema();
}
fetchData();
}, []);
...
I really tried to get smarter on these issues before approaching the SO community, so that my question(s) was more defined. But this is what I have. Happy to provide any info that I missed or clarify confusion. Thank you for your time