frontend-query-mutation

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

Guide for implementing Langflow frontend query and mutation patterns with Axios and TanStack React Query v5. Trigger when creating or updating API hooks in controllers/API/queries, consuming UseRequestProcessor in components, deciding whether to use useQuery or useMutation, handling conditional queries, cache invalidation, mutation error handling, or migrating legacy API calls to the query hook pattern.

.agents/skills/frontend-query-mutation/SKILL.md

Download bundle ↓
main · 595cd723 bundle filesScanned 2026-09-15

references/query-patterns.md

3,196 tokens · o200k_base · 13,223 bytes

Source excerpt starting at line 1.
# Query and Mutation Patterns ## Table of Contents - Intent- Directory structure- Query hook structure- Mutation hook structure- URL constants- Query keys- Type signatures- Anti-patterns ## Intent - Keep API hooks in `controllers/API/queries/{domain}/` organized by domain.- Use `UseRequestProcessor` for consistent behavior across all hooks.- Use the shared `api` Axios instance for all REST calls.- Type hooks with `useQueryFunctionType` or `useMutationFunctionType` from `types/api/`. ## Directory Structure ```textcontrollers/API/  api.tsx                          # Axios instance, interceptors, streaming  helpers/    constants.ts                   # URL constants and getURL()  services/    request-processor.ts           # UseRequestProcessor hook  queries/    flows/      use-get-flow.ts      use-get-refresh-flows-query.ts      use-post-add-flow.ts      use-delete-delete-flows.ts      use-get-download-flows.ts      index.ts    folders/      use-get-folder.ts      use-get-folders.ts      use-post-folders.ts      use-patch-folders.ts      use-delete-folders.ts      index.ts    variables/      use-get-global-variables.ts      use-post-global-variables.ts      use-patch-global-variables.ts      index.ts    auth/      use-get-autologin.ts      use-post-login-user.ts      use-post-logout.ts      use-post-refresh-access.ts      index.ts    messages/      use-get-messages.ts      use-get-messages-polling.ts      use-delete-messages.ts      index.ts``` ### Naming Convention - **Queries**: `use-get-{resource}.ts` (e.g., `use-get-flow.ts`, `use-get-global-variables.ts`)- **Create mutations**: `use-post-{resource}.ts` (e.g., `use-post-add-flow.ts`)- **Update mutations**: `use-patch-{resource}.ts` (e.g., `use-patch-global-variables.ts`)- **Delete mutations**: `use-delete-{resource}.ts` (e.g., `use-delete-messages.ts`)- **Other mutations**: `use-{action}-{resource}.ts` (e.g., `use-rename-session.ts`) ## Query Hook Structure Every query hook follows this structure: ```typescriptimport type { UseQueryResult } from "@tanstack/react-query"import type { useQueryFunctionType } from "@/types/api"import { api } from "../../api"import { getURL } from "../../helpers/constants"import { UseRequestProcessor } from "../../services/request-processor" // 1. Define the response type (or import from types/)interface GlobalVariable {  id: string  name: string  value: string  type: string} // 2. Export the hook typed with useQueryFunctionTypeexport const useGetGlobalVariables: useQueryFunctionType<  undefined,  GlobalVariable[]> = (options?) => {  // 3. Get query helper from UseRequestProcessor  const { query } = UseRequestProcessor()   // 4. Define the query function using the api Axios instance  const getGlobalVariablesFn = async (): Promise<GlobalVariable[]> => {    const res = await api.get(`${getURL("VARIABLES")}/`)    return res.data  }   // 5. Return the query result  const queryResult: UseQueryResult<GlobalVariable[], Error> = query(    ["useGetGlobalVariables"],    getGlobalVariablesFn,    {      refetchOnWindowFocus: false,      ...options,    },  )   return queryResult}``` ### Query Hook with Parameters When a query takes parameters, use the first type argument: ```typescriptexport const useGetFolder: useQueryFunctionType<  { id: string },  FolderResponse> = (params, options?) => {  const { query } = UseRequestProcessor()   const getFolderFn = async (): Promise<FolderResponse> => {    const res = await api.get(`${getURL("FOLDERS")}/${params.id}`)    return res.data  }   return query(["useGetFolder", params.id], getFolderFn, {    ...options,  })}``` ### Important: Some "get" Hooks Use Mutation In the Langflow codebase, some hooks named `use-get-*` (e.g., `useGetFlow`) are actually **mutation hooks** typed with `useMutationFunctionType`. This happens when the "get" operation is triggered imperatively (on demand) rather than declaratively (on mount/re-render). Check the actual type signature before following the query pattern — if a hook uses `mutate` from `UseRequestProcessor`, follow the Mutation Hook Structure below instead. ### Query Hook with Store Updates Many Langflow queries update Zustand stores as a side effect within the query function: ```typescriptexport const useGetGlobalVariables: useQueryFunctionType<  undefined,  GlobalVariable[]> = (options?) => {  const { query } = UseRequestProcessor()   // Access store setters  const setGlobalVariablesEntries = useGlobalVariablesStore(    (state) => state.setGlobalVariablesEntries,  )   const getGlobalVariablesFn = async (): Promise<GlobalVariable[]> => {    const res = await api.get(`${getURL("VARIABLES")}/`)    // Update store as side effect of fetching    setGlobalVariablesEntries(res.data.map((entry) => entry.name))    return res.data  }   return query(["useGetGlobalVariables"], getGlobalVariablesFn, {    refetchOnWindowFocus: false,    ...options,  })}``` ## Mutation Hook Structure Every mutation hook follows this structure: ```typescriptimport type { UseMutationResult } from "@tanstack/react-query"import type { useMutationFunctionType } from "@/types/api"import { api } from "../../api"import { getURL } from "../../helpers/constants"import { UseRequestProcessor } from "../../services/request-processor" // 1. Define the payload typeinterface PostAddFlowPayload {  name: string  data: ReactFlowJsonObject  description: string  folder_id: string} // 2. Export the hook typed with useMutationFunctionTypeexport const usePostAddFlow: useMutationFunctionType<  undefined,       // Params (undefined if none)  PostAddFlowPayload  // Variables (mutation payload)> = (options?) => {  // 3. Get mutate helper and queryClient from UseRequestProcessor  const { mutate, queryClient } = UseRequestProcessor()   // 4. Define the mutation function using the api Axios instance  const postAddFlowFn = async (payload: PostAddFlowPayload): Promise<any> => {    const response = await api.post(`${getURL("FLOWS")}/`, {      name: payload.name,      data: payload.data,      description: payload.description,      folder_id: payload.folder_id || null,    })    return response.data  }   // 5. Return the mutation with cache invalidation in onSettled  // NOTE: Place hook-specific options (onSettled, retry) BEFORE ...options  // so consumers can override them if needed. This matches the codebase convention.  const mutation: UseMutationResult<any, any, PostAddFlowPayload> = mutate(    ["usePostAddFlow"],    postAddFlowFn,    {      onSettled: (response) => {        if (response) {          // Invalidate related queries so they refetch          queryClient.refetchQueries({            queryKey: ["useGetRefreshFlowsQuery", { get_all: true, header_flows: true }],          })          queryClient.refetchQueries({            queryKey: ["useGetFolder", response.folder_id],          })        }      },      ...options,  // Consumer options come LAST (can override onSettled, retry, etc.)    },  )   return mutation}``` ### Delete Mutation Example ```typescriptexport const useDeleteMessages: useMutationFunctionType<undefined, string[]> = (  options?,) => {  const { mutate, queryClient } = UseRequestProcessor()   const deleteMessagesFn = async (messageIds: string[]): Promise<void> => {    await api.delete(`${getURL("MESSAGES")}/`, {      data: messageIds,    })  }   const mutation = mutate(["useDeleteMessages"], deleteMessagesFn, {    onSettled: () => {      queryClient.invalidateQueries({ queryKey: ["useGetMessages"] })    },    ...options,  })   return mutation}``` ### Patch Mutation Example ```typescriptexport const usePatchGlobalVariables: useMutationFunctionType<  undefined,  { id: string; name: string; value: string; type: string }> = (options?) => {  const { mutate, queryClient } = UseRequestProcessor()   const patchGlobalVariablesFn = async (payload: {    id: string    name: string    value: string    type: string  }): Promise<GlobalVariable> => {    const response = await api.patch(      `${getURL("VARIABLES")}/${payload.id}`,      payload,    )    return response.data  }   const mutation = mutate(["usePatchGlobalVariables"], patchGlobalVariablesFn, {    onSettled: () => {      queryClient.invalidateQueries({ queryKey: ["useGetGlobalVariables"] })    },    retry: false,    ...options,  })   return mutation}``` ## URL Constants All API paths are defined in `controllers/API/helpers/constants.ts`: ```typescriptexport const URLs = {  FLOWS: "flows",  FOLDERS: "projects",  VARIABLES: "variables",  MESSAGES: "monitor/messages",  BUILDS: "monitor/builds",  API_KEY: "api_key",  FILES: "files",  // ...} as const``` Use `getURL()` to construct full API paths: ```typescriptimport { getURL } from "../../helpers/constants" // Basic: /api/v1/flowsconst url = getURL("FLOWS") // With params: /api/v1/flows/{flowId}const url = `${getURL("FLOWS")}/${flowId}` // With v2 flag: /api/v2/flowsconst url = getURL("FLOWS", {}, true)``` When adding new endpoints, add the constant to `URLs` first, then use `getURL()` in the hook. ## Query Keys Query keys are arrays that uniquely identify cached data. Langflow conventions: ```typescript// Hook name as key (no params)["useGetGlobalVariables"] // Hook name + params for cache isolation["useGetFlow", flowId]["useGetFolder", folderId] // Hook name + object params for complex queries["useGetRefreshFlowsQuery", { get_all: true, header_flows: true }]["useGetMessages", { flowId, sessionId, page }]``` Rules:- The first element is always the hook name as a string.- Additional elements are parameters that differentiate cache entries.- Use objects for multi-parameter queries.- Keep keys consistent so invalidation works correctly. ## Type Signatures ### useQueryFunctionType Used for query hooks. Defined in `types/api/index.ts`: ```typescript// Without params: useQueryFunctionType<undefined, ResponseType>// The hook signature becomes: (options?) => UseQueryResult<ResponseType> // With params: useQueryFunctionType<ParamsType, ResponseType>// The hook signature becomes: (params, options?) => UseQueryResult<ResponseType>``` ### useMutationFunctionType Used for mutation hooks: ```typescript// Without params: useMutationFunctionType<undefined, VariablesType>// The hook signature becomes: (options?) => UseMutationResult<Data, Error, Variables> // With params: useMutationFunctionType<ParamsType, VariablesType>// The hook signature becomes: (params, options?) => UseMutationResult<Data, Error, Variables>``` ## Anti-Patterns ### Do Not Bypass UseRequestProcessor ```typescript// Do not call useQuery/useMutation directlyconst result = useQuery({  queryKey: ["flows"],  queryFn: () => api.get("/api/v1/flows"),}) // Use UseRequestProcessor for consistent retry and error handlingconst { query } = UseRequestProcessor()const result = query(["useGetFlows"], () => api.get(getURL("FLOWS")))``` ### Do Not Bypass the api Instance ```typescript// Do not use raw axios or fetch for REST callsconst result = await axios.get("/api/v1/flows")const result = await fetch("/api/v1/flows") // Use the shared api instance (has interceptors for auth, headers, error handling)const result = await api.get(`${getURL("FLOWS")}/`)``` ### Do Not Hardcode URLs ```typescript// Do not hardcode API pathsconst result = await api.get("/api/v1/variables/") // Use getURL() helperconst result = await api.get(`${getURL("VARIABLES")}/`)``` ### Do Not Duplicate Query Keys ```typescript// Do not use different key strings for the same queryquery(["getFlows"], fetchFn)       // in hook Aquery(["useGetFlows"], fetchFn)    // in hook Bquery(["flows-list"], fetchFn)     // in hook C // Use one canonical key matching the hook namequery(["useGetFlows"], fetchFn)``` ### Do Not Invalidate from Components ```typescript// Do not put invalidation logic in componentsconst Component = () => {  const queryClient = useQueryClient()  const { mutate } = useDeleteFlow()   const handleDelete = () => {    mutate(flowId, {      onSuccess: () => {        // Avoid: invalidation knowledge leaks into component        queryClient.invalidateQueries({ queryKey: ["useGetFlows"] })        queryClient.invalidateQueries({ queryKey: ["useGetFolder"] })      },    })  }} // Put invalidation in the mutation hook's onSettledexport const useDeleteFlow = (options?) => {  const { mutate, queryClient } = UseRequestProcessor()   return mutate(["useDeleteFlow"], deleteFlowFn, {    onSettled: () => {      queryClient.invalidateQueries({ queryKey: ["useGetFlows"] })      queryClient.invalidateQueries({ queryKey: ["useGetFolder"] })    },    ...options,  // Consumer options come LAST  })}``` ### Do Not Create Thin Wrapper Hooks ```typescript// Do not create hooks that just re-export an existing query hookconst useFlows = () => {  return useGetRefreshFlowsQuery({ get_all: true })} // Import the existing hook directly where neededimport { useGetRefreshFlowsQuery } from "@/controllers/API/queries/flows"``` 
Referenced from SKILL.md