Was this page helpful?

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'
     },
   }
 }

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.

NOTE: The return value of the 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>
  );
};

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;
};