[{"data":1,"prerenderedAt":1540},["ShallowReactive",2],{"blog-\u002Fblog\u002Freact-async-state-react-query-guide":3},{"id":4,"title":5,"body":6,"description":1525,"difficulty":1526,"extension":1527,"framework":1528,"frameworkSlug":1529,"meta":1530,"navigation":148,"order":165,"path":1531,"qaPath":1532,"seo":1533,"stem":1534,"subtopic":1535,"topic":1536,"topicSlug":1537,"updated":1538,"__hash__":1539},"blog\u002Fblog\u002Freact-async-state-react-query-guide.md","Async State & React Query — Complete React Interview Guide",{"type":7,"value":8,"toc":1517},"minimark",[9,26,37,42,45,51,57,89,96,100,114,449,452,474,480,500,504,507,514,645,659,684,688,701,1045,1052,1356,1366,1370,1373,1383,1389,1396,1403,1407,1410,1428,1437,1447,1485,1507,1513],[10,11,12,13,17,18,21,22,25],"p",{},"Every React developer has written it: a ",[14,15,16],"code",{},"useEffect"," that fires on mount,\nsets a loading flag, calls ",[14,19,20],{},"fetch",", then fans out into three separate\n",[14,23,24],{},"setState"," calls for data, loading, and error. It works — until you need\ncaching, background refreshes, race-condition protection, pagination, or\nretry logic. At that point the 20-line hook becomes a 100-line custom hook\nthat you maintain forever.",[10,27,28,32,33,36],{},[29,30,31],"strong",{},"TanStack Query"," (React Query) exists to solve exactly this problem. It\nis not a general-purpose state manager. It is a dedicated layer for\n",[29,34,35],{},"server state"," — data that lives on a server, changes asynchronously, and\nneeds to stay synchronized with your UI. Understanding when and why to\nreach for it is one of the clearest signals that separates junior from\nsenior React developers in interviews.",[38,39,41],"h2",{"id":40},"server-state-vs-client-state-the-mental-model-shift","Server State vs Client State — The Mental Model Shift",[10,43,44],{},"Before writing a single line of React Query, you need to understand the\nfundamental distinction it is built on.",[10,46,47,50],{},[29,48,49],{},"Client state"," is ephemeral and owned entirely by your app: whether a\nmodal is open, which tab is selected, a partially typed form value. You are\nthe only author. It never goes stale.",[10,52,53,56],{},[29,54,55],{},"Server state"," is a snapshot of remote data that your app does not fully\nown: a list of todos, a user profile, an order history. It can change\noutside your app at any moment — another user edits a record, a background\njob updates a field. Because of this, server state has properties that\nclient state never has:",[58,59,60,68,75,82],"ul",{},[61,62,63,64,67],"li",{},"It can become ",[29,65,66],{},"stale"," while your component is on screen.",[61,69,70,71,74],{},"Multiple components might ",[29,72,73],{},"request the same data simultaneously"," and\nshould share one network request.",[61,76,77,78,81],{},"It needs to be ",[29,79,80],{},"cached"," so navigation feels instant.",[61,83,84,85,88],{},"It must be ",[29,86,87],{},"invalidated"," after mutations so the UI reflects the latest\nserver truth.",[10,90,91,92,95],{},"Managing server state with ",[14,93,94],{},"useState"," forces you to re-implement all of\nthese concerns by hand. React Query gives you the full solution out of the\nbox.",[38,97,99],{"id":98},"usequery-fetching-caching-and-refetching","useQuery — Fetching, Caching, and Refetching",[10,101,102,105,106,109,110,113],{},[14,103,104],{},"useQuery"," is the workhorse. At minimum it takes a ",[14,107,108],{},"queryKey"," and a\n",[14,111,112],{},"queryFn",":",[115,116,121],"pre",{"className":117,"code":118,"language":119,"meta":120,"style":120},"language-jsx shiki shiki-themes github-light github-dark","import { useQuery } from '@tanstack\u002Freact-query'\n\nfunction TodoList() {\n  const { data, isLoading, isError, error, isFetching } = useQuery({\n    queryKey: ['todos'],\n    queryFn: () => fetch('\u002Fapi\u002Ftodos').then(r => r.json()),\n    staleTime: 1000 * 30,   \u002F\u002F treat data as fresh for 30 seconds\n  })\n\n  if (isLoading) return \u003CSkeleton \u002F>\n  if (isError)   return \u003CErrorBanner message={error.message} \u002F>\n\n  return (\n    \u003C>\n      {isFetching && \u003CRefreshIndicator \u002F>}  {\u002F* background refresh *\u002F}\n      \u003Cul>{data.map(todo => \u003CTodoItem key={todo.id} {...todo} \u002F>)}\u003C\u002Ful>\n    \u003C\u002F>\n  )\n}\n","jsx","",[14,122,123,143,150,163,209,221,266,288,294,299,320,343,348,357,363,386,432,438,444],{"__ignoreMap":120},[124,125,128,132,136,139],"span",{"class":126,"line":127},"line",1,[124,129,131],{"class":130},"szBVR","import",[124,133,135],{"class":134},"sVt8B"," { useQuery } ",[124,137,138],{"class":130},"from",[124,140,142],{"class":141},"sZZnC"," '@tanstack\u002Freact-query'\n",[124,144,146],{"class":126,"line":145},2,[124,147,149],{"emptyLinePlaceholder":148},true,"\n",[124,151,153,156,160],{"class":126,"line":152},3,[124,154,155],{"class":130},"function",[124,157,159],{"class":158},"sScJk"," TodoList",[124,161,162],{"class":134},"() {\n",[124,164,166,169,172,176,179,182,184,187,189,192,194,197,200,203,206],{"class":126,"line":165},4,[124,167,168],{"class":130},"  const",[124,170,171],{"class":134}," { ",[124,173,175],{"class":174},"sj4cs","data",[124,177,178],{"class":134},", ",[124,180,181],{"class":174},"isLoading",[124,183,178],{"class":134},[124,185,186],{"class":174},"isError",[124,188,178],{"class":134},[124,190,191],{"class":174},"error",[124,193,178],{"class":134},[124,195,196],{"class":174},"isFetching",[124,198,199],{"class":134}," } ",[124,201,202],{"class":130},"=",[124,204,205],{"class":158}," useQuery",[124,207,208],{"class":134},"({\n",[124,210,212,215,218],{"class":126,"line":211},5,[124,213,214],{"class":134},"    queryKey: [",[124,216,217],{"class":141},"'todos'",[124,219,220],{"class":134},"],\n",[124,222,224,227,230,233,236,239,242,245,248,250,254,257,260,263],{"class":126,"line":223},6,[124,225,226],{"class":158},"    queryFn",[124,228,229],{"class":134},": () ",[124,231,232],{"class":130},"=>",[124,234,235],{"class":158}," fetch",[124,237,238],{"class":134},"(",[124,240,241],{"class":141},"'\u002Fapi\u002Ftodos'",[124,243,244],{"class":134},").",[124,246,247],{"class":158},"then",[124,249,238],{"class":134},[124,251,253],{"class":252},"s4XuR","r",[124,255,256],{"class":130}," =>",[124,258,259],{"class":134}," r.",[124,261,262],{"class":158},"json",[124,264,265],{"class":134},"()),\n",[124,267,269,272,275,278,281,284],{"class":126,"line":268},7,[124,270,271],{"class":134},"    staleTime: ",[124,273,274],{"class":174},"1000",[124,276,277],{"class":130}," *",[124,279,280],{"class":174}," 30",[124,282,283],{"class":134},",   ",[124,285,287],{"class":286},"sJ8bj","\u002F\u002F treat data as fresh for 30 seconds\n",[124,289,291],{"class":126,"line":290},8,[124,292,293],{"class":134},"  })\n",[124,295,297],{"class":126,"line":296},9,[124,298,149],{"emptyLinePlaceholder":148},[124,300,302,305,308,311,314,317],{"class":126,"line":301},10,[124,303,304],{"class":130},"  if",[124,306,307],{"class":134}," (isLoading) ",[124,309,310],{"class":130},"return",[124,312,313],{"class":134}," \u003C",[124,315,316],{"class":174},"Skeleton",[124,318,319],{"class":134}," \u002F>\n",[124,321,323,325,328,330,332,335,338,340],{"class":126,"line":322},11,[124,324,304],{"class":130},[124,326,327],{"class":134}," (isError)   ",[124,329,310],{"class":130},[124,331,313],{"class":134},[124,333,334],{"class":174},"ErrorBanner",[124,336,337],{"class":158}," message",[124,339,202],{"class":130},[124,341,342],{"class":134},"{error.message} \u002F>\n",[124,344,346],{"class":126,"line":345},12,[124,347,149],{"emptyLinePlaceholder":148},[124,349,351,354],{"class":126,"line":350},13,[124,352,353],{"class":130},"  return",[124,355,356],{"class":134}," (\n",[124,358,360],{"class":126,"line":359},14,[124,361,362],{"class":134},"    \u003C>\n",[124,364,366,369,372,374,377,380,383],{"class":126,"line":365},15,[124,367,368],{"class":134},"      {isFetching ",[124,370,371],{"class":130},"&&",[124,373,313],{"class":134},[124,375,376],{"class":174},"RefreshIndicator",[124,378,379],{"class":134}," \u002F>}  {",[124,381,382],{"class":286},"\u002F* background refresh *\u002F",[124,384,385],{"class":134},"}\n",[124,387,389,392,395,398,401,403,406,408,410,413,416,418,421,424,427,429],{"class":126,"line":388},16,[124,390,391],{"class":134},"      \u003C",[124,393,58],{"class":394},"s9eBZ",[124,396,397],{"class":134},">{data.",[124,399,400],{"class":158},"map",[124,402,238],{"class":134},[124,404,405],{"class":252},"todo",[124,407,256],{"class":130},[124,409,313],{"class":134},[124,411,412],{"class":174},"TodoItem",[124,414,415],{"class":158}," key",[124,417,202],{"class":130},[124,419,420],{"class":134},"{todo.id} {",[124,422,423],{"class":130},"...",[124,425,426],{"class":134},"todo} \u002F>)}\u003C\u002F",[124,428,58],{"class":394},[124,430,431],{"class":134},">\n",[124,433,435],{"class":126,"line":434},17,[124,436,437],{"class":134},"    \u003C\u002F>\n",[124,439,441],{"class":126,"line":440},18,[124,442,443],{"class":134},"  )\n",[124,445,447],{"class":126,"line":446},19,[124,448,385],{"class":134},[10,450,451],{},"Two flags that trip up developers:",[58,453,454,465],{},[61,455,456,460,461,464],{},[29,457,458],{},[14,459,181],{}," is ",[14,462,463],{},"true"," only when there is no cached data and a fetch is\nin progress. It is the \"blank slate\" state — show a skeleton.",[61,466,467,460,471,473],{},[29,468,469],{},[14,470,196],{},[14,472,463],{}," whenever any fetch is in progress, including\nbackground refetches. Use it to show a subtle \"refreshing\" indicator\nwithout blanking the already-rendered content.",[10,475,476,479],{},[29,477,478],{},"Background refetching"," is one of React Query's most valuable\nbehaviours. By default, every time a component mounts, the browser tab\nregains focus, or the network reconnects, React Query checks whether the\ncached data is stale and re-fetches it silently if so. Users always see\nup-to-date data without manual refresh buttons.",[10,481,482,483,486,487,489,490,493,494,489,496,499],{},"You control this via ",[14,484,485],{},"staleTime",". A ",[14,488,485],{}," of ",[14,491,492],{},"0"," (the default) means\ndata is immediately stale after fetching — a re-focus will always trigger a\nbackground refetch. A ",[14,495,485],{},[14,497,498],{},"Infinity"," means the data is treated\nas permanently fresh and React Query will never refetch it automatically.",[38,501,503],{"id":502},"query-keys-the-caches-primary-key","Query Keys — The Cache's Primary Key",[10,505,506],{},"Query keys are more important than they first appear. The cache is a\nkey-value store where each unique key maps to its own cached response. Get\nthe key design wrong and you will either miss re-fetches or hit the network\nmore than necessary.",[10,508,509,510,513],{},"Best practice is to use ",[29,511,512],{},"arrays"," and include every variable the fetcher\ndepends on:",[115,515,517],{"className":117,"code":516,"language":119,"meta":120,"style":120},"\u002F\u002F Bad — same key for every user, cache will collide\nuseQuery({ queryKey: ['user'], queryFn: () => fetchUser(userId) })\n\n\u002F\u002F Good — each userId gets its own cache slot\nuseQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId) })\n\n\u002F\u002F Good — filters are part of the identity\nuseQuery({\n  queryKey: ['todos', { status: 'active', page: 2 }],\n  queryFn: () => fetchTodos({ status: 'active', page: 2 }),\n})\n",[14,518,519,524,549,553,558,579,583,588,594,616,640],{"__ignoreMap":120},[124,520,521],{"class":126,"line":127},[124,522,523],{"class":286},"\u002F\u002F Bad — same key for every user, cache will collide\n",[124,525,526,528,531,534,537,539,541,543,546],{"class":126,"line":145},[124,527,104],{"class":158},[124,529,530],{"class":134},"({ queryKey: [",[124,532,533],{"class":141},"'user'",[124,535,536],{"class":134},"], ",[124,538,112],{"class":158},[124,540,229],{"class":134},[124,542,232],{"class":130},[124,544,545],{"class":158}," fetchUser",[124,547,548],{"class":134},"(userId) })\n",[124,550,551],{"class":126,"line":152},[124,552,149],{"emptyLinePlaceholder":148},[124,554,555],{"class":126,"line":165},[124,556,557],{"class":286},"\u002F\u002F Good — each userId gets its own cache slot\n",[124,559,560,562,564,566,569,571,573,575,577],{"class":126,"line":211},[124,561,104],{"class":158},[124,563,530],{"class":134},[124,565,533],{"class":141},[124,567,568],{"class":134},", userId], ",[124,570,112],{"class":158},[124,572,229],{"class":134},[124,574,232],{"class":130},[124,576,545],{"class":158},[124,578,548],{"class":134},[124,580,581],{"class":126,"line":223},[124,582,149],{"emptyLinePlaceholder":148},[124,584,585],{"class":126,"line":268},[124,586,587],{"class":286},"\u002F\u002F Good — filters are part of the identity\n",[124,589,590,592],{"class":126,"line":290},[124,591,104],{"class":158},[124,593,208],{"class":134},[124,595,596,599,601,604,607,610,613],{"class":126,"line":296},[124,597,598],{"class":134},"  queryKey: [",[124,600,217],{"class":141},[124,602,603],{"class":134},", { status: ",[124,605,606],{"class":141},"'active'",[124,608,609],{"class":134},", page: ",[124,611,612],{"class":174},"2",[124,614,615],{"class":134}," }],\n",[124,617,618,621,623,625,628,631,633,635,637],{"class":126,"line":301},[124,619,620],{"class":158},"  queryFn",[124,622,229],{"class":134},[124,624,232],{"class":130},[124,626,627],{"class":158}," fetchTodos",[124,629,630],{"class":134},"({ status: ",[124,632,606],{"class":141},[124,634,609],{"class":134},[124,636,612],{"class":174},[124,638,639],{"class":134}," }),\n",[124,641,642],{"class":126,"line":322},[124,643,644],{"class":134},"})\n",[10,646,647,648,651,652,655,656,658],{},"When a variable changes (the user navigates from ",[14,649,650],{},"userId=1"," to ",[14,653,654],{},"userId=2",")\nReact Query detects the key change and fires a new fetch automatically.\nThis replaces the ",[14,657,16],{}," dependency array pattern entirely for data\nfetching.",[10,660,661,662,665,666,669,670,672,673,178,676,679,680,683],{},"Keys also power ",[29,663,664],{},"targeted invalidation",". Because keys form a hierarchy,\n",[14,667,668],{},"queryClient.invalidateQueries({ queryKey: ['todos'] })"," marks every key\nthat starts with ",[14,671,217],{}," as stale — ",[14,674,675],{},"['todos']",[14,677,678],{},"['todos', 1]",",\n",[14,681,682],{},"['todos', { status: 'active' }]"," — all in one call.",[38,685,687],{"id":686},"usemutation-and-optimistic-updates","useMutation and Optimistic Updates",[10,689,690,693,694,696,697,700],{},[14,691,692],{},"useMutation"," handles write operations. Unlike ",[14,695,104],{}," it does not run\nautomatically — you call ",[14,698,699],{},"mutate()"," from an event handler:",[115,702,704],{"className":117,"code":703,"language":119,"meta":120,"style":120},"const queryClient = useQueryClient()\n\nconst createTodo = useMutation({\n  mutationFn: (text) =>\n    fetch('\u002Fapi\u002Ftodos', {\n      method: 'POST',\n      body: JSON.stringify({ text }),\n    }).then(r => r.json()),\n\n  onSuccess: () => {\n    \u002F\u002F Invalidate the list so it refetches with the new item\n    queryClient.invalidateQueries({ queryKey: ['todos'] })\n  },\n})\n\nfunction AddTodoForm() {\n  const [text, setText] = useState('')\n  return (\n    \u003Cform onSubmit={e => { e.preventDefault(); createTodo.mutate(text) }}>\n      \u003Cinput value={text} onChange={e => setText(e.target.value)} \u002F>\n      \u003Cbutton disabled={createTodo.isPending}>\n        {createTodo.isPending ? 'Saving…' : 'Add'}\n      \u003C\u002Fbutton>\n    \u003C\u002Fform>\n  )\n}\n",[14,705,706,723,727,741,758,770,780,797,816,820,832,837,852,857,861,865,874,904,910,946,979,995,1015,1025,1035,1040],{"__ignoreMap":120},[124,707,708,711,714,717,720],{"class":126,"line":127},[124,709,710],{"class":130},"const",[124,712,713],{"class":174}," queryClient",[124,715,716],{"class":130}," =",[124,718,719],{"class":158}," useQueryClient",[124,721,722],{"class":134},"()\n",[124,724,725],{"class":126,"line":145},[124,726,149],{"emptyLinePlaceholder":148},[124,728,729,731,734,736,739],{"class":126,"line":152},[124,730,710],{"class":130},[124,732,733],{"class":174}," createTodo",[124,735,716],{"class":130},[124,737,738],{"class":158}," useMutation",[124,740,208],{"class":134},[124,742,743,746,749,752,755],{"class":126,"line":165},[124,744,745],{"class":158},"  mutationFn",[124,747,748],{"class":134},": (",[124,750,751],{"class":252},"text",[124,753,754],{"class":134},") ",[124,756,757],{"class":130},"=>\n",[124,759,760,763,765,767],{"class":126,"line":211},[124,761,762],{"class":158},"    fetch",[124,764,238],{"class":134},[124,766,241],{"class":141},[124,768,769],{"class":134},", {\n",[124,771,772,775,778],{"class":126,"line":223},[124,773,774],{"class":134},"      method: ",[124,776,777],{"class":141},"'POST'",[124,779,679],{"class":134},[124,781,782,785,788,791,794],{"class":126,"line":268},[124,783,784],{"class":134},"      body: ",[124,786,787],{"class":174},"JSON",[124,789,790],{"class":134},".",[124,792,793],{"class":158},"stringify",[124,795,796],{"class":134},"({ text }),\n",[124,798,799,802,804,806,808,810,812,814],{"class":126,"line":290},[124,800,801],{"class":134},"    }).",[124,803,247],{"class":158},[124,805,238],{"class":134},[124,807,253],{"class":252},[124,809,256],{"class":130},[124,811,259],{"class":134},[124,813,262],{"class":158},[124,815,265],{"class":134},[124,817,818],{"class":126,"line":296},[124,819,149],{"emptyLinePlaceholder":148},[124,821,822,825,827,829],{"class":126,"line":301},[124,823,824],{"class":158},"  onSuccess",[124,826,229],{"class":134},[124,828,232],{"class":130},[124,830,831],{"class":134}," {\n",[124,833,834],{"class":126,"line":322},[124,835,836],{"class":286},"    \u002F\u002F Invalidate the list so it refetches with the new item\n",[124,838,839,842,845,847,849],{"class":126,"line":345},[124,840,841],{"class":134},"    queryClient.",[124,843,844],{"class":158},"invalidateQueries",[124,846,530],{"class":134},[124,848,217],{"class":141},[124,850,851],{"class":134},"] })\n",[124,853,854],{"class":126,"line":350},[124,855,856],{"class":134},"  },\n",[124,858,859],{"class":126,"line":359},[124,860,644],{"class":134},[124,862,863],{"class":126,"line":365},[124,864,149],{"emptyLinePlaceholder":148},[124,866,867,869,872],{"class":126,"line":388},[124,868,155],{"class":130},[124,870,871],{"class":158}," AddTodoForm",[124,873,162],{"class":134},[124,875,876,878,881,883,885,888,891,893,896,898,901],{"class":126,"line":434},[124,877,168],{"class":130},[124,879,880],{"class":134}," [",[124,882,751],{"class":174},[124,884,178],{"class":134},[124,886,887],{"class":174},"setText",[124,889,890],{"class":134},"] ",[124,892,202],{"class":130},[124,894,895],{"class":158}," useState",[124,897,238],{"class":134},[124,899,900],{"class":141},"''",[124,902,903],{"class":134},")\n",[124,905,906,908],{"class":126,"line":440},[124,907,353],{"class":130},[124,909,356],{"class":134},[124,911,912,915,918,921,923,926,929,931,934,937,940,943],{"class":126,"line":446},[124,913,914],{"class":134},"    \u003C",[124,916,917],{"class":394},"form",[124,919,920],{"class":158}," onSubmit",[124,922,202],{"class":130},[124,924,925],{"class":134},"{",[124,927,928],{"class":252},"e",[124,930,256],{"class":130},[124,932,933],{"class":134}," { e.",[124,935,936],{"class":158},"preventDefault",[124,938,939],{"class":134},"(); createTodo.",[124,941,942],{"class":158},"mutate",[124,944,945],{"class":134},"(text) }}>\n",[124,947,949,951,954,957,959,962,965,967,969,971,973,976],{"class":126,"line":948},20,[124,950,391],{"class":134},[124,952,953],{"class":394},"input",[124,955,956],{"class":158}," value",[124,958,202],{"class":130},[124,960,961],{"class":134},"{text} ",[124,963,964],{"class":158},"onChange",[124,966,202],{"class":130},[124,968,925],{"class":134},[124,970,928],{"class":252},[124,972,256],{"class":130},[124,974,975],{"class":158}," setText",[124,977,978],{"class":134},"(e.target.value)} \u002F>\n",[124,980,982,984,987,990,992],{"class":126,"line":981},21,[124,983,391],{"class":134},[124,985,986],{"class":394},"button",[124,988,989],{"class":158}," disabled",[124,991,202],{"class":130},[124,993,994],{"class":134},"{createTodo.isPending}>\n",[124,996,998,1001,1004,1007,1010,1013],{"class":126,"line":997},22,[124,999,1000],{"class":134},"        {createTodo.isPending ",[124,1002,1003],{"class":130},"?",[124,1005,1006],{"class":141}," 'Saving…'",[124,1008,1009],{"class":130}," :",[124,1011,1012],{"class":141}," 'Add'",[124,1014,385],{"class":134},[124,1016,1018,1021,1023],{"class":126,"line":1017},23,[124,1019,1020],{"class":134},"      \u003C\u002F",[124,1022,986],{"class":394},[124,1024,431],{"class":134},[124,1026,1028,1031,1033],{"class":126,"line":1027},24,[124,1029,1030],{"class":134},"    \u003C\u002F",[124,1032,917],{"class":394},[124,1034,431],{"class":134},[124,1036,1038],{"class":126,"line":1037},25,[124,1039,443],{"class":134},[124,1041,1043],{"class":126,"line":1042},26,[124,1044,385],{"class":134},[10,1046,1047,1048,1051],{},"For high-frequency interactions — toggling a like, reordering a list —\nwaiting for the server response before updating the UI feels sluggish.\n",[29,1049,1050],{},"Optimistic updates"," fix this by applying the expected change to the\ncache immediately and rolling back on failure:",[115,1053,1055],{"className":117,"code":1054,"language":119,"meta":120,"style":120},"const toggleLike = useMutation({\n  mutationFn: (postId) => fetch(`\u002Fapi\u002Fposts\u002F${postId}\u002Flike`, { method: 'POST' }),\n\n  onMutate: async (postId) => {\n    await queryClient.cancelQueries({ queryKey: ['posts'] }) \u002F\u002F stop in-flight refetches\n    const previous = queryClient.getQueryData(['posts'])     \u002F\u002F snapshot\n\n    queryClient.setQueryData(['posts'], (old) =>             \u002F\u002F apply optimistically\n      old.map(p => p.id === postId ? { ...p, liked: !p.liked } : p)\n    )\n\n    return { previous }\n  },\n\n  onError: (_err, _postId, context) => {\n    queryClient.setQueryData(['posts'], context.previous)    \u002F\u002F roll back\n  },\n\n  onSettled: () => {\n    queryClient.invalidateQueries({ queryKey: ['posts'] })   \u002F\u002F sync with server\n  },\n})\n",[14,1056,1057,1070,1102,1106,1128,1150,1176,1180,1204,1246,1251,1255,1263,1267,1271,1297,1313,1317,1321,1332,1348,1352],{"__ignoreMap":120},[124,1058,1059,1061,1064,1066,1068],{"class":126,"line":127},[124,1060,710],{"class":130},[124,1062,1063],{"class":174}," toggleLike",[124,1065,716],{"class":130},[124,1067,738],{"class":158},[124,1069,208],{"class":134},[124,1071,1072,1074,1076,1079,1081,1083,1085,1087,1090,1092,1095,1098,1100],{"class":126,"line":145},[124,1073,745],{"class":158},[124,1075,748],{"class":134},[124,1077,1078],{"class":252},"postId",[124,1080,754],{"class":134},[124,1082,232],{"class":130},[124,1084,235],{"class":158},[124,1086,238],{"class":134},[124,1088,1089],{"class":141},"`\u002Fapi\u002Fposts\u002F${",[124,1091,1078],{"class":134},[124,1093,1094],{"class":141},"}\u002Flike`",[124,1096,1097],{"class":134},", { method: ",[124,1099,777],{"class":141},[124,1101,639],{"class":134},[124,1103,1104],{"class":126,"line":152},[124,1105,149],{"emptyLinePlaceholder":148},[124,1107,1108,1111,1114,1117,1120,1122,1124,1126],{"class":126,"line":165},[124,1109,1110],{"class":158},"  onMutate",[124,1112,1113],{"class":134},": ",[124,1115,1116],{"class":130},"async",[124,1118,1119],{"class":134}," (",[124,1121,1078],{"class":252},[124,1123,754],{"class":134},[124,1125,232],{"class":130},[124,1127,831],{"class":134},[124,1129,1130,1133,1136,1139,1141,1144,1147],{"class":126,"line":211},[124,1131,1132],{"class":130},"    await",[124,1134,1135],{"class":134}," queryClient.",[124,1137,1138],{"class":158},"cancelQueries",[124,1140,530],{"class":134},[124,1142,1143],{"class":141},"'posts'",[124,1145,1146],{"class":134},"] }) ",[124,1148,1149],{"class":286},"\u002F\u002F stop in-flight refetches\n",[124,1151,1152,1155,1158,1160,1162,1165,1168,1170,1173],{"class":126,"line":223},[124,1153,1154],{"class":130},"    const",[124,1156,1157],{"class":174}," previous",[124,1159,716],{"class":130},[124,1161,1135],{"class":134},[124,1163,1164],{"class":158},"getQueryData",[124,1166,1167],{"class":134},"([",[124,1169,1143],{"class":141},[124,1171,1172],{"class":134},"])     ",[124,1174,1175],{"class":286},"\u002F\u002F snapshot\n",[124,1177,1178],{"class":126,"line":268},[124,1179,149],{"emptyLinePlaceholder":148},[124,1181,1182,1184,1187,1189,1191,1194,1197,1199,1201],{"class":126,"line":290},[124,1183,841],{"class":134},[124,1185,1186],{"class":158},"setQueryData",[124,1188,1167],{"class":134},[124,1190,1143],{"class":141},[124,1192,1193],{"class":134},"], (",[124,1195,1196],{"class":252},"old",[124,1198,754],{"class":134},[124,1200,232],{"class":130},[124,1202,1203],{"class":286},"             \u002F\u002F apply optimistically\n",[124,1205,1206,1209,1211,1213,1215,1217,1220,1223,1226,1228,1230,1232,1235,1238,1241,1243],{"class":126,"line":296},[124,1207,1208],{"class":134},"      old.",[124,1210,400],{"class":158},[124,1212,238],{"class":134},[124,1214,10],{"class":252},[124,1216,256],{"class":130},[124,1218,1219],{"class":134}," p.id ",[124,1221,1222],{"class":130},"===",[124,1224,1225],{"class":134}," postId ",[124,1227,1003],{"class":130},[124,1229,171],{"class":134},[124,1231,423],{"class":130},[124,1233,1234],{"class":134},"p, liked: ",[124,1236,1237],{"class":130},"!",[124,1239,1240],{"class":134},"p.liked } ",[124,1242,113],{"class":130},[124,1244,1245],{"class":134}," p)\n",[124,1247,1248],{"class":126,"line":301},[124,1249,1250],{"class":134},"    )\n",[124,1252,1253],{"class":126,"line":322},[124,1254,149],{"emptyLinePlaceholder":148},[124,1256,1257,1260],{"class":126,"line":345},[124,1258,1259],{"class":130},"    return",[124,1261,1262],{"class":134}," { previous }\n",[124,1264,1265],{"class":126,"line":350},[124,1266,856],{"class":134},[124,1268,1269],{"class":126,"line":359},[124,1270,149],{"emptyLinePlaceholder":148},[124,1272,1273,1276,1278,1281,1283,1286,1288,1291,1293,1295],{"class":126,"line":365},[124,1274,1275],{"class":158},"  onError",[124,1277,748],{"class":134},[124,1279,1280],{"class":252},"_err",[124,1282,178],{"class":134},[124,1284,1285],{"class":252},"_postId",[124,1287,178],{"class":134},[124,1289,1290],{"class":252},"context",[124,1292,754],{"class":134},[124,1294,232],{"class":130},[124,1296,831],{"class":134},[124,1298,1299,1301,1303,1305,1307,1310],{"class":126,"line":388},[124,1300,841],{"class":134},[124,1302,1186],{"class":158},[124,1304,1167],{"class":134},[124,1306,1143],{"class":141},[124,1308,1309],{"class":134},"], context.previous)    ",[124,1311,1312],{"class":286},"\u002F\u002F roll back\n",[124,1314,1315],{"class":126,"line":434},[124,1316,856],{"class":134},[124,1318,1319],{"class":126,"line":440},[124,1320,149],{"emptyLinePlaceholder":148},[124,1322,1323,1326,1328,1330],{"class":126,"line":446},[124,1324,1325],{"class":158},"  onSettled",[124,1327,229],{"class":134},[124,1329,232],{"class":130},[124,1331,831],{"class":134},[124,1333,1334,1336,1338,1340,1342,1345],{"class":126,"line":948},[124,1335,841],{"class":134},[124,1337,844],{"class":158},[124,1339,530],{"class":134},[124,1341,1143],{"class":141},[124,1343,1344],{"class":134},"] })   ",[124,1346,1347],{"class":286},"\u002F\u002F sync with server\n",[124,1349,1350],{"class":126,"line":981},[124,1351,856],{"class":134},[124,1353,1354],{"class":126,"line":997},[124,1355,644],{"class":134},[10,1357,1358,1359,1362,1363,1365],{},"The five-step pattern — ",[29,1360,1361],{},"cancel → snapshot → apply → rollback on error →\ninvalidate on settle"," — is the canonical approach. Every step matters;\nskipping ",[14,1364,1138],{}," can cause a race where a background refetch\noverwrites your optimistic update.",[38,1367,1369],{"id":1368},"react-query-vs-redux-for-server-state","React Query vs Redux for Server State",[10,1371,1372],{},"This comparison comes up in almost every senior React interview. The short\nanswer: they solve different problems.",[10,1374,1375,1378,1379,1382],{},[29,1376,1377],{},"Redux"," is a global synchronous state container. It is excellent for\nclient state that is shared across many parts of the app, complex state\nmachines, or undo\u002Fredo functionality. The canonical Redux data-fetching\nsolution is ",[29,1380,1381],{},"RTK Query"," (built into Redux Toolkit), which is essentially\nRedux + a React-Query-like caching layer.",[10,1384,1385,1388],{},[29,1386,1387],{},"React Query"," is purpose-built for server state. It is not a general\nstate manager — you cannot put a modal's open\u002Fclosed flag into it. What it\ndoes better than raw Redux for fetching:",[115,1390,1394],{"className":1391,"code":1393,"language":751},[1392],"language-text","                     React Query     Redux (RTK Query)\n─────────────────────────────────────────────────────\nSetup boilerplate       minimal         moderate\nAutomatic refetching    yes             yes (RTK Query)\nOptimistic updates      built-in        built-in (RTK)\nDevtools                excellent       excellent\nClient state            no              yes\nCross-slice derived     no              yes (selectors)\nLearning curve          low             moderate–high\nBundle size             ~13 kB          ~10 kB (RTK)\n",[14,1395,1393],{"__ignoreMap":120},[10,1397,1398,1399,1402],{},"The practical guidance: ",[29,1400,1401],{},"start with React Query for server state",". Only\nadd Redux if you genuinely need global synchronous client state (cross-cutting\nstate machines, collaborative features, complex undo). Many production apps\nsuccessfully combine React Query for server state with Zustand or React\ncontext for client state, never needing Redux at all.",[38,1404,1406],{"id":1405},"what-seniors-know-that-juniors-miss","What Seniors Know That Juniors Miss",[10,1408,1409],{},"Interviewers testing React Query knowledge look for understanding beyond\nthe basics:",[10,1411,1412,1417,1418,1420,1421,1423,1424,1427],{},[29,1413,1414,1416],{},[14,1415,485],{}," is the most impactful tuning knob."," The default ",[14,1419,485],{},"\nof ",[14,1422,492],{}," means every component mount triggers a background refetch. For\nreference data (countries list, configuration) setting ",[14,1425,1426],{},"staleTime: Infinity","\neliminates redundant network requests entirely.",[10,1429,1430,1433,1434,1436],{},[29,1431,1432],{},"Query keys encode dependencies."," If your ",[14,1435,112],{}," closes over a\nvariable but that variable is not in the key, React Query will never\nre-fetch when the variable changes. Treat the key as the exhaustive list of\neverything the fetcher depends on.",[10,1438,1439,1442,1443,1446],{},[29,1440,1441],{},"Devtools are essential, not optional."," The ",[14,1444,1445],{},"ReactQueryDevtools"," panel\nshows every cached entry, its status, staleness, and observer count. You\ncannot debug cache invalidation issues without it.",[10,1448,1449,1455,1456,679,1458,1461,1462,1465,1466,1468,1469,1471,1472,1474,1475,1477,1478,1480,1481,1484],{},[29,1450,1451,1454],{},[14,1452,1453],{},"onSuccess"," was removed in v5."," In React Query v5, ",[14,1457,1453],{},[14,1459,1460],{},"onError",", and ",[14,1463,1464],{},"onSettled"," callbacks were removed from ",[14,1467,104],{}," (they\nremain on ",[14,1470,692],{},"). Side effects after a query should live in\n",[14,1473,16],{}," watching ",[14,1476,175],{}," and ",[14,1479,191],{},", or in the ",[14,1482,1483],{},"QueryCache"," global\ncallbacks.",[10,1486,1487,1490,1491,1494,1495,1498,1499,1502,1503,1506],{},[29,1488,1489],{},"Infinite queries need flat data for rendering."," ",[14,1492,1493],{},"useInfiniteQuery"," stores\ndata as ",[14,1496,1497],{},"{ pages: [...] }"," — a list of page responses. Always flatten with\n",[14,1500,1501],{},"data.pages.flatMap(p => p.items)"," before rendering; mapping over\n",[14,1504,1505],{},"data.pages"," directly will render page objects, not individual items.",[10,1508,1509,1510,1512],{},"The shift from \"I manage my own fetch lifecycle\" to \"I declare what data I\nneed and React Query keeps it fresh\" is the mental model senior engineers\nhave internalised. Once you think in queries, mutations, and cache\ninvalidation rather than loading flags and ",[14,1511,16],{}," chains, you write\nless code, catch more edge cases, and end up with a significantly more\nmaintainable codebase.",[1514,1515,1516],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":120,"searchDepth":145,"depth":145,"links":1518},[1519,1520,1521,1522,1523,1524],{"id":40,"depth":145,"text":41},{"id":98,"depth":145,"text":99},{"id":502,"depth":145,"text":503},{"id":686,"depth":145,"text":687},{"id":1368,"depth":145,"text":1369},{"id":1405,"depth":145,"text":1406},"Master React Query for interviews — useQuery, useMutation, caching, query keys, optimistic updates, and why server state deserves its own management layer.","medium","md","React","react",{},"\u002Fblog\u002Freact-async-state-react-query-guide","\u002Freact\u002Fstate-management\u002Fasync-state-react-query",{"title":5,"description":1525},"blog\u002Freact-async-state-react-query-guide","Async State & React Query","State Management","state-management","2026-06-24","J5aRPGgbJjLO_8Br7ETm0OpZbv4gXZNXvWXBDJAuTug",1782244083254]