Rendering Experiences

Using the Ninetailed SDK to render Experiments and Personalizations.

The <Experience> Component

The Ninetailed React-based SDKs provide an <Experience> component to wrap your existing React components you use to render your content. This is the most declarative way to render Ninetailed personalization and experiment content, and therefore the methodology that most Ninetailed users should adopt when able.

The <Experience> component functions wraps your existing React component. It automatically detects the properties needed from the wrapped component. The experiences prop accepts Ninetailed Experience content that has been appropriately transformed by the ExperienceMapper available from our Utility Libraries.

<Experience> Component Props

PropDescription

id

[Required] The CMS entry ID of the baseline

component

[Required] The React component that your baseline and variants will use to render. This can either be regular React component or a component that opts into React's forwardRef. See the Tracking Impressions of Experiences section for details.

experiences

[Required] An array of experience CMS entries mapped using the ExperienceMapper methods available from our Utility Libraries

{...baseline}

[Required] Any and all props that the function passed as the component prop needs to receive to render the baseline variant entry. This will depend entirely on the structure of your existing React component(s).

passthroughProps

[Optional] An object containing key-value pairs of props that should be sent to the component irrespective of which experience variant is selected. Props supplied here will overwrite those of the selected variant, so this is designed for non-content props like state or refs.

loadingComponent

[Optional] A custom component to show prior to the <Experience> component selecting a variant. This defaults to a transparent version of your component using the baseline props.

Example Use

These examples show working CMS data, our Utility Libraries, and the <Experience> component together in demonstrative examples. Your implementation will vary according to your existing React components and your data source. Consult the Utility Libraries documentation to know what data to fetch from your Content Source and how to transform the returned Experience entries to the format required by the <Experience> component.

These examples show fetching CMS data from within potentially deeply nested React components. In practice, you will likely fetch that data from higher within your rendering tree and pass it to components, especially when statically pre-rendering. However, the mapping exercises and use of the <Experience> component demonstrated remain the same no matter what rendering strategy you adopt.

YourExperience.(jsx|tsx)
// or '@ninetailed/experience.js-next', '@ninetailed/experience.js-gatsby'
import { Experience } from '@ninetailed/experience.js-react';
import { ExperienceMapper } from '@ninetailed/experience.js-utils'

// This function is assumed to return a single entry and all its supporting data, including referenced content, in their entirety
import { getCmsEntry } from '../api/yourEntryGetter'
import { YourComponent } from './YourComponent'

export const YourExperience = (cmsBaselineEntry) => {
  const baselineEntry = getCmsEntry(cmsBaselineEntry);
  const experiences = baselineEntry['nt_experiences']
  
  const mappedExperiences = (experiences || [])
  .map((experience) => {
    return {
      id: experience.id,
      name: experience.name
      type: experience.nt_type as 'nt_personalization' | 'nt_experiment'
      config: experience.nt_config,
      audience: {
        id: experience.nt_audience.nt_audience_id
      },
      variants: experience.variants.map((variant) => {
        return {
          id: variant.id, // Required
          // Map any other data from the variant required by your component
          ...variant,
          someComponentProp: variant.foo
        }
      })
    }
  })
  .filter((experience) => ExperienceMapper.isExperienceEntry(experience))
  .map((experience) => ExperienceMapper.mapExperience(experience));
  
  return (
    <Experience
      id={entry.id} // Required. The id of the BASELINE entry
      component={YourComponent} // Required. What to use to render the selected variant
      {...baselineEntry} // Any props your `component` above needs
      experiences={mappedExperiences} // Array of mapped experiences
  />);
};

Inline Personalization with Merge Tags

Ninetailed allows you to embed content placeholders into Rich Text Fields that can then be rendered client-side using information from the current visitor's profile. These dynamic placeholder entries are called Merge Tags, which can then be used as inline entries within a rich text field of your CMS entries.

The React-based SDKs provide a corresponding <MergeTag /> component that allow you to declaratively render the inlined Merge Tag entries.

While rendering Merge Tag entries embedded within Rich Text Fields is the most common use for merge tags, you simply pass the property accessor (using dot notation) of any Ninetailed profile property as the id of the MergeTag component.

components/RichText.js
import React from 'react';
import { INLINES } from '@contentful/rich-text-types';
import { documentToReactComponents} from '@contentful/rich-text-react-renderer';
// or `@ninetailed/experience.js-next', @ninetailed/experience.js-gatsby'
import { MergeTag } from '@ninetailed/experience.js-react';

export const renderRichText = (richTextDocument) => {
  return documentToReactComponents(richTextDocument, {
    renderNode: {
      [INLINES.EMBEDDED_ENTRY]: (node) => {
        if (node.data.target.sys.contentType.sys.id === 'nt_mergetag')) {
          return (
            <MergeTag 
              id={node.data.target.fields.nt_mergetag_id}
              fallback={node.data.target.fields.nt_fallback}
            />
          );
        }
      }
    },
  });
};

Tracking Impressions of Experiences

The <Experience> component needs to track when the markup it renders is present within the visitor's viewport, because this is the criteria used to fire impression events to any connected Ninetailed plugins.

Unless the component prop being passed to <Experience> is defined as a React forwardRef, the <Experience> component will insert an empty non-displaying <div> of class nt-cmp-marker immediately prior to the rendered component. It is this inserted element's intersection with the viewport is then tracked.

// Returned markup from the <Experience> component when passing a regular component
<div className="nt-cmp-marker" style="display: none !important">
<!-- Your rendered component content -->
<div>
    <!-- ... -->
</div>

Under the hood, the tracking component uses React's useRef to store the DOM node to track. See the React forwardRef documentation for more details.

Because of the use of useRef, it is important that the components you pass have some consistent parent element between re-renders. Conditionally rendering top-level elements in a component may cause tracking to become decoupled.

const SomeAsyncConditionalComponent = () => {
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // ... Do something like get async data
    setLoading(false);
  }, []);

  // This component conditionally renders a top-level element, so tracking might be lost
  return loading ? (
    <div>Some loading markup</div>
  ) : (
    <div>Some markup after loading</div>
  );
};

const YourExperience = (cmsBaselineEntry) => {
  // ... Filter and map as above
  
  return (
    <Experience
      id={entry.id}
      component={SomeAsyncConditionalComponent}
      {...baselineEntry}
      experiences={mappedExperiences}
  />);
};

Last updated