Experience SDK
Table of contents
- Overview
- Composed JavaScript SDKs
- Choose an SDK
- Install the SDK
- Send events
- Utility libraries
- Render Experiences
Overview
Ninetailed provides SDKs and plugins for modern web frameworks to enable you to harness the power of real-time personalization. All SDKs are written in Typescript to maximize your efficiency through code completion and compile-time analysis.
The Ninetailed SDKs provide abstracted means to communicate with the Ninetailed Experience API and the browser. It shortens integration times into JavaScript codebases. The SDKs handle:
- creating
page
,track
, andidentify
events and sending them to the Experience API - errors, retries and queuing
- caching the profile client-side via localStorage.
Composed JavaScript SDKs
Ninetailed's lowest-level SDK is the Shared SDK, @ninetailed/experience.js-shared
, whose primary function is to create an API Client that handles the details of working with the endpoints of the Experience API. It also exposes data objects, types, and methods applicable across development environments
Ninetailed's JavaScript SDK, @ninetailed/experience.js
, is composed from the Shared SDK. The principal function of the JavaScript SDK is to create a Ninetailed instance that stores and updates a profile in response to events. The JavaScript and Shared SDK form the foundations on top of which all other Ninetailed JavaScript-based SDKs are built.
Supported frameworks
For declarative front-end implementations, Ninetailed provides SDKs for:
Additionally, Ninetailed provides a Node.js SDK for interacting with the Experience API in server or edge runtimes.
Choose an SDK
We strongly recommend using the SDK available for your framework if one is available for the fastest, most declarative implemenation.
For most Ninetailed users, installing the appropriate React-based SDK, sending page
, track
, and identify
events to enhance user profiles, and using the <Experience>
component to render personalization and experimentcontent client-side will be sufficient.
However, you may need to arbitrarily access profile data or exercise more control over how experiences are rendered and tracked.
These needs usually arise when:
- you need more control within a React-based web application, like if you're edge- or server-side rendering experiences and/or using React Server Components; or
- you're not implementing Ninetailed within a web-based React project (e.g., Vue/Nuxt, Svelte or React Native).
In such scenarios, the API Client created by the Shared SDK serves as the best starting place for upserting profiles in server and edge runtimes, while the JavaScript SDK is provides declarative tracking behaviour to use in client-side code.
For any front-end integration that is not based on Javascript, explore our Experience API for complete freedom on where to integrate.
Install the SDK
Dependency installation
npm install @ninetailed/experience.js-react
# OR
yarn add @ninetailed/experience.js-react
npm install @ninetailed/experience.js-next
# OR
yarn add @ninetailed/experience.js-next
npm install @ninetailed/experience.js-gatsby
# OR
yarn add @ninetailed/experience.js-gatsby
npm install @ninetailed/experience.js
# OR
yarn add @ninetailed/experience.js
Create a Ninetailed instance
The core responsibility of the Experience SDKs is to define a Ninetailed
class instance based on your input configuration. The created instance is then responsible for:
- keeping track of the current profile and providing a hook into when the profile changes
- exposing declarative methods to interact with the Experience API
- providing extensibility to the instance through plugins
When working with the React, Next.js or Gatsby SDK, a Ninetailed instance will be created and made available to your application globally using a React context provider.
Add the <NinetailedProvider>
component to the top level of the application, so that the Ninetailed profile can be consumed from any child component.
import React from 'react';
import MyAppCode from '../myAppCode.jsx';
import { NinetailedProvider } from '@ninetailed/experience.js-react';
export const App = () => {
return (
<div id="my-app">
<NinetailedProvider
// REQUIRED. An API key uniquely identifying your Ninetailed account.
clientId="NINETAILED_API_KEY"
// === ALL OF THE FOLLOWING PROPS ARE OPTIONAL ===
// === DEFAULT VALUES ARE SHOWN ===
// Your Ninetailed environment, typically either "main" or "development"
environment="main"
// Add any plugin instances
plugins=[]
// Specify an amount of time (ms) that an <Experience /> component must be present in the viewport to register a component view
componentViewTrackingThreshold={2000}
// Specify a maximum amount of time (ms) to wait for an Experience API response before falling back to baseline content
requestTimeout={5000}
// Specify a locale to localize profile location information
locale="en-US"
// Specify an alternative Experience API base URL
url="https://experience.ninetailed.co"
// Set to to true ONLY if using an unindexed CMS
useSDKEvaluation=true
>
<MyAppCode />
</NinetailedProvider>
</div>
);
}
Add the <NinetailedProvider>
component to the top level of the application, so that the Ninetailed profile can be consumed from any child component.
The Next.js <NinetailedProvider>
also hooks into the Next's page router to automatically call ninetailed.page()
on every route change. Do not additionally call this method on your own, otherwise you will duplicate events.
import { NinetailedProvider } from '@ninetailed/experience.js-next';
export const App = ({component, pageProps}) => {
return (
<div id="my-app">
<NinetailedProvider
// REQUIRED. An API key uniquely identifying your Ninetailed account.
clientId="NINETAILED_API_KEY"
// === ALL OF THE FOLLOWING PROPS ARE OPTIONAL ===
// === DEFAULT VALUES ARE SHOWN ===
// Your Ninetailed environment, typically either "main" or "development"
environment="main"
// Add any plugin instances
plugins=[]
// Specify an amount of time (ms) that an <Experience /> component must be present in the viewport to register a component view
componentViewTrackingThreshold={2000}
// Specify a maximum amount of time (ms) to wait for an Experience API response before falling back to baseline content
requestTimeout={5000}
// Specify a locale to localize profile location information
locale="en-US"
// Specify an alternative Experience API base URL
url="https://experience.ninetailed.co"
// Set to to true ONLY if using an unindexed CMS
useSDKEvaluation=true
>
<Component {...pageProps} />
</NinetailedProvider>
</div>
);
}
}
Add the plugin to your plugins array in your gatsby-config.js|ts
file. By using the Gatsby JS plugin there's no need to configure the NinetailedProvider
as described for the React and Next.js SDKs.
The plugin automatically calls ninetailed.page()
on every route change. Do not additionally call this method on your own, otherwise you will duplicate events.
... // Your Other Gatsby configuration
plugins: [
... // Your existing Gatsby plugins
{
resolve: `@ninetailed/experience.js-gatsby`,
options: {
// REQUIRED. An API key uniquely identifying your Ninetailed account.
clientId: "NINETAILED_API_KEY",
// === ALL OF FOLLOWING PROPERTIRES ARE OPTIONAL ===
// === DEFAULT VALUES ARE SHOWN ===
// Your Ninetailed environment, typically either "main" or "development"
environment: "main",
// Add any plugin instances
ninetailedPlugins: [],
// Specify an amount of time (ms) that a component must be present in the viewport to register a component view
componentViewTrackingThreshold: 2000,
// Specify a maximum amount of time (ms) to wait for an Experience API response before falling back to baseline content
requestTimeout: 5000,
// Specify a locale to localize profile location information
locale: "en-US",
// Specify an alternative Experience API base URL
url: "https://experience.ninetailed.co",
// Set to to true ONLY if using an unindexed CMS
useSDKEvaluation: true
}
}
]
import { Ninetailed } from '@ninetailed/experience.js';
export const ninetailed = new Ninetailed(
{
// REQUIRED. An API key uniquely identifying your Ninetailed account.
clientId: "NINETAILED_API_KEY",
// OPTIONAL. Your Ninetailed environment, typically either "main" or "development"
environment: "main" // Default
},
// === THE FOLLOWING ARGUMENT AND ALL OF ITS PROPERTIRES ARE OPTIONAL ===
// === DEFAULT VALUES ARE SHOWN ===
{
// Add any plugin instances
plugins: [],
// Specify an amount of time (ms) that a component must be present in the viewport to register a component view
componentViewTrackingThreshold: 2000,
// Specify a maximum amount of time (ms) to wait for an Experience API response before falling back to baseline content
requestTimeout: 5000,
// Specify a locale to localize profile location information
locale: "en-US",
// Specify an alternative Experience API base URL
url: "https://experience.ninetailed.co"
// Set to to true ONLY if using an unindexed
useSDKEvaluation: true
}
);
Browser instance access
Installing the Experience SDK exposes several Ninetailed properties and methods on a window.ninetailed
object to facilitate testing and debugging.
Properties and methods | Description |
---|---|
page() , track() , and identify() |
The core tracking methods of Ninetailed. |
debug(arg: boolean) |
Turn on debug mode, which will output additional information about your experiences assignement to the console. |
plugins |
Access the methods of any plugins attached to the Ninetailed instance. |
profile |
Output the current profile state. |
reset() |
Discard the current profile. |
For a full description of instance properties and methods available to your application, consult the Ninetailed Instance documentation.
Send events
Ninetailed profiles are created and updated by sending events about that profile to the Experience API. Rather than interacting with the Experience API endpoints directly, the principal way to send events is to use the methods made available from the Ninetailed JavaScript SDK.
A Ninetailed class instance created by any of Ninetailed's SDKs composed from the JavaScript SDK provides three methods for sending events: page
, track
and identify
.
page
type Page = (properties?: Object) => Promise<void>;
A page
event indicates a user has viewed the current page. The SDK provides a context object describing the parameters of page that has been viewed including the referrer
, url
, path
, user-agent
and other properties to be consumed by the API.
While most of the time you will call this page
method with no arguments, you may optionally specify any properties you would like to associate with the page view event. This can be useful for when creating Audiences. For example, if the category to which viewed blog posts belong is not contained within the URL of the blog posts but you'd like to create an Audience of visitors who have viewed blog posts of a certain category a certain number of times, you can pass the category along in the argument:
page({'category': 'YOUR_BLOG_CATEGORY'})
page
event on every route change. If your application uses the React SDK or JavaScript SDK, you should manually implement a page
call on every route change.
track
type Track = (event: string, properties?: Object) => Promise<void>;
track
events are used to log specific named actions a user takes, like signup
or registered_for_newsletter
. Events can be named anything you like.
Track events may also accept a properties argument detailing additional information. For example, you might include the SKU and quantity of items on an add_to_cart
event.
track('add_to_cart', {sku: '9T41L', quantity: 1})
In addition to serving as an Audience rule, track
events are used to indicate the conversion events you want to measure in Experience Insights.
identify
type Identify = (uid: string, traits?: Traits) => Promise<void>;
Identify
events serve two purposes:
First, identify
allows you to add custom information, called traits, to a profile. Traits can be any attribute of interest about a customer. Any valid JSON is a valid trait. You can store any information of interest on profiles that you may want to use to segment users. For example, you might store a user's responses from a sign up form, a list of products they recently visited, or data from an upstream source of your customer data like a CRM or CDP.
identify('', { favoriteColor: "red" })
Second, identify
allows you to name or "alias" a profile such that it can be referenced by that same alias in the future. IDs stored within an analytics system, customer data platform, or e-commerce platform make ideal aliases.
identify('customer12345')
After aliasing a profile, you can reinstate the aliased profile on a different device or browser by calling identify
again using the same supplied alias. This merges the latest activity of the current profile with the profile at that alias. For personalizations and experiments served to logged in users, you will likely want to call identify
after each successful authentication.
The flexibility of identify
is powerful but should be used with caution. In particular, you should consider what privacy legislation your application needs to abide by, and whether that affects what data should not be stored as traits. For example, you probably would not want to store contact information (email addresses, phone numbers, etc.) as traits or use them as aliases.
Access event methods
The useNinetailed
hook provides the tracking functions page
, track
, and identify
. These methods will call the Experience API using the configuration parameters supplied to a <NinetailedProvider>
.
import React from 'react';
import { useNinetailed } from '@ninetailed/experience.js-react';
// or import { useNinetailed } from '@ninetailed/experience.js-next';
// or import { useNinetailed } from '@ninetailed/experience.js-gatsby';
export const myComponent = () => {
const { page, track, identify } = useNinetailed();
page();
identify('anAlias', {someTrait: "value"});
return (
<button
type="button"
onClick={() => { track('add_to_cart'); }}
>
Add to Cart
</button>
);
}
The page
, track
, and identify
methods are available directly on a Ninetailed class instance.
import { Ninetailed } from '@ninetailed/experience.js';
const ninetailed = new Ninetailed({ clientId: "NINETAILED_API_KEY"})
ninetailed.page();
ninetailed.track('myEvent')
ninetailed.identify('alias', {traitName: "traitValue"})
See the Ninetailed Instance documentation for a full list of configuration options and methods available from the JS SDK.
Utility libraries
Our Experience API utility libraries provide methods to map experience content to the format required by the <Experience>
component exported by our React, Next.js, and Gatsby SDKs.
Use the Contentful Utility SDK if you are are retrieving content and experiences using Contentful's client libraries that interface with the Contentful REST APIs, including:
- the Contentful Content Delivery API
- the Contentful Content Preview API
For all other sources, including the Contentful GraphQL API or your own internal content APIs, middleware, use the JavaScript Utility SDK and map your experiences to the type required by the ExperienceMapper
class methods.
JavaScript library usage
npm install @ninetailed/experience.js-utils
# OR
yarn add @ninetailed/experience.js-utils
You must map your fetched CMS Experience entries to a particular shape prior to transforming them with the ExperienceMapper
methods. The following examples show the format required in a .map
step prior to calling .filter
to remove ill-formatted entries.
import { ExperienceMapper } from '@ninetailed/experience.js-utils';
const mappedExperiences = (myEntry.nt_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
// If mapping for the Preview Plugin, this displays audience names
name: experience.nt_audience.nt_name
},
variants: experience.variants.map((variant) => {
return {
id: variant.id, // Required
// Map any other fields required by your components
...variant,
someComponentProp: variant.foo
}
})
}
})
.filter((experience) => ExperienceMapper.isExperienceEntry(experience))
.map((experience) => ExperienceMapper.mapExperience(experience));
Your exact query and mapping will vary depending on both your content model and the props required by the component you use to render your content. This example assumes a content model using a content type of page
that contains a field called sections
that can reference entries of type hero
.
It also shows using a lightweight GraphQL client library graphql-request
to make the API request, but any GraphQL client is suitable.
import { request, gql } from "graphql-request";
import { filterAndMapExperiences, mapAudiences } from "../lib/helpers";
const CONTENTFUL_HERO_QUERY = gql`
fragment SysId on Entry {
sys {
id
}
}
fragment HeroEntry on Hero {
...SysId
internalName
}
fragment NtExperienceFields on NtExperience {
...SysId
ntExperienceId
ntName
ntType
ntConfig
ntAudience {
ntAudienceId
}
}
fragment NinetailedHero on Hero {
...HeroEntry
ntExperiencesCollection(limit: 10) {
items {
...NtExperienceFields
ntVariantsCollection(limit: 10) {
items {
...HeroEntry
}
}
}
}
}
query NinetailedHeroQuery($heroEntryid: String!) {
page(id: $heroEntryid) {
...SysId
sectionsCollection(limit: 10) {
items {
...NinetailedHero
}
}
}
}
`;
export async function getHeroData(heroId) {
const data = await request(
`https://graphql.contentful.com/content/v1/spaces/${process.env.CTFL_SPACE_ID}`,
CONTENTFUL_HERO_QUERY,
heroId,
{
Authorization: `Bearer ${process.env.CTFL_API_KEY}`,
"Content-Type": "application/json",
}
);
return data;
}
import { ExperienceMapper } from '@ninetailed/experience.js-utils';
import { getHeroData } from 'api/yourDataFetcher';
const hero = await getHeroData('aHeroEntryId')
const mappedExperiences = (hero.ntExperiencesCollection?.items || [])
.map((experience) => {
return {
id: experience.ntExperienceId,
name: experience.ntName,
type: experience.ntType,
config: experience.ntConfig,
// This syntax accounts for the possibility of an audience not being set on an experiment
...(experience.ntAudience
? {
audience: {
id: experience.ntAudience.ntAudienceId
// If mapping for the Preview Plugin, this displays audience names
name: experience.ntAudience.ntName
},
}
} : {})
variants: experience.ntVariantsCollection.items.map((variant) => {
return {
id: variant.sys.id, // Required
// Map any other fields required by your rendering component
...variant
}
})
}
})
.filter((experience) => ExperienceMapper.isExperienceEntry(experience))
.map((experience) => ExperienceMapper.mapExperience(experience));
Notice the use of fragments to capture the sys.id, since this is required on the Ninetailed Experience (NtExperience
) entry as well as all variants referenced by the entry. Additionally, note the use of a fragment to isolate the fields of the Experience entry so that the base HeroEntry
fragment can be used to query both the baseline and the variant content without introducing a circular reference.
Contentful library usage
npm install @ninetailed/experience.js-utils-contentful
# OR
yarn add @ninetailed/experience.js-utils-contentful
import { ExperienceMapper } from '@ninetailed/experience.js-utils-contentful'
import { createClient } from 'contentful';
const client = createClient({
accessToken: 'youtAccessToken',
space: 'yourSpaceId'
})
// Specify what entries with Ninetailed Experience references to get from Contentful
const query = {...}
const rawEntries = await client.getEntries(query);
// Extract one entry, as an example
const [yourEntry] = rawEntries.items
// Filter and map with ExperienceMapper methods
const experiences = (yourEntry.fields.nt_experiences || [])
.filter(ExperienceMapper.isExperienceEntry)
.map(ExperienceMapper.mapExperience)
See also our Contentful + Next.js example project for context.
ExperienceMapper class methods
isExperienceEntry
Determines if a provided entry is of valid type to be consumed by mapExperience
. Use with .filter
to remove any invalidly typed experiences.
mapExperience
Transform an experience to the type required by the <Experience>
component.
isExperimentEntry
Determines if a provided entry is of valid type to be consumed by mapExperiment
. Use with .filter
to remove any invalidly typed experiments.
mapExperiment
Transform an experiment to the type required by the React and Next.js <NinetailedProvider>
experiments
prop.
mapCustomExperience
This class method is for the Contentful library only. If you need to modify how the variants referenced by an experience entry retrieved from Contentful are mapped, use this method to pass a custom variant mapping function. Example usage:
const experiences = myExperience.fields.nt_experiences
.filter(ExperienceMapper.isExperienceEntry)
.map((experience) => {
ExperienceMapper.mapCustomExperience(experience, (variant) => {
id: variant.sys.id // required
// Add any data required by your `component` prop on the <Experience> component
...variant.fields,
someComponentProp: variant.foo
});
})
mapCustomExperienceAsync
This class method is for the Contentful library only, SDK >= 7.7.x. Similar to mapCustomExperience
, but allows asynchronous operations to be executed in the variant mapping step.
const experiences = myExperience.fields.nt_experiences
.filter(ExperienceMapper.isExperienceEntry)
.map((experience) => {
ExperienceMapper.mapCustomExperienceAsync(experience, async (variant) => {
await new Promise ((resolve) => setTimeout(resolve, 1000)); // Simulated delay supported by async handler
return {
id: variant.sys.id // required
// Add any data required by your `component` prop on the <Experience> component
...variant.fields,
someComponentProp: variant.foo
}
});
})
mapBaselineWithExperiences
This class method is for the Contentful library only. Supply an object representing a baseline entry and it's attached experiences and return an array of filtered and mapped experiences.
Render Experiences
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 |
---|---|
{...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). |
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 . |
experiences | [Required] An array of experience CMS entries mapped using the ExperienceMapper methods available from our utility libraries |
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.
// 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
{...baselineEntry} // Any props your `component` above needs
id={entry.id} // Required. The id of the BASELINE entry
component={YourComponent} // Required. What to use to render the selected variant
experiences={mappedExperiences} // Array of mapped experiences
/>);
};
// or '@ninetailed/experience.js-next', '@ninetailed/experience.js-gatsby'
import { Experience } from '@ninetailed/experience.js-react';
// For use with Contentful REST APIs only
import { 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'
export const YourExperience = (cmsBaselineEntry) => {
const baselineEntry = getContentfulEntry(cmsBaselineEntry);
const experiences = baselineEntry.fields.nt_experiences;
const mappedExperiences = experiences
.filter((experience) => ExperienceMapper.isExperienceEntry(experience))
.map((experience) => ExperienceMapper.mapExperience(experience))
return (
<Experience
{...baselineEntry} // Any props your `component` above needs
id={baselineEntry.sys.id} // Required. The sys.id of the BASELINE entry
component={YourComponent} // Required. What to use to render the selected variant
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.
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}
/>
);
}
}
},
});
};
import React from 'react';
// or `@ninetailed/experience.js-next', @ninetailed/experience.js-gatsby'
import { MergeTag } from '@ninetailed/experience.js-react';
const Greeting = () => {
return (
<>
<p>Welcome back, <MergeTag id="traits.firstName" fallback="you" />
<p>How is <MergeTag id="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. 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 that 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 component
s 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}
/>);
};
const SomeAsyncConditionalComponent = () => {
const [loading, setLoading] = useState(true);
useEffect(() => {
// ... Do something like get async data
setLoading(false);
}, []);
// Wrapping an element around the conditional rendering to preserve tracking
return (
<div>
(loading ? (<div>Some loading markup</div>) : (
<div>Some markup after loading</div>) )
</div>
);
};
const YourExperience = (cmsBaselineEntry) => {
// ... Filter and map as above
return (
<Experience
id={entry.id}
component={SomeAsyncConditionalComponent}
{...baselineEntry}
experiences={mappedExperiences}
/>);
};