-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
Question: When fetching data with RTKQ on the server side, what is the proper way of making sure that redux state does not persist across requests?
Subquestion: If one creates an RTKQ api slice using the createSlice api (and then uses the injectEndpoints method on that slice), doesn't it make the slice into a singleton that, on the server, persists between requests, along with its state?
Another subquestion Can the resetApiState be used on the server when a new store is created? If an api slice is a singleton, wouldn't this interrupt any other possible fetches that were started during other requests and that may still be in flight?
===========
EXPANDED VERSION OF THE QUESTION
I am using redux-toolkit with RTKQ in a react app with a little bit of server rendering. The app is completely custom-made and does not use any off-the-shelf framework, but perhaps resembles the early Next.js with its pages router. The pages that render on the server export a serverFetch function that fetches data with the help of RTKQ.
I have recently realized that I am seeing a contamination of redux state between routes. To my utter astonishment, that contamination only appears in some routes and not in others (I can't explain this; I would have thought that if I am leaking state between requests, this would be obvious on any request to any route). This state contamination looks like this in the stringified state sent for rehydration:
A fragment of serialized state sent to the client for rehydration
{
"restApi": {
"queries": {
"genomeSummaryByGenomeSlug(\"grch38\")": {
"status": "fulfilled",
"endpointName": "genomeSummaryByGenomeSlug",
"requestId": "tOb9Kl-OiDuefN4IojSix",
"originalArgs": "grch38",
"startedTimeStamp": 1760628699336,
"data": {
"genome_id": "a7335667-93e7-11ec-a39d-005056b38ce3",
"genome_tag": "grch38",
"common_name": "Human",
"scientific_name": "Homo sapiens",
"species_taxonomy_id": "9606",
"type": null,
"is_reference": true,
"assembly": {
"accession_id": "GCA_000001405.29",
"name": "GRCh38.p14"
},
"release": {
"name": "2025-02",
"type": "integrated"
},
"latest_genome": {
"genome_id": "2b5fb047-5992-4dfb-b2fa-1fb4e18d1abb",
"genome_tag": null,
"common_name": "Human",
"scientific_name": "Homo sapiens",
"species_taxonomy_id": "9606",
"type": null,
"is_reference": true,
"assembly": {
"accession_id": "GCA_000001405.29",
"name": "GRCh38.p14",
"url": "https://identifiers.org/insdc.gca/GCA_000001405.29"
},
"release": {
"name": "2025-04-28",
"type": "partial",
"is_current": false
}
}
},
"fulfilledTimeStamp": 1760628699339
}
},
"mutations": {},
"provided": {
"tags": {},
"keys": {
"genomeSummaryByGenomeSlug(\"grch38\")": []
}
},
"subscriptions": {
"getHelpArticle({\"pathname\":\"/help/articles/gene-annotation\"})": {
"vi3PjshCKV0iWwqDhUe0M": {},
"pgxDyb7s8GWZolU42tIUu": {},
"6bFpEQ2g7lKmu0HMh7Uf-": {},
"Ai6vVG2-6KNSpDTqKxGjE": {},
"WryjuOk7AKBNM6WB6qeLJ": {},
"sYG_tPLny7dJWhXKr5sTj": {},
"c7QkZS9ylEF2lJLLxdv_P": {},
"tN7IEcYjiOiZYA1qrMf_l": {},
"1MCLWJXRREqO5DQMaiLD8": {},
"eC4N2O2jfvum_Mith_BZi": {},
"OkHoie2AaIzh2s2zcVQn_": {},
"uQvtNc6PkQ76hMWLJOF3-": {},
"T22kAwPXBP9bzrozhtvcJ": {},
"43oCs21ydXOWvyfniFj1m": {},
"XV5R69WzZ2yfts8gGCKJ-": {},
"dJHJE0MMDa02Q4ss7R0W7": {},
"wClvVGAxr4AUz0PalCTpD": {},
"rdH_uK_YYahxOEWDrBMx-": {},
"p3xW0eNuzPkkEUHznLh2x": {},
"54iU9ebr18kyacaMhOwW_": {},
"8CrkUMJTvOc3o1-3-wpC6": {},
"H0pXaOnVI_CDl9iYXJ3SI": {},
"N0Akq9rnZrt__lhO2em1-": {},
"pZEKFAJqVYshsxx3-z1J3": {},
"cVKxOiU3p4NNb0igNQS_R": {},
"eiusFkfGPBCgMu5AzNcbF": {},
"3fazjMjUavcVbpem7k7Xg": {},
"A1NW9zBABiAMOoWXXG69l": {},
"oQ7ohXxqx9Yo7acTnpuPZ": {},
"zQ6Nnm5mktkkmrsZVs-s7": {},
"VTLkKN4Nbegz26eGbs_0Y": {},
"gQWMlf_i5KstgRsFznfnQ": {},
"8AK_mn9jBUUjI7HLcFbHa": {},
"Sd3cwZM1yr1bOOIQv6gHc": {},
"5ZUrCdw95C_zLkHvgMKxQ": {},
"foyPkmJmZLYc9ojZAtd3e": {},
"a2qc6NvOVL9dGixvBaN0i": {},
"CkwFatxi5BcigLoLS6b7m": {},
"V_8ZRjOHMLPse6jbAmI-r": {},
"gcvRFzb3so4PGeKw1wjvj": {},
"7f8Axve_J6OHcOwLlt-Ly": {},
"MkLAKnJgfW9iStGJZ5hfB": {},
"nom7O_klzQ6Jjnm5KhWtP": {},
"P-6n2OCsNz3pn0VmHqpa4": {},
"a0tUoxqP8eCRzXaMN3uWc": {},
"TYGIFGgnZkyKrwg_XiILt": {},
"BtVoDvcVG03CpDqEIO4C9": {},
"k7fGO1A6AmCtm8MWnaI25": {},
"UcCOA5V0BrpYevZlxfl_L": {},
"wKGurxnDO00OJ23RzO51V": {},
"bK2rgh4jALRKUWXAiwkdf": {}
},
"getHelpArticle({\"pathname\":\"/help/articles/using-graph-ql,\"})": {
"VrYLOGcQsrGTRrVcwO7sP": {},
"Tr71WtH27GZBYpHK8Ljr-": {},
"UBkGfJ8LtXHtwLnOTCdzz": {},
"6CBI5naxsr_pJEmMusYUE": {},
"iRlGgbGy1JmlYO54o5PUH": {},
"9DL8QOOjfEJ1nppy4fDsx": {},
"dDY-bp3fvqyk66flOIOD4": {},
"hkweOhQlNx6qwlkT3WQMB": {},
"9VX3Ai2VQNYmYBOYneFHy": {},
"pPMX0A3jw5r27_azEGQjI": {},
"JgxoclGTSJ0LbDdj5sMeu": {},
"vIQzcGQte8LZazT8fVpjI": {},
"LVbabdS2Abju7rv32YCE9": {}
},
"getHelpArticle({\"pathname\":\"/help/articles/human-genome-automated-annotation\"})": {
"sYtfcz2bRMs3HBVIj2HlU": {},
"_-dBYjXcVEk7NB0CJfJrI": {},
"moYbWa6pkKrx7LvDdV201": {},
"bewIjXn5JPBbUtqQzvDPp": {},
"O5lPR_SbEdm0aflIkkMaC": {},
"0dtj_mSBXuhNGVGJSRDdY": {},
"ASFPond_C_JFroX-XRJJt": {},
"GKNUqWE5u_PmRMLu7YL4Y": {},
"biOvK0fDkzPU5CsYroWBm": {},
"Wk_eUO5vcLuFQcUC_cRG7": {},
"NVqYbC77z2Jiy8y_jZEXo": {},
"fz0QzRB-54jCItNbqwFmt": {},
"05RsZB9B6PrNU6P2Ni377": {},
"sqGTffTmly0U9aALtLheh": {},
"NbvnJY7Y6sUhTs8EN53JZ": {},
"V1qWkRUlugiQtaC6vdGEm": {},
"bY_-rv5-ap1y9ASUzRBNE": {},
"xwz384xYLcRBp2WvyuMQT": {},
"lZ_6wlihaj8PPKEES6XKa": {},
"u7ouvfiWS1S2YZfQZC0YG": {},
"aSZqbFsPWVVQi_1cy20DS": {},
"w4_SYgNY9DT5ZftnHfn97": {},
"orRvaZ3vIKoaedzQ_Gg9L": {},
"hXICt9k3gPWAV1SXNpZTJ": {},
"3A0Z_rqhswt4-VRWThgOh": {},
"o7M8-6yVAe5j-JxT6g59b": {}
},
"getHelpArticle({\"pathname\":\"/help/articles/braker-2-genome-annotation\"})": {
"EnECKzvpEpxCCmno_AAUL": {},
"ec0mkW8PGmQEzlzWdHEYC": {},
"GKvqnf9vmpNAJpvCffD3K": {},
"wCWpYoUpD8Iie6M-7nuou": {},
"kAOaDVqxELWh305BwEm6D": {},
"7GEgcpSA1rY0uFX9nRRHT": {},
"JJQY8G5fYl_xM5n2ilL4r": {},
"3EQ_BlC-URUmBWTo_jmCl": {},
"MZ3BcOzbchSGt1rU4DbiY": {},
"-nWn8LpignsRg5rRl_iuO": {},
"PGKyBWq6CSPTiecXDKxP_": {}
},
"getHelpArticle({\"pathname\":\"/help/articles/community-genome-annotation\"})": {
"VZtxxhvfKstZt5Fbn1gsf": {},
"W26eGgfZJDd8equwWYvMQ": {},
"t3WsDr8xH6KAxhvmVPgXm": {},
"BmoO3vlfGV2S_ifXMR3tm": {},
"6B6H-ecf2adxSW8-I17i0": {},
"tc4RHN84UYE7ByCLsBjsJ": {},
"Rr6AXzAf4biU7Rg1qD8i4": {},
"FZO65gKCZy7eKECs8Gc1C": {},
"yb2K2cs6U6qvpnTARcwyp": {},
"PPFZV7f35zD18uLujM01s": {},
"dN_n-iET09xfBu9LWPCbC": {},
"M854esrWN50I6BHiKhSkR": {},
"LO7EXPsiz-F3xAJm1O6AK": {},
"4hVrLTVT0wGSGDR3IBTtm": {},
"nZo6y9eXQFMENMn0pSglg": {},
"ym9fR-5rseU5gNZmYQILx": {},
"HGHoP7NQ6edIUFc8kuoEC": {},
"0S0x3TBPYa71INDtgFF1K": {},
"cpahuIE8XOlcZ0tvWb9bh": {},
"DDplzkYyxG3PGeocmxvOw": {},
"CzLWL_dO-kzcJaGDSkqDo": {},
"5R72LFK1Sj8NPHqxplts0": {},
"pIW9swFT8KJtIY-jEJJLB": {},
"2eFsq6j7k7uUPd0A0ERQK": {},
"86OUeXe53eRcO9yHz_2TV": {},
"7NXA0XJAhb6rUR4hHyo-5": {}
},
"getHelpArticle({\"pathname\":\"/help/articles/vertebrate-genome-annotation\"})": {
"0c5q99Cq8MycPOPN9F9xC": {},
"zNth3VRPoRQcvszkiy3pk": {},
"FuXHw9qcpQIl26HzRogMN": {},
"rfpJNwMnSNmHerJpyz1RV": {},
"5Kge36V1ulHbeZmM4Tygb": {},
"L1EOeG-RHSc4k6jaFLolL": {},
"sSguEFQWZO1xiSkeYU0vu": {},
"T1LuwzJR1gvZ7lZNcQUvb": {},
"Vt__lp6F0A3AOhJkLN8Ag": {},
"W1Mm0XNFfTpUaDDiUWkPt": {},
"RBnT6L5L98UDhzKykX5L1": {},
"dk5Ofw_2G7TDV1Q7st8se": {},
"CpHSics9Lx5ujp-C_Pbcn": {},
"WObKp5tQ7WPtkXulsGcLG": {},
"jQGf9VJ9WlVrj1eX5DnO5": {},
"BGGPA9EtN6XDpqgrmrCWW": {},
"VV8kKOI79vw4jJIydWOv0": {},
"K0Rbl1JjQXGsrlM3sWDUu": {},
"BClqVsMDPTABw1lA0B1uk": {},
"5PSbR7IJIBnAHDUYhzw_E": {},
"mp9vDeP52JkvdRYlinv9v": {},
"Hu2Q0XCwYvhh0LOrtHdmn": {},
"DSs0y1bOWyhS63o1K81Wz": {},
"qv8ppHGUAZ0KaAKXRMszr": {},
"yKua7MujIE6k2euXtYH9z": {},
"-w81HnGA9qyIYH_RtEbPX": {},
"jqfcXhiD2dhiU4p-_wr8v": {},
"o3Sd6kFACzCufYa2X3awj": {},
"yw9J42Qi5i-A6YB-_TQGY": {},
"cSVacC1Z7Hss3z0wIvyu4": {},
"cxtrrfBrI6vQ-5BK-NXRp": {},
"6J9Z44k5YtBlUkJBsPKN4": {},
"mhSlYHa9KBVI3AcjKgBdk": {},
"jfl6krl-F6rGuIWOCA1xt": {},
"AMISOWxYR6lXwghYhThIH": {},
"KOkpZP0RBEfJV3s1LMoG1": {},
"5muIkM2Q-J7gQ2NcUurQf": {},
"PRcvkSVY93N-GmzNJwoyJ": {},
"pcqoVJcwSI8vj3gMHkU5z": {},
"tX2YgjYPSUwJCqMYj8GuF": {},
"pFtnACQgzCRRNa2IVCtU4": {}
}
}One obvious lesson that I learnt from this was that I forgot to unsubscribe from a particular query. But it also made it painfully obvious to me that I do not fully understand how redux works on the server.
In particular, here is how I am creating a redux store:
import restApiSlice from 'src/shared/state/api-slices/restSlice';
import createRootReducer from 'src/root/rootReducer';
const middleware = [graphqlApiSlice.middleware, restApiSlice.middleware];
export const getServerSideReduxStore = () => {
return configureStore({
reducer: createRootReducer(),
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(middleware)
});
};The restApiSlice, in turn, is created and exported like this:
// from the restSlice file
export default createApi({
reducerPath: 'restApi',
baseQuery: staggeredBaseQueryWithBailout,
endpoints: () => ({}) // will inject endpoints in other files
});The above snippet suggests to me that restApiSlice becomes a singleton that is created only once during the first module evaluation, and is not recreated at every request to the server. It must be a singleton, because it would be imported in other files that would inject their endpoints into it, like so:
// another api slice
import restApiSlice from 'src/shared/state/api-slices/restSlice';
// ...
const helpApiSlice = restApiSlice.injectEndpoints({
endpoints: (builder) => ({
getHelpArticle: builder.query<HelpArticleResponse, HelpArticleQueryParams>({
query: (params) => ({
url: `${config.docsBaseUrl}/article?url=${encodeURIComponent(
params.pathname
)}`
})
})
})
});Eventually, this getServerSideReduxStore function is called in an Express route per every request; so every request creates a brand new store:
const viewRouter = async (req: Request, res: Response) => {
const reduxStore = getServerSideReduxStore();
const matchedPageConfig = routesConfig.find((route) =>
matchPath(route.path, req.path)
) as RouteConfig;
let statusCode = 200;
let didError = false;
if (matchedPageConfig.serverFetch) {
try {
const fetchResult = await matchedPageConfig.serverFetch({
path: req.path,
store: reduxStore
})
}
}
// ...But, if restApiSlice is a singleton, then creating a fresh copy of the store every request won't help, if it uses the same restApiSlice every time. And, if restApiSlice holds a state and if it persists through the whole lifetime of a server, how to prevent it from carrying over its state between requests?
Repro
https://github.com/azangru/repro-redux-toolkit-query-server
In the repro, If an api slice has an async queryFn that returns immediately, then the subscriptions block in the serialized redux store is always empty (good!). However, if a queryFn function takes long time to return, and if server-side code does not explicitly unsubscribe from the query after its completion, then the serialized redux store will contain evidence of other requests.
My questions to this are:
- Why do previous subscriptions persist in the serialized redux state even though a new store is created every request?
- What is the proper way of handling server-side redux queries to make sure that there is no cross-request state contamination?