一键导入
pimcore-studio-ui-rtk-query-fundamentals
RTK Query basics for data fetching in Pimcore Studio - queries, mutations, caching, error handling with trackError
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
RTK Query basics for data fetching in Pimcore Studio - queries, mutations, caching, error handling with trackError
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when creating a new color theme or token set for Pimcore Studio UI (a DynamicTypeThemeAbstract subclass registered in the ThemeRegistry), including choosing an extends chain, filling AntD color tokens, seeding the boot palette, and picking colors that meet WCAG AAA contrast
Building filters in Pimcore Studio UI - the declarative defineFilter framework for filter panels and sidebars (createFiltersStore, FiltersRenderer, useFilterQuery) and custom field-filter dynamic types for per-column listing filters (DynamicTypeFieldFilterAbstract, DynamicTypeFieldFilterRegistry). Use this whenever building, customizing, or extending any kind of filtering, filter panel, filter sidebar, or search/filter UI, or adding a new filter type to a listing or data view - even when the user only says "add filtering", "let users filter by X", "filter this list", or "build a filter sidebar".
Building listings in Pimcore Studio UI using the ListingBuilder decorator pattern - sorting, paging, filtering, inline editing, and custom decorators
UX and UI design conventions for Pimcore Studio - layout, spacing, action labels, writing style, and design principles for consistent extensions
Pimcore Studio migration and review checklist — 34-point verification checklist, anti-patterns to avoid, and reference base classes for validating Studio code completeness
Pimcore Studio PHP code style rules — strict types, class modifiers, final/readonly patterns, formatting (120 chars), named arguments, imports, PHPDoc, @throws documentation, and constructor promotion
| name | pimcore-studio-ui-rtk-query-fundamentals |
| description | RTK Query basics for data fetching in Pimcore Studio - queries, mutations, caching, error handling with trackError |
| metadata | {"audience":"pimcore-developers","focus":"data-fetching"} |
Fundamental patterns for using RTK Query (Redux Toolkit Query) in Pimcore Studio:
trackErrorUse this when:
BEFORE writing any import, read CRITICAL-IMPORT-PATHS.md.
All examples below use bundle imports (@pimcore/studio-ui-bundle/*). For core development (@sdk/*, @Pimcore/*), see the referenced file.
RTK Query is Redux Toolkit's data fetching layer:
In Pimcore Studio: API hooks are auto-generated from OpenAPI specifications, ensuring type safety and backend/frontend consistency.
Use queries for: GET requests, fetching data
import { useAssetGetByIdQuery } from '@pimcore/studio-ui-bundle/api/asset'
const MyComponent = ({ assetId }: { assetId: number }) => {
const { data, isLoading, error, refetch } = useAssetGetByIdQuery({
id: assetId
})
// data: response from API
// isLoading: true during initial fetch
// error: contains error if request failed
// refetch: function to manually refetch
}
Use mutations for: POST, PUT, PATCH, DELETE requests
import { useAssetUpdateMutation } from '@pimcore/studio-ui-bundle/api/asset'
const MyComponent = () => {
const [updateAsset, { isLoading, error }] = useAssetUpdateMutation()
const handleSave = () => {
updateAsset({
id: 123,
body: { filename: 'new-name.jpg' }
})
// Don't use .unwrap() - handle errors via error state
}
}
Key Difference:
DON'T use .unwrap() and try/catch blocks! Instead, handle errors via the error state with trackError in useEffect.
import { useAssetGetByIdQuery } from '@pimcore/studio-ui-bundle/api/asset'
import { trackError, ApiError } from '@pimcore/studio-ui-bundle/modules/app'
import { useEffect } from 'react'
import { isNil } from 'lodash'
const AssetDetail = ({ assetId }: { assetId: number }) => {
const { data, isLoading, error } = useAssetGetByIdQuery({ id: assetId })
// Track errors at component top level
useEffect(() => {
if (!isNil(error)) {
trackError(new ApiError(error))
}
}, [error])
if (isLoading) return <Skeleton />
if (!data) return null
return <div>{data.filename}</div>
}
import { useAssetUpdateMutation } from '@pimcore/studio-ui-bundle/api/asset'
import { trackError, ApiError } from '@pimcore/studio-ui-bundle/modules/app'
import { useEffect } from 'react'
import { isNil } from 'lodash'
const AssetEditor = ({ asset }: { asset: Asset }) => {
const [updateAsset, { isLoading, error }] = useAssetUpdateMutation()
// Track mutation errors at component top level
useEffect(() => {
if (!isNil(error)) {
trackError(new ApiError(error))
}
}, [error])
const handleSave = (values: FormValues) => {
// No try/catch needed - errors tracked via useEffect
updateAsset({
id: asset.id,
body: values
})
}
return (
<Form onFinish={handleSave}>
{/* fields */}
<Button htmlType="submit" loading={isLoading}>
Save
</Button>
</Form>
)
}
When you have multiple mutations, track each error separately:
import { isNil } from 'lodash'
const MyComponent = () => {
const [createMutation, { error: createError }] = useCreateMutation()
const [updateMutation, { error: updateError }] = useUpdateMutation()
const [deleteMutation, { error: deleteError }] = useDeleteMutation()
// Track each error separately
useEffect(() => {
if (!isNil(createError)) {
trackError(new ApiError(createError))
}
}, [createError])
useEffect(() => {
if (!isNil(updateError)) {
trackError(new ApiError(updateError))
}
}, [updateError])
useEffect(() => {
if (!isNil(deleteError)) {
trackError(new ApiError(deleteError))
}
}, [deleteError])
}
Instead of try/catch, check the error state:
import { isNil } from 'lodash'
const MyForm = () => {
const [createEntity, { data, error, isLoading }] = useCreateEntityMutation()
useEffect(() => {
if (!isNil(error)) {
trackError(new ApiError(error))
}
}, [error])
// Check for success
useEffect(() => {
if (!isNil(data)) {
// Success! Navigate, close dialog, show message, etc.
message.success('Created successfully')
}
}, [data])
const handleSubmit = (values: FormValues) => {
createEntity({ body: values })
}
}
trackError automatically:
You don't need to manually show error messages - trackError handles it!
import { useDataObjectGetByIdQuery } from '@pimcore/studio-ui-bundle/api/data-object'
import { trackError, ApiError } from '@pimcore/studio-ui-bundle/modules/app'
import { isNil } from 'lodash'
const DataObjectDetail = ({ id }: { id: number }) => {
const { data, isLoading, error } = useDataObjectGetByIdQuery({ id })
useEffect(() => {
if (!isNil(error)) {
trackError(new ApiError(error))
}
}, [error])
if (isLoading) return <Spin />
if (!data) return null
return <div>{data.key}</div>
}
Don't fetch until condition is met:
import { isNil } from 'lodash'
const DetailPanel = ({ selectedId }: { selectedId: number | null }) => {
const { data, error } = useAssetGetByIdQuery(
{ id: selectedId! },
{ skip: selectedId === null } // Don't fetch if no selection
)
useEffect(() => {
if (!isNil(error)) {
trackError(new ApiError(error))
}
}, [error])
if (!selectedId) return <div>Select an asset</div>
if (!data) return <Spin />
return <div>{data.filename}</div>
}
const AssetView = ({ id }: { id: number }) => {
const { data, isFetching, refetch } = useAssetGetByIdQuery({ id })
return (
<div>
<Button
onClick={() => void refetch()}
loading={isFetching}
>
Refresh
</Button>
<div>{data?.filename}</div>
</div>
)
}
import { useDataObjectCreateMutation } from '@pimcore/studio-ui-bundle/api/data-object'
import { trackError, ApiError } from '@pimcore/studio-ui-bundle/modules/app'
import { isNil } from 'lodash'
const CreateDialog = () => {
const [create, { data, error, isLoading }] = useDataObjectCreateMutation()
useEffect(() => {
if (!isNil(error)) {
trackError(new ApiError(error))
}
}, [error])
useEffect(() => {
if (!isNil(data)) {
message.success('Created successfully')
// Close dialog, navigate, etc.
}
}, [data])
const handleCreate = (values: FormValues) => {
create({
body: {
parentId: values.parentId,
key: values.key,
className: values.className
}
})
}
return (
<FormKit type="form" onSubmit={handleCreate}>
{/* form fields */}
<FormKit type="submit" loading={isLoading}>
Create
</FormKit>
</FormKit>
)
}
import { useAssetUpdateMutation } from '@pimcore/studio-ui-bundle/api/asset'
import { trackError, ApiError } from '@pimcore/studio-ui-bundle/modules/app'
import { isNil } from 'lodash'
const AssetEditor = ({ asset }: { asset: Asset }) => {
const [updateAsset, { data, error, isLoading }] = useAssetUpdateMutation()
useEffect(() => {
if (!isNil(error)) {
trackError(new ApiError(error))
}
}, [error])
useEffect(() => {
if (!isNil(data)) {
message.success('Saved successfully')
}
}, [data])
const handleSave = (values: FormValues) => {
updateAsset({
id: asset.id,
body: values
})
}
return (
<FormKit type="form" onSubmit={handleSave}>
{/* fields */}
<FormKit type="submit" loading={isLoading}>
Save
</FormKit>
</FormKit>
)
}
import { useAssetDeleteMutation } from '@pimcore/studio-ui-bundle/api/asset'
import { trackError, ApiError } from '@pimcore/studio-ui-bundle/modules/app'
import { isNil } from 'lodash'
const DeleteButton = ({ assetId }: { assetId: number }) => {
const [deleteAsset, { data, error, isLoading }] = useAssetDeleteMutation()
useEffect(() => {
if (!isNil(error)) {
trackError(new ApiError(error))
}
}, [error])
useEffect(() => {
if (!isNil(data)) {
message.success('Deleted successfully')
// Navigate away, close dialog, etc.
}
}, [data])
const handleDelete = () => {
deleteAsset({ id: assetId })
}
return (
<Button
danger
onClick={handleDelete}
loading={isLoading}
>
Delete
</Button>
)
}
const { data, isLoading, isFetching } = useAssetGetByIdQuery({ id })
isLoading: true only on first fetch (no cached data exists)isFetching: true on any fetch (including refetch with cached data)Use cases:
isLoading: Show skeleton/spinner for initial loadisFetching: Show refresh indicator when refetching// Initial load
if (isLoading) {
return <Skeleton active />
}
// Refetch indicator
return (
<div>
{isFetching && <Spin />}
<Button onClick={() => void refetch()}>
Refresh
</Button>
<Content data={data} />
</div>
)
const [updateAsset, { isLoading: isSaving }] = useAssetUpdateMutation()
return (
<FormKit type="form" onSubmit={handleSave}>
{/* form fields */}
<FormKit
type="submit"
loading={isSaving}
disabled={isSaving}
>
{isSaving ? 'Saving...' : 'Save'}
</FormKit>
</FormKit>
)
RTK Query caches responses automatically:
// First component
const ComponentA = () => {
const { data } = useAssetGetByIdQuery({ id: 123 })
// Fetches from server
}
// Second component (same query)
const ComponentB = () => {
const { data } = useAssetGetByIdQuery({ id: 123 })
// Uses cached data, no network request!
}
Cache is automatically invalidated when:
Force refetch even with cached data:
const { refetch } = useAssetGetByIdQuery({ id: 123 })
// Refetch from server
refetch()
import { isNil } from 'lodash'
const AssetBrowser = () => {
const [selectedId, setSelectedId] = useState<number | null>(null)
// List query (always active)
const { data: list, error: listError } = useAssetListQuery()
// Detail query (only when selected)
const { data: detail, error: detailError } = useAssetGetByIdQuery(
{ id: selectedId! },
{ skip: !selectedId }
)
// Track errors
useEffect(() => {
if (!isNil(listError)) {
trackError(new ApiError(listError))
}
}, [listError])
useEffect(() => {
if (!isNil(detailError)) {
trackError(new ApiError(detailError))
}
}, [detailError])
return (
<div>
<AssetList
assets={list}
onSelect={setSelectedId}
/>
{detail && <AssetDetail asset={detail} />}
</div>
)
}
Load data based on previous query result:
import { isNil } from 'lodash'
const AssetWithParent = ({ id }: { id: number }) => {
// First query
const { data: asset, error: assetError } = useAssetGetByIdQuery({ id })
// Second query (depends on first)
const { data: parent, error: parentError } = useAssetGetByIdQuery(
{ id: asset?.parentId! },
{ skip: !asset?.parentId } // Don't fetch until we have parentId
)
useEffect(() => {
if (!isNil(assetError)) {
trackError(new ApiError(assetError))
}
}, [assetError])
useEffect(() => {
if (!isNil(parentError)) {
trackError(new ApiError(parentError))
}
}, [parentError])
return (
<div>
<div>Asset: {asset?.filename}</div>
{parent && <div>Parent: {parent.filename}</div>}
</div>
)
}
Automatically refetch at intervals:
const { data } = useAssetGetByIdQuery(
{ id: 123 },
{
pollingInterval: 5000 // Refetch every 5 seconds
}
)
Show UI change immediately, revert if mutation fails:
const ToggleButton = ({ asset }: { asset: Asset }) => {
const [localState, setLocalState] = useState(asset.published)
const [updateAsset, { error }] = useAssetUpdateMutation()
useEffect(() => {
if (error !== undefined) {
// Revert on error
setLocalState(asset.published)
trackError(new ApiError(error))
}
}, [error, asset.published])
const handleToggle = () => {
const newState = !localState
setLocalState(newState) // Optimistic update
updateAsset({
id: asset.id,
body: { published: newState }
})
}
return (
<Switch checked={localState} onChange={handleToggle} />
)
}
Pimcore Studio follows a consistent naming pattern:
use{Entity}{Action}{Query|Mutation}
Examples:
useAssetGetByIdQuery - Get single assetuseAssetListQuery - Get asset listuseAssetCreateMutation - Create assetuseAssetUpdateMutation - Update assetuseAssetDeleteMutation - Delete assetuseDataObjectGetByIdQuery - Get data objectuseDataObjectUpdateMutation - Update data objectImport pattern:
import {
useAssetGetByIdQuery,
useAssetUpdateMutation
} from '@pimcore/studio-ui-bundle/api/asset'
Hooks are fully typed:
// Query hook returns typed response
const { data } = useAssetGetByIdQuery({ id: 123 })
// data: Asset | undefined
// Mutation accepts typed parameters
const [update] = useAssetUpdateMutation()
update({
id: number,
body: AssetUpdateBody
})
Types are auto-generated from OpenAPI spec - always up to date with backend.
❌ Don't use .unwrap() and try/catch
// BAD - Don't do this!
try {
await updateAsset({ id, body }).unwrap()
message.success('Saved')
} catch (error) {
message.error('Failed')
}
✅ Use error state with trackError
// GOOD
const [updateAsset, { data, error }] = useAssetUpdateMutation()
useEffect(() => {
if (error !== undefined) {
trackError(new ApiError(error))
}
}, [error])
useEffect(() => {
if (data !== undefined) {
message.success('Saved')
}
}, [data])
const handleSave = (values: FormValues) => {
updateAsset({ id, body: values })
}
return (
<FormKit type="form" onSubmit={handleSave}>
{/* form fields */}
</FormKit>
)
❌ Don't forget to handle loading state
// BAD
const { data } = useAssetGetByIdQuery({ id })
return <div>{data.filename}</div> // Error if data undefined!
✅ Always check data exists
// GOOD
const { data, isLoading } = useAssetGetByIdQuery({ id })
if (isLoading) return <Spin />
if (!data) return null
return <div>{data.filename}</div>
❌ Don't call mutation in render
// BAD - causes infinite loop
const [update] = useAssetUpdateMutation()
update({ id, body }) // Called every render!
✅ Call mutation in event handler
// GOOD
const [update] = useAssetUpdateMutation()
const handleSave = (values: FormValues) => update({ id, body: values })
return (
<FormKit type="form" onSubmit={handleSave}>
{/* form fields */}
<FormKit type="submit">Save</FormKit>
</FormKit>
)
❌ Don't manually show error messages
// BAD - trackError already shows the error!
useEffect(() => {
if (error !== undefined) {
trackError(new ApiError(error))
message.error('Failed') // Duplicate error display!
}
}, [error])
✅ Trust trackError to handle error display
// GOOD - trackError shows error modal automatically
useEffect(() => {
if (error !== undefined) {
trackError(new ApiError(error))
}
}, [error])
skip option - Don't fetch until needed// ❌ WRONG - Don't use .unwrap()
const [updateAsset] = useAssetUpdateMutation()
const handleSave = async () => {
try {
await updateAsset({ id: 123, body: data }).unwrap()
} catch (error) {
console.error(error)
}
}
// ✅ CORRECT - Use error state with trackError
const [updateAsset, { error }] = useAssetUpdateMutation()
useEffect(() => {
if (error !== undefined) {
trackError(new ApiError(error))
}
}, [error])
// ❌ WRONG - Falsy check
const { data } = useAssetGetByIdQuery({ id })
if (!data) return null
// ✅ CORRECT - Use isNil
import { isNil } from 'lodash'
const { data } = useAssetGetByIdQuery({ id })
if (isNil(data)) return null
// ❌ WRONG - trackError already shows error modal!
useEffect(() => {
if (error !== undefined) {
trackError(new ApiError(error))
message.error('Failed to load') // Duplicate!
}
}, [error])
// ✅ CORRECT - trackError handles display
useEffect(() => {
if (error !== undefined) {
trackError(new ApiError(error))
}
}, [error])
// ❌ WRONG - Rendering before data loaded
const { data } = useAssetGetByIdQuery({ id })
return <div>{data.name}</div> // Crashes if data is undefined!
// ✅ CORRECT - Check loading and nil
import { isNil } from 'lodash'
const { data, isLoading } = useAssetGetByIdQuery({ id })
if (isLoading) return <Skeleton />
if (isNil(data)) return null
return <div>{data.name}</div>
// ❌ WRONG - In bundle, using @sdk
import { useAssetGetByIdQuery } from '@sdk/api/asset'
// ✅ CORRECT - Use bundle imports
import { useAssetGetByIdQuery } from '@pimcore/studio-ui-bundle/api/asset'
import { trackError, ApiError } from '@pimcore/studio-ui-bundle/modules/app'