@crossingminds/beam-react
v0.11.9
Published
Utilities for adding Crossing Minds recommendations to React apps.
Downloads
9,864
Readme
Crossing Minds - Beam - React
Reusable React hooks and utilities for interacting with the Crossing Minds API. Beam is a customized recommendation platform, learn more here: https://www.crossingminds.com/beam
This library works with many flavors of React, including Remix-run, Shopify Hydrogen, and Next JS.
Type Documentation
For the most up-to-date information on exports and interfaces, please visit TS Docs.
Note: This documentation is currently in beta and works most of the time.
Prerequisites
Account details
You'll need the following Crossing Minds account details:
- A service account
serviceLoginId
with afrontend
role - A password/key for the service account
- A
databaseId
in the same organization as your service account.
If you don't have a service account, you can add one in the Crossing Minds dashboard, or contact Crossing Minds directly.
individual or team accounts cannot be used with this library, only service accounts.
By default only accounts with a frontend
role can be used with this library. This is because this library assumes lenient permissions where the password/key of frontend accounts is not secret, allowing for easier auth implementation.
If you need strict permissions you will have to handle logins and JWT tokens from your backend instead.
SessionId
sessionId
s are required to use most parts of this library. As the name suggests, a sessionId
is a unique id that is assigned to an end-user of your product. Every end-user must be assigned a unique sessionId
. It must be a UUID, and it should remain consistent for that end-user across server and client/browser/app for as long as your product defines a session. You are responsible for generating and managing your app's session ids.
Note, userId
s are different than sessionId
s. userId
s are only assigned to known end-users, and remain consistent for the lifetime of the end-user (permanent). sessionId
s are assigned to all end-users including anonymous end-users.
Installation
Add this library to your react application using the package manager CLI of your choice (we like pnpm). Typescript types are included.
npm install @crossingminds/beam-react
Exports
getItemRecommendations
Note: This function is currently in beta.
Retrieve item recommendations based on generic input. This functionality allows for testing various recommendation types without the need for code deployment.
/** Get item recommendations from a generic input.
* @beta
* @throws
*
* Supported recommendation types: item_to_items, profile_to_items, session_to_items, profile_to_items_w_ctx_items, session_to_items_w_ctx_items, precomputed_item_to_items, precomputed_profile_to_items, item_properties_to_items
* Supported Generic Inputs: ItemToItems, UserToItems, SessionToItems, UserToItemsWithContext, SessionToItemsWithContext, PrecomputedItemToItems, PrecomputedUserToItems, ItemPropertiesToItems
* Authenticates: https://docs.api.crossingminds.com/endpoints/account.html#login-as-service-account
* Gets Item Recommendations from Generic Input: https://docs.api.crossingminds.com/endpoints/reco.html#get-items-recommendations-from-generic-input */
type GetItemRecommendationsParam<
T extends keyof ItemRecommendationInputs | undefined
> = {
databaseId: string
password: string
serviceLoginId: string
clientOptions?:
| Omit<RecommendationClientOptions, 'initialCredentials'>
| undefined
} & GetGenericItemRecommendationsInput<T>
This function's input parameters depend on the generic T
type parameter. T corresponds to recommendation types, and follows the options below:
- item_to_items -> ItemToItems
- profile_to_items -> UserToItems
- session_to_items -> SessionToItems
- profile_to_items_w_ctx_items -> UserToItemsWithContext
- session_to_items_w_ctx_items -> SessionToItemsWithContext
- precomputed_item_to_items -> PrecomputedItemToItems
- precomputed_profile_to_items -> PrecomputedUserToItems
- item_properties_to_items -> ItemPropertiesToItems
Example:
const recommendations = await getItemRecommendations<'ItemToItems' | 'PrecomputedItemToItems'>({
databaseId: 'your-database-id',
serviceLoginId: 'your-service-login-id',
password: 'your-service-password',
itemId: 'your-item-id'
})
Not specifying a recommendation type is the most flexible option and accepts all potential parameters. Parameters that are not applicable will be ignored silently.
getItemBasedRecommendations
Get recommendations for items based on an item id.
/** Either a userId or a sessionId must be provided.
* If a `userId` and a `sessionId` are provided, the `userId` will be used.
* Authenticates: https://docs.api.crossingminds.com/endpoints/account.html#login-as-service-account
* Gets related-item recommendations: https://docs.api.crossingminds.com/endpoints/reco.html#get-similar-items-recommendations */
type getRelatedItemRecommendationsParam = {
databaseId: string
password: string
serviceLoginId: string
scenario: string | typeof SCENARIO_OMITTED
clientOptions?: OptimizedInputProperties['clientOptions'] | undefined
itemId: GetRelatedItemRecommendationsInput['itemId']
maxResults?: number | undefined
cursor?: string | undefined
filters?: GetRelatedItemRecommendationsInput['filters'] | undefined
reranking?: GetRelatedItemRecommendationsInput['reranking'] | undefined
skipDefaultScenario?: boolean | undefined
userId?: string | undefined
sessionId?: string | undefined
} & (
| { userId?: string | undefined; sessionId: string }
| { userId: string; sessionId?: string | undefined }
)
getPrecomputedItemBasedRecommendations
Get precomputed recommendations for items based on an item id. This function is similar to getItemBasedRecommendations
, but it uses precomputed recommendations instead of calculating them on the fly. This can be useful for performance or consistency.
/** Either a userId or a sessionId must be provided.
* If a `userId` and a `sessionId` are provided, the `userId` will be used.
* Authenticates: https://docs.api.crossingminds.com/endpoints/account.html#login-as-service-account
* Gets precomputed item-based recommendations: https://docs.api.crossingminds.com/endpoints/reco.html#get-precomputed-similar-items-recommendations */
type GetPrecomputedItemBasedRecommendationsParam = {
databaseId: string
password: string
serviceLoginId: string
scenario: string
clientOptions?: OptimizedInputProperties['clientOptions'] | undefined
itemId: GetPrecomputedRelatedItemRecommendationsInput['itemId']
maxResults?: number | undefined
skipDefaultScenario?: boolean | undefined
userId?: string | undefined
sessionId?: string | undefined
} & (
| { userId?: string | undefined; sessionId: string }
| { userId: string; sessionId?: string | undefined }
)
getPersonalizedRecommendations
Gets either sessionId or userId based recommendations.
/** Either a `userId` or a `sessionId` must be provided.
* If both a `userId` and a `sessionId` are provided, user-based recommendations will be returned.
* Authenticates: https://docs.api.crossingminds.com/endpoints/account.html#login-as-service-account
* Gets session-based recommendations: https://docs.api.crossingminds.com/endpoints/reco.html#get-session-based-items-recommendations
* Gets user-based recommendations: https://docs.api.crossingminds.com/endpoints/reco.html#get-profile-based-items-recommendations
* @throws */
type GetPersonalizedRecommendationsParam = {
databaseId: string
password: string
serviceLoginId: string
authLocalStorageKey?:
| OptimizedInputProperties['authLocalStorageKey']
| undefined
contextItems?:
| GetSessionWithContextItemBasedItemRecommendationsInput['contextItems']
| GetUserWithContextItemBasedItemRecommendationsInput['contextItems']
| undefined
clientOptions?: OptimizedInputProperties['clientOptions'] | undefined
cursor?: string | undefined
excludeRatedItems?: boolean | undefined
filters?: GetUserBasedItemRecommendationsInput['filters'] | undefined
maxResults?: number | undefined
reranking?: GetUserBasedItemRecommendationsInput['reranking'] | undefined
sessionId?: string | undefined
sessionScenario?: Scenario | undefined
sessionWithContextScenario?: Scenario | undefined
skipDefaultScenario?: boolean | undefined
userId?: string | undefined
userScenario?: Scenario | undefined
userWithContextScenario?: Scenario | undefined
}
useRecordItemInteractions
Hook that returns a set of functions used to record end-user interactions with items. Interactions are used to improve recommendations.
/** Either a `sessionId` or a `userId ` must be provided.
* If both a `sessionId` and a `userId` are provided, the interaction will be recorded for the user AND the session will be "resolved".
* Resolved sessions should not be used again to get recommendations or record interactions. */
type UseRecordItemInteractionsParam = {
databaseId: string
password: string
serviceLoginId: string
clientOptions?: OptimizedInputProperties['clientOptions'] | undefined
authLocalStorageKey?:
| OptimizedInputProperties['authLocalStorageKey']
| undefined
/** @default 'browser' */
runMode?: 'browser' | 'server' | 'always' | 'never' | undefined
} & PipelineOptions
type useRecordItemInteractionsOutput = {
recordCustomItemInteraction: (input: {
itemId: string
interactionType: string
properties?: SerializableObject | undefined
}) => void
recordAddItemToCartInteraction: (itemId: string) => void
recordItemClickInteraction: (itemId: string) => void
recordItemPageViewInteraction: (itemId: string) => void
recordItemTransactionCompleteInteraction: (itemId: string) => void
}
Hook that returns a set of functions used to record user interactions. Interactions are used to improve recommendations.
resolveSession
Associates a sessionId with a userId, "resolving" the session. This is critical for statistical analysis of logged-in users. If possible, this should only be called once per sessionId/userId pair.
type ResolveSessionIdParam = {
sessionId: string
userId: string
} & OptimizedInputProperties
type OptimizedInputProperties
Properties included in input params of most functions. If these remain the same across multiple calls, some performance optimizations will be applied.
type OptimizedInputProperties = {
serviceLoginId: LoginServiceInput['serviceLoginId']
password: LoginServiceInput['password']
databaseId: NonNullable<LoginServiceInput['databaseId']>
sessionId?: NonNullable<LoginServiceInput['sessionId']>
userId?: LoginServiceInput['userId']
/** Auth tokens are stored in local storage by default.
* Providing your own localStorage key (property name) is recommended.
* Only one set of auth tokens is stored per key. If you have multiple auth configurations, provide a different key for each.
* If you don't want to use localStorage, set this to the `MEMORY_CACHE_ONLY` constant (exported by this package). */
authLocalStorageKey?: AuthLocalStorageKey | undefined
clientOptions?:
| Partial<Omit<RecommendationClientOptions, 'initialCredentials'>>
| undefined
}
Usage
Remix-run, Shopify Hydrogen with Oxygen (server-side)
With Remix and Hydrogen apps, we recommend using a loader
function to fetch recommendations server-side. In this example we'll use Oxygen as our host, but you can use any host you'd like (example: import { json } from "@remix-run/node"
). To add recommendations to a page, it only takes one function call, either getPersonalizedRecommendations
for userId and sessionId based recommendations, or getItemBasedRecommendations
for itemId based recommendations.
As mentioned in the SessionId section, you're responsible for managing your own SessionIds. These ids are extremely important for personalizing recommendations. The Hydrogen framework allows you to pass a session
object into your loader function. Please see the Hydrogen docs for more information on how to use sessions.
If you'd like to get recommendations client-side in a Remix app, we recommend following the React Query example in the next section.
import {
getPersonalizedRecommendations,
type OptimizedInputProperties,
} from "@crossingminds/beam-react";
import { json, type LoaderArgs } from "@shopify/remix-oxygen";
import { useLoaderData } from "@remix-run/react";
/* Crossing Minds account details
This object is best stored and exported from common location and used throughout your app.
You'll need it whenever you get new recommendations or record interactions. */
const beamReactOptions = {
databaseId: "UnIqUeIdOfMyDb",
password: "really-more-of-an-api-key-than-a-password-1-!",
serviceLoginId: "frontend-service-id-for-my-account",
} satisfies OptimizedInputProperties;
/* This is a typical loader function for Remix apps that will fetch the recommendations on the server
https://shopify.dev/docs/custom-storefronts/hydrogen/data-fetching/fetch-data#load-data-and-query-the-storefront-api */
export async function loader({ params, context: { session } }: LoaderArgs) {
const { itemIds } = await getPersonalizedRecommendations({
...beamReactOptions,
sessionId: session.session.id,
});
/* We'll return our recommendations and our sessionId to the page component */
return json({
itemIds,
sessionId,
});
}
export default function Page() {
/* Import the data from the server into your page component */
const { itemIds, sessionId } = useLoaderData<typeof loader>();
/* Use the itemIds to display your recommendations
We're also passing in the sessionId so that we can record interactions with the items to make future recommendations better */
return <BYOProductDisplay itemIds={itemIds} sessionId={sessionId} />;
}
React Query (client-side)
When fetching recommendations in a client (browser) environment, we recommend using React Query. Here's an example of getting item-based recommendations on a product page.
import React, { Fragment } from "react";
import { useQuery } from "@tanstack/react-query";
import { getItemBasedRecommendations } from "@crossingminds/beam-react";
import type { OptimizedInputProperties } from "@crossingminds/beam-react";
/* Crossing Minds account details */
const inputProperties = {
databaseId: "UnIqUeIdOfMyDb",
serviceLoginId: "frontend-service-id-for-my-account",
password: "really-more-of-an-api-key-than-a-password-1-!",
} satisfies Partial<OptimizedInputProperties>;
interface ProductPageProps {
itemId: string;
sessionId: string;
}
/** Display item based recommendations */
export function ProductPage({ itemId, sessionId }: ProductPageProps) {
const { data, isLoading, isError } = useQuery({
queryKey: ["itemRecommendations", itemId, sessionId, inputProperties],
retry: false, // getItemBasedRecommendations will retry internally
refetchOnMount: true, // Fetch on every page view as the recommendations are dynamic
staleTime: Infinity, // No need to refetch unless the page is reloaded
refetchOnWindowFocus: false, // No need to refetch unless the page is reloaded
cacheTime: 60 * 1000, // Recommendations will likely change as new interactions are recorded. This value will be based on the loading UX you're going for.
async queryFn() {
const { itemIds } = await getItemBasedRecommendations({
...inputProperties,
itemId,
sessionId,
});
return itemIds;
},
});
return (
/* This library does not include any UI. You'll be responsible for you own item display */
<ProductDisplay itemIds={data} isLoading={isLoading} isError={isError} />
);
}
Resolve a session
When both a userId
and a sessionId
are available, the session should be "resolved". This means that the session and user are linked. This is important for personalizing recommendations, as well as data analysis and reporting.
This can be done on the server or client. It's OK to continue using a sessionId
after it has been resolved.
import { resolveSession } from "@crossingminds/beam-react";
...
if (sessionId && userId) {
const isSessionResolved = await resolveSession({
sessionId,
userId,
serviceLoginId,
password,
databaseId,
});
if (sessionIsResolved) {
/*
If possible, only call resolveSession once per sessionId.
The function returns `true` when successful.
You can then set a cookie, sessionStorage or localStorage value to avoid calling resolveSession again.
*/
}
}