> This page is for Studio.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://contentful-staging.ferndocs.com/developers/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://contentful-staging.ferndocs.com/developers/docs/_mcp/server.

# Migrate from Studio SDK v1 to SDK v2

> Learn how to migrate from v1 of the Contentful Studio SDK to v2.

* [Overview](#overview)
* [What changed](#what-changed)
  * [Handling of Array/Link variable bindings](#handling-of-arraylink-variable-bindings)
  * [Utility methods to handle resolving Array/Link variable bindings](#utility-methods-to-handle-resolving-array-link-variable-binding)
  * [Utility methods to load additional entities into memory](#utility-methods-to-load-additional-entities-into-memory)
  * [Universal utility methods](#universal-utility-methods)
  * [Fixed semantics of useFetchBySlug() return values](#fixed-semantics-of-usefetchbyslug-return-values)
* [Change details for handling Array/Link variable bindings](#changes-details-for-handling-arraylink-variable-bindings)
* [Migration checklist](#migration-checklist)

## 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?

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

### Handling of `Array/Link` variable bindings

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:

```typescript
// 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>&#123;intro&#125;</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.

### Utility methods to handle resolving `Array` link variable binding

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:

```typescript
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`.

```typescript
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();
};
```

```typescript
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()`.

```typescript
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](/manually-download-entries-and-assets)
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.

```typescript
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()` .

```typescript
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:

```typescript
...
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

```typescript
...
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>
}
```

## Changes details for handling `Array/Link` variable bindings

* **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.:

```typescript
...
// 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:

```typescript
...
// 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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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:

```typescript
...
// 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

```typescript
...
// 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.

> **Info**
>
> The full guide for how to implement is available at [How to correctly resolve references for Array/Link bindings](/how-to-resolve-references-for-array-link-bindings)

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

```typescript
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.