npx skills add ...
npx skills add automattic/wp-calypso --skill calypso-react-query-migration
Use when editing or migrating Calypso Reader data-fetching code — `client/components/data/query-reader-*` components, `@automattic/data-stores` Reader hooks, or new Reader queries/mutations. Triggers on any work involving `api-core`, `api-queries`, `dispatchRequest`, `READER_*_REQUEST` actions, or Redux `isRequesting*` selectors in the Reader.
npx skills add automattic/wp-calypso --skill calypso-react-query-migration
Target architecture: fetchers in @automattic/api-core → query/mutation options in @automattic/api-queries → components call useQuery() / useMutation() directly.
Two source patterns migrate into it:
dispatchRequest + http actions in client/state/data-layer/wpcom/read/)@automattic/data-stores Reader hooks (custom hooks using wpcomRequest)| What | Where |
|---|---|
| API fetcher | packages/api-core/src/read-{name}/fetchers.ts |
| Mutators | packages/api-core/src/read-{name}/mutators.ts |
| Response types | packages/api-core/src/read-{name}/types.ts |
| Barrel | packages/api-core/src/read-{name}/index.ts + add to packages/api-core/src/index.ts |
| Query/mutation options | packages/api-queries/src/read-{name}.ts + add to packages/api-queries/src/index.ts |
| Bridge component (if kept) | client/components/data/query-reader-{name}/index.tsx |
| Component tests | client/components/data/query-reader-{name}/test/index.test.tsx |
| Redux to remove | client/state/data-layer/wpcom/read/{name}/index.js, client/state/reader/{name}/{actions,reducer}.{ts,js}, client/state/reader/action-types.ts |
Every Reader fetcher / query / mutation / response type carries the Read prefix — the rest of api-core and api-queries does this consistently and breaking the pattern stands out in code review.
| Kind | Pattern | Examples |
|---|---|---|
| Fetcher | fetchRead{Name} | fetchReadFeed, fetchReadSubscriptionDetails |
| Query factory | read{Name}Query | readFeedQuery, readSubscriptionDetailsQuery |
| Mutation factory | {verb}Read{Name}Mutation | addReadListFeedMutation, unfollowReadTagMutation |
| Response types | Read{Name}Response, Read{Name}ErrorResponse | ReadFeedSearchResponse, ReadSubscriptionDetailsResponse |
| Args/params types | Read{Name}Args, FetchRead{Name}Params | ReadSubscriptionDetailsArgs |
Always plan before editing. A one-line request like "migrate QueryReaderTag" is a spec — turn it into a plan first.
REQUIRED SUB-SKILLS:
superpowers:writing-plans — to draft the plansuperpowers:executing-plans — to execute it with checkpointsThe plan must include: full CRUD audit, bridge decision (with reason), commit split, per-mutation invalidations + optimistic-update decision, list of consumers to update, test plan.
Save the plan to .context/plan-migrate-reader-{name}.md.
List every action for the resource — read AND mutations. Partial migrations create two sources of truth (deleted item still shows in sidebar until refresh).
| Operation | Redux pattern | Target |
|---|---|---|
| Read | READER_XXX_REQUEST → dispatchRequest GET | useQuery(readXxxQuery(...)) |
| Create / Update / Delete | READER_XXX_* → dispatchRequest POST | useMutation(...Mutation()) |
| Follow / Unfollow | READER_XXX_FOLLOW etc. | dedicated mutation each |
Plan one commit per action group; each commit removes its Redux counterpart (mutations don't need a bridge).
api-core fetchersDon't duplicate. Grep before creating:
If a fetcher exists: reuse (import from @automattic/api-core) or extend (add to existing folder, not a parallel one).
Before changing or replacing a query key, find every other piece of code reading or writing it. Mutations and shared helpers commonly do optimistic setQueryData / invalidateQueries against the old key — change the key without updating them and they silently no-op, leaving the user looking at stale data after every action.
For every match, decide:
Document the decision per match in the plan. The trap: when the only consumer of the old key is a mutation's optimistic write, deleting the data-stores hook looks clean — grep for the hook name finds nothing — but the mutation still operates on a phantom cache that no live query reads, and the visible page never updates after the user acts.
The QueryReader* bridge dispatches RECEIVE so the rest of Calypso keeps reading from Redux. It's not the long-term target. Evaluate removing it in this migration before defaulting to keeping it.
| Audit result | Recommended decision |
|---|---|
| 1–3 function-component consumers, slice not read elsewhere | Remove bridge. Adapt consumers to useQuery. Delete RECEIVE action, reducer, selectors. |
| Class component is the main consumer | Wrap class with HOC that calls useQuery() and forwards props. Delete bridge + slice. (See redux-cleanup.md for the HOC pattern.) |
| Many consumers, or selectors used cross-codebase, or cross-Reader code reads slice | Keep bridge. Record reason in plan; note as follow-up. |
Record the decision verbatim in the plan. When removing the bridge, grep to confirm nothing else reads RECEIVE/reducer/selectors before deleting them.
api-core)Find method, path, apiVersion, and query/body in the existing data-layer http() call. Copy exactly. Always import wpcom from ../wpcom-fetcher (not directly).
Add types in types.ts, barrel-export from index.ts, then export the module from packages/api-core/src/index.ts.
Some Reader endpoints (/read/sites/{blogId}/subscription-details, /read/subscriptions/{id}, anything reachable from the public /subscriptions/... landing pages) accept logged-out callers via an X-WPSUBKEY header. The legacy data-stores callApi helper handles this; wpcom.req.get does not, because wpcom-proxy-request requires a session.
Trigger: the source hook calls callApi with an isLoggedIn arg, or imports getSubkey, or the consumer renders under a /subscriptions/... route.
Before designing the fork, confirm with backend:
X-WPSUBKEY?window.currentUser.subscriptionManagementSubkey populated? (Today: only the logged-out subscriptions bootstrap.)Pattern when both auth modes are accepted and cookie wins server-side — the bifurcation becomes pure transport, not URL or auth strategy:
Use raw fetch rather than @wordpress/api-fetch — avoids adding the dep to api-core and the subkey path needs none of apiFetch's middleware. The client/lib/request-with-subkey-fallback/ helper does the same thing for non-Reader code, but don't import it from api-core (wrong direction); inline the logic.
api-queries)queryKey: ['read', '{domain}', ...params]. staleTime: ~1min for data with external change events (payments, renewals, server-side mutations), ~5min for slowly-changing lists. Confirm change-rate with backend when unsure. enabled: when params can be null.
When migrating from data-stores, preserve the same queryKey for cache compatibility mid-session.
If the legacy code dispatched success/failure actions, mirror both via isSuccess / isError effects.
See test-scaffolding.md for the full template (createTestStore, renderWithProviders, nock setup).
nock URL: https://public-api.wordpress.com/rest/v{apiVersion}/{path} for wpcom.req.get calls, https://public-api.wordpress.com/wpcom/v2{path} for apiNamespace: 'wpcom/v2' v2 calls. Run with yarn test-client client/components/data/query-reader-{name}/test/.
See mutations.md — covers mutators, mutationOptions, cache invalidation rules, optimistic updates (default for user-facing mutations), and where side effects live (consumer's onSuccess, not api-queries).
data-storesSame fetcher/query pattern, with two differences:
After updating consumers, delete the hook from packages/data-stores/src/reader/queries/ and remove its export from the Reader barrel.
Read redux-cleanup.md for: per-action checklist, replacing isRequesting* selectors with React Query state, function-component connect() → hooks conversion, and the class-component HOC pattern.
Cleanup is a separate commit from the migration commit — easier to review and revert.
If you catch yourself doing any of these, stop and reconsider:
api-core, api-queries, client/components/data/query-reader-*, or Redux Reader state without a written plan → write the plan first.packages/api-core/src/read-{name}/ without grepping for an existing module → check api-core first.isRequesting* selector or RECEIVE reducer without grepping for consumers → grep first.invalidateQueries and no onMutate for a user-facing action → consider optimistic updates.onSuccess in api-queries → side effects belong in the consumer's onSuccess.queryKey when migrating from data-stores → preserve it for mid-session cache compatibility.| Mistake | Fix |
|---|---|
Forgetting to dispatch RECEIVE from the bridge | Other code still reads from Redux — bridge is required when kept |
Wrong nock URL (missing /rest/v{N}/) | Always https://public-api.wordpress.com/rest/v{apiVersion}/{path} |
| Removing unrelated handlers from data-layer file | Surgical removal — only the specific REQUEST handler |
Missing enabled when query has required params | Query fires with undefined and fails |
| Cleanup in same commit as migration | Keep separate for reviewability |
.jsx for a new file | Always .tsx |
| Forgetting barrel exports | Update api-core/src/index.ts AND api-queries/src/index.ts |
Importing wpcom directly | Always from ../wpcom-fetcher |
invalidateQueries for a deleted item's detail query | Use removeQueries; invalidateQueries for list |
Forgetting cancelQueries before optimistic write | An in-flight refetch can overwrite your optimistic value |
Skipping onSettled invalidation in optimistic mutation | Optimistic value drifts from server truth |
Leaving connect() HOC after request state moved to React Query | Convert to hooks, remove HOC entirely (function components only) |
Leaving the old data-stores hook exported | Delete file AND remove from Reader barrel |
Naming the fetcher / query without the Read prefix | Match other modules: fetchRead{Name}, read{Name}Query, Read{Name}Response |
Importing client/lib/request-with-subkey-fallback/ from api-core | Wrong dependency direction — inline the subkey + X-WPSUBKEY logic in the fetcher |