diff --git a/CHANGELOG.md b/CHANGELOG.md
index e179bbf896..0efb60825e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,10 @@ Our versioning strategy is as follows:
> ⚠️ **JSS 23 supports Sitecore XP 10.5 only. Sitecore AI is not supported - use Sitecore Content SDK for that scenario.**
+### 🎉 New Features & Improvements
+
+* `[sitecore-jss]` `[sitecore-jss-react]` Treat Layout Service `isContentResolved` as datasource validity in `withDatasourceCheck()` ([#2219](https://github.com/Sitecore/jss/pull/2219))
+
### 🐛 Bug Fixes
* `[sitecore-jss-react]` Suppress hydration mismatch warnings in Experience Editor by adding `suppressHydrationWarning` to SDK chrome paths (ErrorBoundary, placeholders, field components)([#2218](https://github.com/Sitecore/jss/pull/2218))
diff --git a/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.test.tsx b/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.test.tsx
index f2d81852c5..296d1675ba 100644
--- a/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.test.tsx
+++ b/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.test.tsx
@@ -146,4 +146,217 @@ describe('withDatasourceCheck', () => {
expect(wrapper.container.innerHTML).to.contain(props.rendering.componentName);
expect(wrapper.container.innerHTML).to.contain(props.rendering.dataSource);
});
+
+ it('should return null if no datasource is configured on the rendering', () => {
+ const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent);
+ const props = {
+ rendering: {
+ componentName: 'TestComponent',
+ },
+ };
+
+ const wrapper = render(
+
+
+
+ );
+
+ expect(wrapper.container.innerHTML).to.be.empty;
+ });
+
+ it('should return wrapped component when isContentResolved is true', () => {
+ const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent);
+ const props = {
+ rendering: {
+ componentName: 'TestComponent',
+ dataSource: '{CACDB205-2386-4271-9F05-AE20AAC2A39E}',
+ isContentResolved: true,
+ },
+ };
+
+ const wrapper = render(
+
+
+
+ );
+
+ expect(wrapper.container.innerHTML).to.contain(props.rendering.componentName);
+ expect(wrapper.container.innerHTML).to.contain(props.rendering.dataSource);
+ });
+
+ it('should return wrapped component when isContentResolved is true in editing mode', () => {
+ const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent);
+ const props = {
+ rendering: {
+ componentName: 'TestComponent',
+ dataSource: '{CACDB205-2386-4271-9F05-AE20AAC2A39E}',
+ isContentResolved: true,
+ },
+ };
+
+ const wrapper = render(
+
+
+
+ );
+
+ expect(wrapper.container.innerHTML).to.contain(props.rendering.componentName);
+ expect(wrapper.container.innerHTML).to.contain(props.rendering.dataSource);
+ });
+
+ it('should return null when isContentResolved is false in normal mode', () => {
+ const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent);
+ const props = {
+ rendering: {
+ componentName: 'TestComponent',
+ dataSource: '{CACDB205-2386-4271-9F05-AE20AAC2A39E}',
+ isContentResolved: false,
+ },
+ };
+
+ const wrapper = render(
+
+
+
+ );
+
+ expect(wrapper.container.innerHTML).to.be.empty;
+ });
+
+ it('should return default error component when isContentResolved is false in editing mode', () => {
+ const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent);
+ const props = {
+ rendering: {
+ componentName: 'TestComponent',
+ dataSource: '{CACDB205-2386-4271-9F05-AE20AAC2A39E}',
+ isContentResolved: false,
+ },
+ };
+
+ const wrapper = render(
+
+
+
+ );
+
+ expect(wrapper.container.querySelectorAll('div.sc-jss-editing-error')).to.have.length(1);
+ });
+
+ it('should not render when the datasource item was deleted and isContentResolved is false', () => {
+ const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent);
+ const props = {
+ rendering: {
+ componentName: 'TestComponent',
+ dataSource: '{DELETED-DATASOURCE-ID}',
+ isContentResolved: false,
+ },
+ };
+
+ const wrapper = render(
+
+
+
+ );
+
+ expect(wrapper.container.innerHTML).to.be.empty;
+ });
+
+ it('should not render when the datasource item was archived and isContentResolved is false', () => {
+ const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent);
+ const props = {
+ rendering: {
+ componentName: 'TestComponent',
+ dataSource: '{ARCHIVED-DATASOURCE-ID}',
+ isContentResolved: false,
+ },
+ };
+
+ const wrapper = render(
+
+
+
+ );
+
+ expect(wrapper.container.innerHTML).to.be.empty;
+ });
+
+ it('should preserve existing missing-datasource behavior when isContentResolved is true', () => {
+ const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent);
+ const props = {
+ rendering: {
+ componentName: 'TestComponent',
+ dataSource: '',
+ isContentResolved: true,
+ },
+ };
+
+ const wrapper = render(
+
+
+
+ );
+
+ expect(wrapper.container.innerHTML).to.be.empty;
+ });
+
+ it('should preserve existing behavior when isContentResolved is absent', () => {
+ const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent);
+ const propsWithDatasource = {
+ rendering: {
+ componentName: 'TestComponent',
+ dataSource: '{CACDB205-2386-4271-9F05-AE20AAC2A39E}',
+ },
+ };
+ const propsWithoutDatasource = {
+ rendering: {
+ componentName: 'TestComponent',
+ dataSource: '',
+ },
+ };
+
+ const rendered = render(
+
+
+
+ );
+ const hidden = render(
+
+
+
+ );
+
+ expect(rendered.container.innerHTML).to.contain(propsWithDatasource.rendering.componentName);
+ expect(hidden.container.innerHTML).to.be.empty;
+ });
+
+ it('should evaluate nested placeholder renderings independently', () => {
+ const TestComponentWithDatasourceCheck = withDatasourceCheck()(TestComponent);
+ const nestedChild = {
+ componentName: 'ChildComponent',
+ dataSource: '{CHILD-DATASOURCE-ID}',
+ isContentResolved: false,
+ };
+ const parentRendering = {
+ componentName: 'ParentComponent',
+ dataSource: '{PARENT-DATASOURCE-ID}',
+ isContentResolved: true,
+ placeholders: {
+ nested: [nestedChild],
+ },
+ };
+
+ const parentWrapper = render(
+
+
+
+ );
+ const childWrapper = render(
+
+
+
+ );
+
+ expect(parentWrapper.container.innerHTML).to.contain(parentRendering.componentName);
+ expect(childWrapper.container.innerHTML).to.be.empty;
+ });
});
diff --git a/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.tsx b/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.tsx
index 390ecaf91c..6430f9ce65 100644
--- a/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.tsx
+++ b/packages/sitecore-jss-react/src/enhancers/withDatasourceCheck.tsx
@@ -14,18 +14,40 @@ export interface WithDatasourceCheckProps {
export interface WithDatasourceCheckOptions {
/**
- * A component that is rendered when a datasource is missing during editing.
+ * A component that is rendered when a datasource is missing or failed to resolve during editing.
* If unspecified, a default component with message is displayed.
*/
editingErrorComponent?: React.ComponentClass | React.FC;
}
/**
- * Checks whether a Sitecore datasource is present and renders appropriately depending on page mode (normal vs editing).
+ * Returns true when the rendering has a datasource and Layout Service did not report unresolved content.
+ * @param {ComponentRendering} [rendering] rendering data from Layout Service
+ * @returns {boolean} whether the datasource is present and valid
+ */
+function hasValidDatasource(rendering?: ComponentRendering): boolean {
+ if (!rendering?.dataSource) {
+ return false;
+ }
+
+ return rendering.isContentResolved !== false;
+}
+
+/**
+ * Checks whether a Sitecore datasource is present and valid, then renders appropriately depending on page mode (normal vs editing).
+ * `isContentResolved: false` is treated the same as a missing datasource. If the property is omitted, the original presence check is used.
* @param {WithDatasourceCheckOptions} [options]
* @returns
- * The wrapped component, if a datasource is present.
- * A null component (in normal mode) or an error component (in editing mode), if a datasource is not present.
+ * The wrapped component, if a datasource is present and valid.
+ * A null component (in normal mode) or an error component (in editing mode), if a datasource is missing or failed to resolve.
+ * @example
+ * // Wrap once. Deleted/archived datasources (isContentResolved: false) use the same
+ * // fallback as a missing datasource: hide in normal mode, show an editing error in editing mode.
+ * const ContentBlock = (props) => {props.fields.heading}
;
+ * export default withDatasourceCheck()(ContentBlock);
+ *
+ * // Layout Service: { componentName: 'ContentBlock', dataSource: '{id}', isContentResolved: false }
+ * // → ContentBlock is not rendered; no extra app-level check is required.
*/
export function withDatasourceCheck(options?: WithDatasourceCheckOptions) {
return function withDatasourceCheckHoc(
@@ -35,7 +57,7 @@ export function withDatasourceCheck(options?: WithDatasourceCheckOptions) {
const { sitecoreContext } = useSitecoreContext();
const EditingError = options?.editingErrorComponent ?? DefaultEditingError;
- return props.rendering?.dataSource ? (
+ return hasValidDatasource(props.rendering) ? (
) : sitecoreContext.pageEditing ? (
diff --git a/packages/sitecore-jss/src/layout/models.ts b/packages/sitecore-jss/src/layout/models.ts
index a5d70b304f..d38c90c8a1 100644
--- a/packages/sitecore-jss/src/layout/models.ts
+++ b/packages/sitecore-jss/src/layout/models.ts
@@ -85,6 +85,12 @@ export interface ComponentParams {
export interface ComponentRendering {
componentName: string;
dataSource?: string;
+ /**
+ * `true` when Layout Service resolved this rendering's datasource content.
+ * `false` when resolution failed (for example because the item was deleted or archived).
+ * Omitted by older Layout Service versions; absence preserves existing behavior.
+ */
+ isContentResolved?: boolean;
uid?: string;
placeholders?: PlaceholdersData;
fields?: T;