Custom queryFn reusing other endpoints - redux-toolkit

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?

Related

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

Sequential execution / queue of endpoint of createApi

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

What is cancelToken by axios and how I fix it?

I made a React app and I making requests to the backend using Axios. I created a middleware in my backend for authorization and on the frontend side I'm trying to pass to every call that is made to the backend the auth token if exists in the localStorage. Before I added the logic for that everything worked perfectly, now every time I try to log in or register I get this in the console
TypeError: Cannot read properties of undefined (reading 'cancelToken')
at throwIfCancellationRequested (dispatchRequest.js:12:1)
at dispatchRequest (dispatchRequest.js:24:1)
at async auth.js:6:1
My index.js which handles every call to the backend looks like this:
import axios from 'axios';
const API = axios.create({
baseURL: 'http://localhost:3500'
})
API.interceptors.request.use((req) => {
if (localStorage.getItem('profile')) {
req.headers.Authorization = `Bearer ${JSON.parse(localStorage.getItem('profile')).token}`
}
})
export const fetchHunts = () => API.get('/hunts');
export const createHunt = (newHunt) => API.post('/hunts', newHunt);
export const updateHunt = (id, updatedHunt) => API.patch(`/hunts/${id}`, updatedHunt);
export const deleteHunt = (id) => API.delete(`/hunts/${id}`);
export const signInAdmin = (formData) => API.post('/admins/signinadmin', formData);
export const signUpAdmin = (formData) => API.post('/admins/signupadmin', formData);
Right now I am not logged in so there is no profile in the localStorage. I tried to add this, I found this here on stack overflow but didn't work
const CancelToken = Axios.CancelToken;
instance.interceptors.request.use(req => {
/* some logic */
const CancelToken = Axios.CancelToken;
return {
...req,
cancelToken: new CancelToken((cancel) => cancel('Cancel repeated request'))
};
});
but when I used this it only returned " Cancel repeated request " and did nothing. Do you know how can I fix that? Thank you in advance!
Based on the Axios Documentation - Interceptors, the interceptor function should return the req.
API.interceptors.request.use((req) => {
if(localStorage.getItem('profile')) {
req.headers.Authorization = `Bearer ${JSON.parse(localStorage.getItem('profile')).token}`;
}
return req;
})
My 2cents:
looks like it's easier NOT to make the request in the first place, if user is not authorized =)
Just add a global middleware on frontend to redirect user to auth page.
Here is an example in Vue, but you get the logic.
import { Middleware } from '#nuxt/types';
import { RoutesName } from '~/shared/repository/routes/routes-name';
const auth: Middleware = async (context) => {
const { route, store } = context;
const isAuthorized = store.getters['user/isAuthorized'];
const isAuthPage = route.name === RoutesName.auth;
if (!isAuthorized && !isAuthPage) {
return context.redirect(`/${RoutesName.auth}`);
}
if (isAuthorized && isAuthPage) {
return context.redirect('/');
}
};
export default auth;

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

PRISMA: How to receive REST API post requests (non GraphQL)?

How to create one route for receiving non graphql post requests?
I have my graphql server, and want to receive some non graphql data on it.
const server = new GraphQLServer({ ... })
server.express.get('/route', async (req, res, done) => {
const params = req.body;
// do some actions with ctx..
})
How can we access to ctx.db.query or ctx.db.mutation from this route?
Thanks!
Related question: https://github.com/prisma/graphql-yoga/issues/482
https://www.prisma.io/forum/t/how-to-create-one-route-for-receiving-rest-api-post-requests/7239
You can use the same variable you passed in the context:
const { prisma } = require('./generated/prisma-client')
const { GraphQLServer } = require('graphql-yoga')
const server = new GraphQLServer({
typeDefs: './schema.graphql',
resolvers,
context: {
prisma,
},
})
server.express.get('/route', async (req, res, done) => {
const params = req.body;
const user = prisma.user({where: {id: params.id} })
res.send(user)
})