| 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"} |
What This Skill Covers
Fundamental patterns for using RTK Query (Redux Toolkit Query) in Pimcore Studio:
- Queries vs mutations
- Using auto-generated API hooks
- Proper error handling with
trackError
- Loading and error states
- Cache management
- Common patterns
When to Use This Skill
Use this when:
- Fetching data from the Pimcore backend
- Implementing create/update/delete operations
- Managing loading states
- Handling API errors properly
- Understanding caching behavior
- Need to refetch data
🚨 Import Paths
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.
What is RTK Query?
RTK Query is Redux Toolkit's data fetching layer:
- Auto-generates React hooks from API endpoints
- Manages caching automatically
- Handles loading/error states
- Provides refetch capabilities
- Built-in request deduplication
In Pimcore Studio: API hooks are auto-generated from OpenAPI specifications, ensuring type safety and backend/frontend consistency.
Queries vs Mutations
Queries - Reading Data
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
})
}
Mutations - Writing Data
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' }
})
}
}
Key Difference:
- Queries trigger automatically when component mounts
- Mutations only execute when you call them
Error Handling - The Correct Way
IMPORTANT: Use trackError, Not try/catch
DON'T use .unwrap() and try/catch blocks! Instead, handle errors via the error state with trackError in useEffect.
Query Error Handling Pattern
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 })
useEffect(() => {
if (!isNil(error)) {
trackError(new ApiError(error))
}
}, [error])
if (isLoading) return <Skeleton />
if (!data) return null
return <div>{data.filename}</div>
}
Mutation Error Handling Pattern
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()
useEffect(() => {
if (!isNil(error)) {
trackError(new ApiError(error))
}
}, [error])
const handleSave = (values: FormValues) => {
updateAsset({
id: asset.id,
body: values
})
}
return (
<Form onFinish={handleSave}>
{/* fields */}
<Button htmlType="submit" loading={isLoading}>
Save
</Button>
</Form>
)
}
Multiple Mutations - Track Each Error Separately
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()
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])
}
Checking for Success vs Error
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])
useEffect(() => {
if (!isNil(data)) {
message.success('Created successfully')
}
}, [data])
const handleSubmit = (values: FormValues) => {
createEntity({ body: values })
}
}
How trackError Works
trackError automatically:
- Shows error modal with formatted error message
- Prevents duplicates - won't show same error multiple times
- Extracts error details from API response (message, errorKey, etc.)
- Displays formatted UI for API errors
You don't need to manually show error messages - trackError handles it!
Basic Query Patterns
Simple Data Fetch
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>
}
Conditional Query (Skip Pattern)
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 }
)
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>
}
Manual Refetch
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>
)
}
Basic Mutation Patterns
Create Operation
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')
}
}, [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>
)
}
Update Operation
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>
)
}
Delete Operation
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')
}
}, [data])
const handleDelete = () => {
deleteAsset({ id: assetId })
}
return (
<Button
danger
onClick={handleDelete}
loading={isLoading}
>
Delete
</Button>
)
}
Understanding Loading States
isLoading vs isFetching
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 load
isFetching: Show refresh indicator when refetching
if (isLoading) {
return <Skeleton active />
}
return (
<div>
{isFetching && <Spin />}
<Button onClick={() => void refetch()}>
Refresh
</Button>
<Content data={data} />
</div>
)
Mutation Loading State
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>
)
Caching Behavior
Automatic Caching
RTK Query caches responses automatically:
const ComponentA = () => {
const { data } = useAssetGetByIdQuery({ id: 123 })
}
const ComponentB = () => {
const { data } = useAssetGetByIdQuery({ id: 123 })
}
Cache Invalidation
Cache is automatically invalidated when:
- Mutation that affects the data is executed
- Cache timeout expires
- Manual invalidation is triggered
Manual Refetch
Force refetch even with cached data:
const { refetch } = useAssetGetByIdQuery({ id: 123 })
refetch()
Common Patterns
List + Detail Pattern
import { isNil } from 'lodash'
const AssetBrowser = () => {
const [selectedId, setSelectedId] = useState<number | null>(null)
const { data: list, error: listError } = useAssetListQuery()
const { data: detail, error: detailError } = useAssetGetByIdQuery(
{ id: selectedId! },
{ skip: !selectedId }
)
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>
)
}
Dependent Queries
Load data based on previous query result:
import { isNil } from 'lodash'
const AssetWithParent = ({ id }: { id: number }) => {
const { data: asset, error: assetError } = useAssetGetByIdQuery({ id })
const { data: parent, error: parentError } = useAssetGetByIdQuery(
{ id: asset?.parentId! },
{ skip: !asset?.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>
)
}
Polling (Auto-refresh)
Automatically refetch at intervals:
const { data } = useAssetGetByIdQuery(
{ id: 123 },
{
pollingInterval: 5000
}
)
Optimistic Updates with Error Recovery
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) {
setLocalState(asset.published)
trackError(new ApiError(error))
}
}, [error, asset.published])
const handleToggle = () => {
const newState = !localState
setLocalState(newState)
updateAsset({
id: asset.id,
body: { published: newState }
})
}
return (
<Switch checked={localState} onChange={handleToggle} />
)
}
API Hook Naming Convention
Pimcore Studio follows a consistent naming pattern:
use{Entity}{Action}{Query|Mutation}
Examples:
useAssetGetByIdQuery - Get single asset
useAssetListQuery - Get asset list
useAssetCreateMutation - Create asset
useAssetUpdateMutation - Update asset
useAssetDeleteMutation - Delete asset
useDataObjectGetByIdQuery - Get data object
useDataObjectUpdateMutation - Update data object
Import pattern:
import {
useAssetGetByIdQuery,
useAssetUpdateMutation
} from '@pimcore/studio-ui-bundle/api/asset'
TypeScript Types
Hooks are fully typed:
const { data } = useAssetGetByIdQuery({ id: 123 })
const [update] = useAssetUpdateMutation()
update({
id: number,
body: AssetUpdateBody
})
Types are auto-generated from OpenAPI spec - always up to date with backend.
Common Mistakes to Avoid
❌ Don't use .unwrap() and try/catch
try {
await updateAsset({ id, body }).unwrap()
message.success('Saved')
} catch (error) {
message.error('Failed')
}
✅ Use error state with trackError
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
const { data } = useAssetGetByIdQuery({ id })
return <div>{data.filename}</div>
✅ Always check data exists
const { data, isLoading } = useAssetGetByIdQuery({ id })
if (isLoading) return <Spin />
if (!data) return null
return <div>{data.filename}</div>
❌ Don't call mutation in render
const [update] = useAssetUpdateMutation()
update({ id, body })
✅ Call mutation in event handler
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
useEffect(() => {
if (error !== undefined) {
trackError(new ApiError(error))
message.error('Failed')
}
}, [error])
✅ Trust trackError to handle error display
useEffect(() => {
if (error !== undefined) {
trackError(new ApiError(error))
}
}, [error])
Performance Tips
- Use
skip option - Don't fetch until needed
- Leverage caching - Same query parameters = cached response
- Conditional rendering - Only mount components when data is ready
- Avoid unnecessary refetches - Trust the cache unless data must be fresh
- Track errors at component top level - One useEffect per error
Common Mistakes
❌ Mistake 1: Using .unwrap() with try/catch
const [updateAsset] = useAssetUpdateMutation()
const handleSave = async () => {
try {
await updateAsset({ id: 123, body: data }).unwrap()
} catch (error) {
console.error(error)
}
}
const [updateAsset, { error }] = useAssetUpdateMutation()
useEffect(() => {
if (error !== undefined) {
trackError(new ApiError(error))
}
}, [error])
❌ Mistake 2: Not Checking isNil Before Using Data
const { data } = useAssetGetByIdQuery({ id })
if (!data) return null
import { isNil } from 'lodash'
const { data } = useAssetGetByIdQuery({ id })
if (isNil(data)) return null
❌ Mistake 3: Double Error Display
useEffect(() => {
if (error !== undefined) {
trackError(new ApiError(error))
message.error('Failed to load')
}
}, [error])
useEffect(() => {
if (error !== undefined) {
trackError(new ApiError(error))
}
}, [error])
❌ Mistake 4: Not Handling Loading States
const { data } = useAssetGetByIdQuery({ id })
return <div>{data.name}</div>
import { isNil } from 'lodash'
const { data, isLoading } = useAssetGetByIdQuery({ id })
if (isLoading) return <Skeleton />
if (isNil(data)) return null
return <div>{data.name}</div>
❌ Mistake 5: Wrong Import Paths
import { useAssetGetByIdQuery } from '@sdk/api/asset'
import { useAssetGetByIdQuery } from '@pimcore/studio-ui-bundle/api/asset'
import { trackError, ApiError } from '@pimcore/studio-ui-bundle/modules/app'
Next Steps