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?
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:
// 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
entrymyBlogPost.fields.title
TextmyBlogPost.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 deepermyBlogPost.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:
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 existingisLinkToAsset()
.
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 sitesfetchBySlug()
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}
- PRO TIP: This is why in your Page components in pure React sites, you can use it to detect standalone mode e.g.
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>
}
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.:
...
// 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:
- binding to scalar variables
- for example.
Text
(prop passed to component isstring
)
- for example.
- 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 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
orLink
variable bindings you have to add to the component logic to resolve references or stitch the data tree that starts from the entity passed viaLink
binding (or an array of entities passed viaArray
binding). - if you’re using
useFetchBySlug()
(which is rarely used in NextJS apps over preferredfetchBySlug()
) you have to check that you’re not relying onexperience
being true all the time. And that your handling ofuseFetchBySlug()
return values corresponds to the logic described earlier in this guide.