Build a collection component
Overview
If you want to implement a React component that displays a collection of items in Studio, you can use the Array
variable type to allow binding multiple entries to a field. By including the Array
variable type inside your React component, your component prop can receive entries that will be linking to other entries or assets. To convert those links into actual entries and assets for use in your component, you have to add special logic to your code.
This guide covers how to add special logic for resolving links to entries or assets when using the Array
variable type. This logic also applies when a React component is using the Link
variable type.
The Link
variable type is useful when you need to pass an entry and its associated data into your component. For example, if a component displays the BlogPost
item together with the relevant BlogAuthor
, then a blogPost
variable of type Link
can be used with this component. And after preforming link resolution logic as described in this guide, the component will be able to access the BlogAuthor
entry instance via props.blogPost.fields.blogAuthor.fields.fullName
.
Build a custom component with Contentful Studio SDK v2
In Contentful Studio SDK v2, entries passed to components, such as Link
or Array
bindings, do not have references resolved. You must resolve those references manually, using a utility function.
The Studio SDK does the heavy lifting and traverses the tree of the entries, following their links until depth level-3, and downloads and stores in memory all the referenced entries and assets.
This means that your React component doesn't need to have entity downloading logic, or to have any sophisticated useEffects or other calls to REST calls to fetch data from the next level.
Starting with the Studio SDK v2, when entries are passed into your React component, the entries have their references unresolved, meaning entries are “shallow”.
This means that when your entry has reference fields (or multi-reference fields), those fields will contain thin UnresolvedLink<>
objects and not the actual entry/asset. This is changed behaviour vs Studio SDK v1, where entries passed as props via Link
or Array
variable types would have their references resolved, and instead of thin UnresolvedLink<>
object, would have the actual entry or asset.
To make things clear, here's an example of a "shallow" entry with unresolved reference fields passed to a React component as of the Studio SDK v2:
type ItemShallow = {
sys: { id: string },
fields: {
title: string;
description: string;
image: UnresolvedLink<'Asset'>;
},
};
// Sample of a shallow item would be:
const sampleOfItemShallow : ItemShallow = {
fields: {
title: 'title',
description: 'description',
image: {
sys: {
type: 'Link',
linkType: 'Asset',
id: 'imageId'
},
}
}
Resolve references using the inMemoryEntities.maybeResolveLink()
utility function
In the previous section, we mentioned that as of Studio SDK v2, your React component is responsible for adding the special logic to resolve the references into actual entries or assets.
To make this easy, the Studio SDK v2 provides a utility namespace, inMemoryEntities
, and helper methods like inMemoryEntities.maybeResolveLink()
.
The following example shows a React component that accepts a single prop item
of Studio type Array
. In the Studio user interface, users can bind a multi-reference field to such a prop which will make the React component receive an array of actual entities references by the multi-reference field. But note that the items
property will contain entities with unresolved references. This is demonstrated in the code via the ItemWithUnresolvedReference
type.
The code below iterates over all of the elements of the items
array, and for the purpose of the example, manually creates new entry with explicit fields title
, description
, image
. You may want to use Object.entries()
to iterate over all of the fields and automatically create a new entry with all reference fields resolved into actual entries or assets. Below, however, the inMemoryEntities.maybeResolveLink()
utility function is used to resolve a single reference field item.fields.image
.
inMemoryEntities.maybeResolveLink()
utility function can be either resulting entry or asset. It could also return undefined
if such an entry is not available in the SDK memory. Remember that the SDK will do the heavy lifting and download into memory all the entries and assets up to depth level-3. Which means that item
and its references will be available in memory.
import React from 'react';
import type { Asset, UnresolvedLink } from 'contentful';
import { inMemoryEntities } from '@contentful/experiences-sdk-react';
import styles from './styles.module.css';
type ItemWithUnresolvedReference = {
sys: { id: string };
fields: {
title?: string;
description?: string;
image?: UnresolvedLink<'Asset'>;
};
};
type ItemResolved = Omit<ItemWithUnresolvedReference, 'fields'> & {
fields: Omit<ItemWithUnresolvedReference['fields'], 'image'> & {
image?: Asset;
};
};
type StudioItemProps = {
item: ItemResolved;
};
type StudioCollectionProps = {
items: ItemWithUnresolvedReference[];
};
export const StudioCollection: React.FC<StudioCollectionProps> = ({ items }) => {
if (items === undefined) {
return <div className={styles.studioCollectionEmpty}>No items available</div>;
}
/*
At this point items[0] is a "shallow" item, with with shape of ItemWithUnresolvedReference
In this shape the field `.fields.image` does NOT contain the actual Asset shape, but merely a link to the asset.
The asset must be acquired and substituted into `fields.image` field as shown below.
Let's look at the algorithm to resolve the links into real assets/entries:
To do this we need to make a copy of the itemWithUnresolvedReference, and replace
all of its link fields with the actual objects representing assets/entries.
In this case, we only have a single field `.fields.image` which we need to set to the actual asset.
We can acquire the asset by link form the SDK memory by calling `inMemoryEntities.maybeResolveLink(item.fields.image)`.
REMEMBER: It is important to always make a copy of the original itemWithUnresolvedReference
as the JS object representing the item is frozen with Object.freeze() and cannot be mutated.
The items in SDK's memory are immutable, so that on multiple renders/queries their contents is consistent.
Here I am making copy by recreating the object, but you can use structuredClone(item) as well.
*/
const itemsResolved = items.map((itemWithUnresolvedReference) => {
const resolvedItem: ItemResolved = {
sys: {
id: itemWithUnresolvedReference.sys.id,
},
fields: {
title: itemWithUnresolvedReference.fields.title,
description: itemWithUnresolvedReference.fields.description,
image: inMemoryEntities.maybeResolveLink(itemWithUnresolvedReference.fields.image) as unknown as Asset|undefined,
},
};
return resolvedItem;
});
return (
<div className={styles.studioCollection}>
{itemsResolved.map((resolvedItem) => (
<StudioItem key={resolvedItem.sys.id} item={resolvedItem} />
))}
</div>
);
};
const StudioItem: React.FC<StudioItemProps> = ({ item }) => {
// Defensive programming: during Editing, user fields of any entities may be undefined.
// Contentful platform can only guarantee presence of user`s entities marked as required in the Content Model,
// when entity is published. During editing in Studio, draft entities may be passed to the react component and
// need to be handled accordingly.
const title = item.fields.title || "No title";
const description = item.fields.description || "No description";
const image_url: string = item.fields.image?.fields?.file?.url as string || "https://placehold.co/300x300";
return (
<div className={styles.studioItem}>
<img
src={image_url}
alt={title}
className={styles.image}
/>
<div className={styles.content}>
<h3>{title}</h3>
<p>{description}</p>
<button className={styles.button}>Explore</button>
</div>
</div>
);
};
Resolve all references using the resolveEntityLinks
utility function
As we've seen above, you can resolve each individual link using inMemoryEntities.maybeResolveLink()
function. However, that resolving each item individually may be tedious.
So to help you avoid resolving things one by one, we have prepared a utility function resolveEntityLinks() that will automatically resolve all the reference fields of an entity into actual entries or assets.
This variant of the utility function is not recursive, and will resolve just the next level. This means that in your component you can use your "level-2" data via item.fields.title
. And next level - the "level-3" data via item.fields.image.fields.file.url
.
import React from 'react';
import { resolveEntityLinks } from './resolutionUtils';
import { isAsset } from '@contentful/experiences-sdk-react';
import type { Asset, Entry, EntrySkeletonType } from 'contentful';
import styles from './styles.module.css';
type ItemFields = {
title: string;
description: string;
image: Asset;
};
type ItemSkeleton = EntrySkeletonType<ItemFields, 'myContentTypeId'>;
type Item = Entry<ItemSkeleton>;
type StudioItemProps = {
item: Item;
};
type StudioCollectionProps = {
items: Item[];
};
export const StudioCollection: React.FC<StudioCollectionProps> = ({ items }) => {
if (items === undefined) {
return <div className={styles.studioCollectionEmpty}>No items available</div>;
}
const itemsResolved: Array<Item> = items
.map((item) => resolveEntityLinks(item))
.filter(Boolean) as Array<Item>; // remove items which resolution (there should not be any)
return (
<div className={styles.studioCollection}>
{itemsResolved.map((resolvedItem) => (
<StudioItem key={resolvedItem!.sys.id} item={resolvedItem} />
))}
</div>
);
};
const StudioItem: React.FC<StudioItemProps> = ({ item }) => {
// some type juggling to make TypeScript happy
const image = isAsset(item.fields.image)
? item.fields.image
: createPlaceholderAsset({ url: `https://via.placeholder.com/150` });
const itemFields: ItemFields = item.fields as ItemFields;
return (
<div className={styles.studioItem}>
<img
src={image?.fields?.file?.url as string}
alt={itemFields.title}
className={styles.image}
/>
<div className={styles.content}>
<h3>{itemFields.title}</h3>
<p>{itemFields.description}</p>
<button className={styles.button}>Explore</button>
</div>
</div>
);
};
function createPlaceholderAsset({ url }: { url: string }): Asset {
return {
sys: {
id: 'placeholderId',
type: 'Asset',
},
fields: {
title: 'Placeholder Image',
file: {
url,
details: {
size: 123,
image: {
width: 123,
height: 123,
},
},
contentType: 'image/jpeg',
},
},
} as unknown as Asset; // this is minimal set of fields to work with asset
}
Here is the source code for the resolveEntityLinks()
function.
You can find the latest source code in the examples folder for the SDK repo.
import type { Entry, Asset, UnresolvedLink } from 'contentful';
import { inMemoryEntities, isAsset, isEntry, isArrayOfLinks, isLink } from '@contentful/experiences-sdk-react';
export const resolveEntityLinks = (
entity: Entry | Asset | undefined,
): Entry | Asset | undefined => {
if (entity === undefined) {
return undefined;
}
if (isAsset(entity)) {
return structuredClone(entity);
}
if (isEntry(entity)) {
const e = structuredClone(entity) as Entry;
for (const [fname, value] of Object.entries(e.fields)) {
// @ts-expect-error casting unkonwn
e.fields[fname] = resolveLinkOrArrayOrPassthrough(value); // default behaviour when link is not resolved is to return undefined
}
return e;
}
// everything else just pass through, but copy
return structuredClone(entity);
};
export const resolveLinkOrArrayOrPassthrough = (
fieldValue: unknown | UnresolvedLink<'Asset'> | UnresolvedLink<'Entry'>,
) => {
if (isLink(fieldValue)) {
return inMemoryEntities.maybeResolveLink(
fieldValue as unknown as UnresolvedLink<'Asset'> | UnresolvedLink<'Entry'>,
);
}
if (isArrayOfLinks(fieldValue)) {
return fieldValue.map((link) => inMemoryEntities.maybeResolveLink(link));
}
// we just pass through the value
return fieldValue;
};