Was this page helpful?

Migrate from SDK v1 to SDK v2

Overview

At the heart of every website built with Contentful Studio is the Studio SDK. Over the last two years we’ve been making constant improvements to the SDK by adding some incremental features and fixing bugs.

During that time, we have also accumulated a list of improvements and fixes to the SDK which cannot be introduced in an incremental fashion and we need to bump a major version of the SDK to signal some breaking changes. However, in 98% of cases the upgrade from v1 to v2 should be as simple as a dependency bump.

Code and logic adjustments will be necessary only in the case when your website was relying on some specific niche features (eg. like using Link or Array variable bindings), or relied on some undocumented utility functions or logic.

This guide outlines the list of changes to help you understand the level of changes and whether any code and logic adjustments are required.

What changed?

Important: If your Studio powered website is using Array/Link variable bindings you have to adjust the code according to the instructions in this guide.

The most significant breaking change in the SDK v2 concerns the mechanism of how Array or Link variable bindings are passed into your React component props.

Let’s look at how receiving props when using Link variable binding used to work in SDK v1. Usually, your React component would have the following structure:

// First create definition for a component
// which let's Studio know that we want `myBlogPost` 
// prop to be bound to a link to an entry
const MyBlogPostComponentDefinition = {
  id: 'my-blog-post-component', 
  variables: {
    myBlogPost: {
      type: 'Link', 
    },
  },
};

const MyBlogPostComponent = (props)=>{

  const { myBlogPost } = props; 

  // You have access to flat properties of your entry
  const title = myBlogPost.fields.title;
  const intro = myBlogPost.fields.intro;

  // You have access to assets on your entry (assets are nested by nature)
  const imageUrl = myBlogPost.fields.image.fields.file.url; 

  // In SDKv1 the referenced entry myBlogPost.fields.author was
  // automatically downloaded and available as myBlogPost.fields.author
  // Thus it was fully legal to access referenced entries primitive 
  // fields and assets
  const authorName = myBlogPost.fields.author.fields.fullName;
  const authorImageUrl = myBlogPost.fields.author.fields.authorImage.fields.file.url;


  return (
   <div>
     <header>{title}</header>
     <pre>{intro}</pre>
     <img src={imageUrl}/>
     <div>
       written by <em>{authorName}</em>
       photo: <img src={autohrImageUrl}/>
     </div>
   </div>

  );

}

Note that in this example the React component has immediate access to:

  • all immediate (flat) fields of the myBlogPost entry
    • myBlogPost.fields.title Text
    • myBlogPost.fields.intro Text
  • the Asset present on the entry
    • myBlogPost.fields.image Asset (to get actual url of the asset you’d need to go deeper myBlogPost.fields.image.fields.file.url)
  • the referenced entry (aka nested entry) (and all of its primitive and asset fields)
    • myBlogPost.fields.author
    • myBlogPost.fields.author.fields.fullName
    • myBlogPost.fields.author.fields.authorImage.fields.file.url (nested asset url)

Whilst availability of the immediate fields of the actual myBlogPost entry are expected to be there, the actual values of the referenced entries (eg. myBlogPost.fields.author) are “downloaded” and inserted instead of automatically linked. This happens because, behind the scenes, SDK v1 stitches together the tree of data. The “stitching” mechanism, takes the “head” entry myBlogPost and recursively follows its references (asset references like myBlogPost.fields.image or entry references like myBlogPost.fields.author ) and stitches together a tree of data.

The mechanism for this automatic data stitching provided some convenience in SDK v1, but it also introduced some disadvantages:

  • The stitching mechanism was not rigorously defined, could have had inconsistent tree depth, mutated some entries in the store.
  • It didn't provide enough control over the stitching mechanism. For example, if your React component only needs 1 level of the entry, this stitching may not be required at all. Meanwhile, if your React component needs 3 levels of nesting, the stitching mechanism may not provide enough.

Also, there was a side effect: during the stitching, when receiving an entry into a component, it would mutate it from its original “unstitched” state which could cause some unexpected results in other components that were reusing the same entry.

To solve all of these issues, in SDK v2 we have introduced a stricter and more consistent mechanism for your React component props to receive bindings to entries when using Link/Array variable bindings. The binding mechanism now relies on these clear principles:

  • All entries you receive are “shallow” (their reference fields are just links)
  • All entries you receive are immutable (processed with Object.freeze() to ensure consistency)
  • The stitching mechanism of your data tree is now a transparent mechanism that you can control and explicitly have to add to your components.

To migrate your website from SDK v1 to SDK v2, the most significant change would be changing and adding mechanism for “stitching” your data tree. We’ll provide more details on migration below. Meanwhile let’s outline some less impactful changes of the SDK v2.

As the mechanism for “stitching” data for Array/Link variable bindings is now moved out the SDK and into React components, we provided a few methods to control and implement this mechanism.

One of the foundational steps for stitching data would be taking the reference and resolving it into the actual entry. To accomplish this in your React component, the following pattern can be used:

const MyComponent = (props)=>{
 const myBlogPost = structuredClone(props.myBlogPost);
 const title = myBlogPost.fields.title;
 const linkToAuthor = myBlogPost.fields.author;

 const authorEntry = resolveLinkToEntryInSomeWay(myBlogPost.fields.author);

 // you can use authorEntry directly
 console.log(authorEntry.fields.fullName);

 // or you can reinsert it into it's place in the entry
 myBlogPost.fields.author = authorEntry;

 return (<>
   ...
   <h3>Author: {myBlogPost.fields.author.fields.fullName}</h3>
   ...
 </>);


}

Note that the approach shown above relies on the resolveLinkToEntryInSomeWay() method.

In SDK v2 we provide such a method, as part of the new utility object that we expose, called inMemoryEntities. This utility object represents the concept of the “in memory entity store”. That store is preloaded by the SDK and contains all of the entities and assets referenced in the experience. Your React components have access to this store via the inMemoryEntities namespace or useInMemoryEntities.

import { 
  useInMemoryEntities, 
} from '@contentful/experiences-sdk-react';

const MyComponent = ()=> {

  const inMemoryEntities = useInMemoryEntities();

  // We have a variety of utility methods available 
  // on the `inMemoryEntities` which provide access to the 
  // "in memory entities store". 
  inMemoryEntities.maybeResolveLink();
  inMemoryEntities.maybeResolveByEntryId();
  inMemoryEntities.maybeResolveByAssetId();
  inMemoryEntities.hasAsset();
  inMemoryEntities.hasEntry();
};
import { 
  useInMemoryEntities, 
} from '@contentful/experiences-sdk-react';

const MyComponent = (props) => {
  ...
  const myBlogPost = structuredClone(props.myBlogPost);
  const inMemoryEntities = useInMemoryEntities();
  myBlogPost.fields.author = inMemoryEntities.maybeResolveLink(
    myBlogPost.fields.author
  );
  ...

  return <>
    ...
    <h3>Author name: {myBlogPost.fields.author.fullName}</h3>  
    ...
  </>

};

Utility methods to load additional entities into memory

When you load a webpage powered by the Studio SDK v2, it will load all the entities bound in that webpage. And it will also recursively download up to 3 levels of referenced entries (4 levels of assets).

This recursive downloading ensures that when your React component receives a prop, myBlogPost, enough of its references are already downloaded into the SDK memory and can be accessed by the React component, using inMemoryEntities.maybeResolveLink().

myBlogPost-> author   -> authorImage
LEVEL 2      LEVEL 3     LEVEL 4 
                         SDK downloads
                         assets only
                         on level 4

However, sometimes there may use cases, where your React components rely on even deeper tree of references available from the entry passed in via Link or Array.

The diagram below shows that in case your comopnent wants to have access to deeply nested field like:

myBlogPost.fields.author.fields.authorCity.fields.country.fields.countryCode

You will not get such a deep entity automatically loaded by the SDK, but you will need to download it yourself. There's a detailed guide on how to Manually download entries and assets into the SDK memory and how to use inMemoryEntities.addEntities() method.

However, manually loading such a deep data would be considered an anti-pattern and we'd advise to use it as a last resort.

myBlogPost-> author   -> authorCity      -> country
LEVEL 2      LEVEL 3     LEVEL 4         LEVEL 5
                         not downloaded  not downloaded
                         by SDK          by SDK

Universal utility methods

Many methods are implemented as TypeScript typeguards which will help with typing.

  • The method isLinkToEntry() is added as the missing counterpart of the already existing isLinkToAsset() .
import { 
  isLinkToAsset,
  isLinkToEntry,
} from '@contentful/experiences-sdk-react';

if ( isLinkToEntry(anything) ) {
   assert(anything.sys.linkType === 'Entry');
}

if ( isLinkToAsset(anything) ) {
   assert(anything.sys.linkType === 'Asset');
}

There is also a minor improvement for the logic of isLink(): detection was made stricter. Now, it also expects target.sys.linkType defined (not just the presence of the target.sys.type === 'Link' ).

Fixed semantics of useFetchBySlug() return values

When using Contentful Studio you can publish a website which is pure React or NextJS with Server-Side Rendering (SSR). To publish production sites, most of our customers chose NextJS which conveniently allows rendering the initial version of the Studio website using SSR.

For the Studio SDK to render the website, it must first load the webpage data, meaning loading the experience. The SDK (since v1) provided two mechanisms to do that:

  • useFetchBySlug() hook for pure React CSR sites
  • fetchBySlug() method for NextJS SSR sites

In this SDK v2 we fixed small inconsistency in the return value of the useFetchBySlug() which affected only pure React SSR sites.

Previously, in the SDK v1:

...
export default Page(){

  const {
    experience,
    error,
    isLoading,
    mode,
  } = useFetchBySlug({
    slug,
    localeCode,
    client,
    experienceTypeId: config.experienceTypeId,
    hyperlinkPattern: '/{entry.fields.slug}',
  });

  // In SDK v1 

  // using `error` worked as expcted 
  if (error) ...

  // using `isLoading` worked as expected
  if (isLoading) ...    

  // when using `experience` you would always get an object
  // with single property { hyperlinkPattern }. You'd get
  // this object regardless of whether experience was truly loaded
  // or not.
  if (experience) {
     // this would always execute, regardless of the loading state
     // and will be experience === { hyperLinkPattern: 'some pattern'}
  } 
  else {
     // this would never execute
  }

}

In the SDK v2 we fixed the semantics of the experience variable:

  • it will start as undefined
  • and it will ONLY become defined when an experience is truly loaded when the webpage runs in “standalone” (outside of Studio editor iframe)
  • the variable will still stay undefined in case the webpage runs inside of Studio editor iframe
    • PRO TIP: This is why in your Page components in pure React sites, you can use it to detect standalone mode e.g. if (experience) { do something only in standalone mode}

Let’s look at an example of how semantics of experience were fixed in SDK v2

...
export default PageThatIsClientSideReactComponent(){
  ...
  // extract url parameters  
  ...
  const {
    experience,
    error,
    isLoading,
    mode,
  } = useFetchBySlug({
    slug,
    localeCode,
    client,
    experienceTypeId: config.experienceTypeId,
    hyperlinkPattern: '/{entry.fields.slug}',
  });

  // In SDK v2
  // using `error` works as expected 
  if (error) ...

  // using `isLoading` works as expectd
  if (isLoading) ...    

  // as of SDK v2, during initial render the value of 
  // `experience`=== undefined
  // It only will become defined when two conditions happen:
  //  - page is running in standalone mode (aka outside of Studio Editor iframe)
  //  - experience was succesfully loaded
  if (experience) {
     // You are in "standalone mode" (which is sometimes called 
     // "Preview+Delivery mode" and experience was loaded.
  } 
  else {
     // This means that either experience wasn't yet loaded
     // Or we're running inside of Studio Editor iframe 
     // and `experience` variable will not be populated at all.
     // (This is because when SDK runs inside of Studio Editor iframe
     // the SDK doesn't need to load data itself, all data will be
     // provided by the parent frame).

  }
   ...
   return <div id="mypage"> ... 
     <ExperienceRoot experience={experience} locale={localeCode}/>
   </div>
}
  • All entries you receive are “shallow”

In the SDK v2, when you receive a React prop (configured with Link variable binding) named myBlogPost the prop will have one of the two values:

  • undefined in case user who uses Studio didn’t bind any data (or bound to an empty link, to a link to deleted item or to a link to archived item).
  • “shallow” shape of the MyBlogPost entry (with unresolved links), e.g.:
...
// component definintion
  variables: {
    myBlogPost: {
      type: 'Link'
    }
  },
... 
const MyBlogPostComponent = (props)=>{
  const { myBlogPost } = props;
  console.log(`myBlogPost: `, myBlogPost);
  //... 
}

// And we expect console log output in SDK v2
// having reference fields as links
myBlogPost: {
  sys: {... },
  fields: {
    title: 'My blog post',
    intro: 'This is blog post about...',
    image: {
       sys: {
         type: 'Link',
         linkType: 'Asset',
         id: 'assetId123'
       },
    },
    author: {
       sys: {
         type: 'Link',
         linkType: 'Entry',
         id: 'entryOfAuthorId222'
       },
    },
  },
}

Note that every reference field of the myBlogPost entry (including asset reference, as assets are internally represented as references), is just a link. It’s not a real entry.

This is the main difference vs SDK v1, where every reference field of the entry would have actual resolved entry, similar to this shape:

...
// component definintion
  variables: {
    myBlogPost: {
      type: 'Link'
    }
  },
... 
const MyBlogPostComponent = (props)=>{
  const { myBlogPost } = props;
  console.log(`myBlogPost: `, myBlogPost);

  //... 
}

// And we expect console log output in SDK v1
// having reference fields resolve to entries
myBlogPost: {
  sys: {... },
  fields: {
    title: 'My blog post',
    intro: 'This is blog post about...',
    image: {
       ...
       fields: {
         file: {
           ... 
             url: 'https://some-image-url',
         }
       },
    },

    author: {
       ...
       fields: {
         fullName: 'John Doe',
         authorImage: {
            ...
            fields: { file: { url: 'https://some-image-url' }}
         },
       },
    },
  },
}

In some simple cases your React component may be satisfied with just “shallow” entry, but in most of the cases you will want to have entire data tree stitched. In the SDK v2 we provide explicit tools - utility functions - which can help with that. We’ll describe the dedicated section further below.

  • All entries your React component receives are immutable

In the SDK v2 when your React component receives a prop e.g. myBlogPost that has binding to Link variable type, the object you receive will be immutable. The entry is processed recursively with Object.freeze.

Thus, if you’re planning to mutate this object, you have to clone it first.

Example of using the value of the prop as normal for read-only use-cases:

...
// Example of using as read only
const MyBlogPostComponent = (props)=>{
  const { myBlogPost } = props;
  // fine to use myBlogPost as read only variable
  ...
  return <>...</>;
}
...

Example for use cases where you plan to mutate the entry

...
// Example of using myBlogPost for read/write use cases
const MyBlogPostComponent = (props)=>{
  const myBlogPost  = structuredClone(props.myBlogPost);
  // after cloning you can mutate any field or subfield of the obejct
  myBlogPost.fields.title = "Some new title"
  myBlogPost.fields.image = { ..., fields: { file: { url: 'https://new-image-url'}}}; 
  ...
  return <>...</>;
}
...
  • Mechanism to “stitching” your data tree must be explicitly added to your components

When migrating from Contentful Studio SDK v1 to SDK v2, you have to alter the code of your React component to manually resolve field references for all the entries.

For example, when you’re creating a custom React component that uses binding, that binding will fall into one of the two categories:

  1. binding to scalar variables
    • for example. Text (prop passed to component is string)
  2. binding to objects (entries with references)
    • Link (prop passed to component is a JS object representing entry)
    • Array (prop passed to component is a JS Array holding entries, each of them is a JS-object representing an entry)

In case your React components used Link or Array variable bindings, then you’d need to add logic which “stitches” the data tree aka “resolve references”.

You can resolve single level of references explicitly, or you can do it recursively.

The full guide for how to implement is available at How to correctly resolve references for Array/Link bindings

The principles of the changes are shown in the snippet below:

import type { Asset, UnresolvedLink } from 'contentful';
import { useInMemoryEntities } from '@contentful/experiences-sdk-react';

type ItemWithResolvedReferences = {
  sys: {
    id: string;
  };
  fields: {
    title?: string;
    description?: string;
    image?: Asset; // resolved asset, as opposed to unresolved link
  };
};

type ItemWithUnresolvedReferences = {
  sys: {
    id: string;
  };
  fields: {
    title?: string;
    description?: string;
    image?: UnresolvedLink<'Asset'>; // unresolved link to an asset
  };
};

type ItemWithManuallyResolvedReferences = Omit
<ItemWithUnresolvedReferences, 'fields'> & {
  fields: Omit<ItemWithUnresolvedReferences['fields'], 'image'> & {
    image?: Asset; // manually resolved asset
  };
};

type PropsV1 = {
  item: ItemWithResolvedReferences;
};

type PropsV2 = {
  item: ItemWithUnresolvedReferences;
};

// In SDK v1 the component receives an item, which is an Entry with resolved 
// references. Thus the image field is already an asset, not link to an asset,
// and thus can be used immediately without resolution.
export const ComponentUsingSdkV1: React.FC<PropsV1> = ({ item }) => {
  return (
    <div>
      <img 
        src={item?.fields.image?.fields.file?.url as string} 
        alt={item?.fields.title} />
      <div>
        <h3>{item?.fields.title}</h3>
        <p>{item?.fields.description}</p>
        <button> Explore </button>
      </div>
    </div>
  );
};

// In SDK v2 the component receives an item, which is an Entry with unresolved
// references. Thus the image field is a link object, which needs to be resolved
// and for convenience of access replaced with the actual asset object.
export const ComponentUsingSdkV2: React.FC<PropsV2> = ({ 
  item: itemWithUnresolvedReferences 
}) => {

  // Must make copy! as `item` is marked as immutable by the SDK v2
  const item: ItemWithManuallyResolvedReferences = structuredClone(
    itemWithUnresolvedReferences
    ) as ItemWithManuallyResolvedReferences;
  const inMemoryEntities = useInMemoryEntities();
  item.fields.image = inMemoryEntities.maybeResolveLink(
    item.fields.image
    ) as Asset | undefined;

  return (
    <div>
      <img 
        src={item?.fields.image?.fields.file?.url as string} 
        alt={item?.fields.title} 
       />
      <div>
        <h3>{item?.fields.title}</h3>
        <p>{item?.fields.description}</p>
        <button> Explore </button>
      </div>
    </div>
  );
};

Migration checklist

Here’s a quick checklist for migration:

  • if you’re using Array or Link variable bindings you have to add to the component logic to resolve references or stitch the data tree that starts from the entity passed via Link binding (or an array of entities passed via Array binding).
  • if you’re using useFetchBySlug() (which is rarely used in NextJS apps over preferred fetchBySlug()) you have to check that you’re not relying on experience being true all the time. And that your handling of useFetchBySlug() return values corresponds to the logic described earlier in this guide.