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
Prop
Description
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'exportconstYourExperience= (cmsBaselineEntry) => {constbaselineEntry=getCmsEntry(cmsBaselineEntry);constexperiences= baselineEntry['nt_experiences']constmappedExperiences= (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 ( <Experienceid={entry.id} // Required. The id of the BASELINE entrycomponent={YourComponent} // Required. What to use to render the selected variant {...baselineEntry} // Any props your `component` above needsexperiences={mappedExperiences} // Array of mapped experiences />);};
YourExperience.(jsx|tsx)
// or '@ninetailed/experience.js-next', '@ninetailed/experience.js-gatsby'import { Experience } from'@ninetailed/experience.js-react';// For use with Contentful REST APIs onlyimport { ExperienceMapper } from'@ninetailed/experience.js-utils-contentful'// This function is assumed to return a single entry and all its nested references from the REST Contentful CDA in their entirety
import { getContentfulEntry } from'../api/yourEntryGetter'import { YourComponent } from'./YourComponent'exportconstYourExperience= (cmsBaselineEntry) => {constbaselineEntry=getContentfulEntry(cmsBaselineEntry);constexperiences=baselineEntry.fields.nt_experiences;constmappedExperiences= experiences.filter((experience) =>ExperienceMapper.isExperienceEntry(experience)).map((experience) =>ExperienceMapper.mapExperience(experience))return ( <Experienceid={baselineEntry.sys.id} // Required. The sys.id of the BASELINE entrycomponent={YourComponent} // Required. What to use to render the selected variant {...baselineEntry} // Any props your `component` above needsexperiences={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.
import React from'react';// or `@ninetailed/experience.js-next', @ninetailed/experience.js-gatsby'import { MergeTag } from'@ninetailed/experience.js-react';constGreeting= () => {return ( <> <p>Welcome back, <MergeTagid="traits.firstName"fallback="you" /> <p>How is <MergeTagid="location.city"fallback="your city" /> this time of year?</p> </>};
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<divclassName="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.
constSomeAsyncConditionalComponent= () => {const [loading,setLoading] =useState(true);useEffect(() => {// ... Do something like get async datasetLoading(false); }, []);// This component conditionally renders a top-level element, so tracking might be lostreturn loading ? ( <div>Some loading markup</div> ) : ( <div>Some markup after loading</div> );};constYourExperience= (cmsBaselineEntry) => {// ... Filter and map as abovereturn ( <Experienceid={entry.id}component={SomeAsyncConditionalComponent} {...baselineEntry}experiences={mappedExperiences} />);};
constSomeAsyncConditionalComponent= () => {const [loading,setLoading] =useState(true);useEffect(() => {// ... Do something like get async datasetLoading(false); }, []);// Wrapping an element around the conditional rendering to preserve trackingreturn ( <div> (loading ? (<div>Some loading markup</div>) : ( <div>Some markup after loading</div>) ) </div> );};constYourExperience= (cmsBaselineEntry) => {// ... Filter and map as abovereturn ( <Experienceid={entry.id}component={SomeAsyncConditionalComponent} {...baselineEntry}experiences={mappedExperiences} />);};