@nosto/nosto-react 0.5.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
- import React, { useEffect, isValidElement, useState, useRef } from "react"
2
- import { NostoContext } from "./context"
3
- import { createRoot, Root } from "react-dom/client"
4
- import { NostoClient, Recommendation } from "../types"
1
+ import React, { useEffect, isValidElement } from "react"
2
+ import { NostoContext, RecommendationComponent } from "../context"
3
+ import { NostoClient } from "../types"
4
+ import { isNostoLoaded } from "./helpers"
5
5
 
6
6
  /**
7
7
  * @group Components
@@ -19,7 +19,10 @@ export interface NostoProviderProps {
19
19
  * Indicates an url of a server
20
20
  */
21
21
  host?: string
22
- children: React.ReactElement
22
+ /**
23
+ * children
24
+ */
25
+ children: React.ReactElement | React.ReactElement[]
23
26
  /**
24
27
  * Indicates if merchant uses multiple currencies
25
28
  */
@@ -27,9 +30,7 @@ export interface NostoProviderProps {
27
30
  /**
28
31
  * Recommendation component which holds nostoRecommendation object
29
32
  */
30
- recommendationComponent?: React.ReactElement<{
31
- nostoRecommendation: Recommendation
32
- }>
33
+ recommendationComponent?: RecommendationComponent
33
34
  /**
34
35
  * Enables Shopify markets with language and market id
35
36
  */
@@ -67,7 +68,7 @@ export default function NostoProvider(props: NostoProviderProps) {
67
68
  host,
68
69
  children,
69
70
  recommendationComponent,
70
- shopifyMarkets,
71
+ shopifyMarkets
71
72
  } = props
72
73
  const [clientScriptLoadedState, setClientScriptLoadedState] = React.useState(false)
73
74
  const clientScriptLoaded = React.useMemo(() => clientScriptLoadedState, [clientScriptLoadedState])
@@ -78,61 +79,6 @@ export default function NostoProvider(props: NostoProviderProps) {
78
79
  // Set responseMode for loading campaigns:
79
80
  const responseMode = isValidElement(recommendationComponent) ? "JSON_ORIGINAL" : "HTML"
80
81
 
81
- // RecommendationComponent for client-side rendering:
82
- function RecommendationComponentWrapper(props: { nostoRecommendation: Recommendation }) {
83
- return React.cloneElement(recommendationComponent!, {
84
- // eslint-disable-next-line react/prop-types
85
- nostoRecommendation: props.nostoRecommendation,
86
- })
87
- }
88
-
89
- // custom hook for rendering campaigns (CSR/SSR):
90
- const [pageType, setPageType] = useState("")
91
- function useRenderCampaigns(type: string = "") {
92
- const placementRefs = useRef<Record<string, Root>>({})
93
- useEffect(() => {
94
- if (pageType !== type) {
95
- setPageType(type)
96
- }
97
- }, [])
98
-
99
- const pageTypeUpdated = type === pageType
100
-
101
- function renderCampaigns(
102
- data: {
103
- recommendations: Record<string, Recommendation>
104
- campaigns: {
105
- recommendations: Record<string, Recommendation>
106
- }
107
- },
108
- api: NostoClient
109
- ) {
110
- if (responseMode == "HTML") {
111
- // inject content campaigns as usual:
112
- api.placements.injectCampaigns(data.recommendations)
113
- } else {
114
- // render recommendation component into placements:
115
- const recommendations = data.campaigns.recommendations
116
- for (const key in recommendations) {
117
- const recommendation = recommendations[key]
118
- const placementSelector = "#" + key
119
- const placement = () => document.querySelector(placementSelector)
120
-
121
- if (placement()) {
122
- if (!placementRefs.current[key]) placementRefs.current[key] = createRoot(placement()!)
123
- const root = placementRefs.current[key]!
124
- root.render(
125
- <RecommendationComponentWrapper
126
- nostoRecommendation={recommendation}
127
- ></RecommendationComponentWrapper>
128
- )
129
- }
130
- }
131
- }
132
- }
133
- return { renderCampaigns, pageTypeUpdated }
134
- }
135
-
136
82
  useEffect(() => {
137
83
  if (!window.nostojs) {
138
84
  window.nostojs = (cb: (api: NostoClient) => void) => {
@@ -141,7 +87,7 @@ export default function NostoProvider(props: NostoProviderProps) {
141
87
  window.nostojs(api => api.setAutoLoad(false))
142
88
  }
143
89
 
144
- if (!document.querySelectorAll("[nosto-client-script]").length && !shopifyMarkets) {
90
+ if (!isNostoLoaded() && !shopifyMarkets) {
145
91
  const script = document.createElement("script")
146
92
  script.type = "text/javascript"
147
93
  script.src = "//" + (host || "connect.nosto.com") + "/include/" + account
@@ -209,9 +155,7 @@ export default function NostoProvider(props: NostoProviderProps) {
209
155
  clientScriptLoaded,
210
156
  currentVariation,
211
157
  responseMode,
212
- recommendationComponent,
213
- useRenderCampaigns,
214
- pageType,
158
+ recommendationComponent
215
159
  }}
216
160
  >
217
161
  {children}
@@ -1,5 +1,4 @@
1
- import { useNostoContext } from "./context"
2
- import { useNostoApi } from "../utils/hooks"
1
+ import { useRenderCampaigns, useNostoApi } from "../hooks"
3
2
 
4
3
  /**
5
4
  * You can personalise your search pages by using the NostoSearch component.
@@ -26,9 +25,7 @@ import { useNostoApi } from "../utils/hooks"
26
25
  */
27
26
  export default function NostoSearch(props: { query: string; placements?: string[] }) {
28
27
  const { query, placements } = props
29
- const { recommendationComponent, useRenderCampaigns } = useNostoContext()
30
-
31
- const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("search")
28
+ const { renderCampaigns } = useRenderCampaigns()
32
29
 
33
30
  useNostoApi(
34
31
  async (api) => {
@@ -36,19 +33,9 @@ export default function NostoSearch(props: { query: string; placements?: string[
36
33
  .viewSearch(query)
37
34
  .setPlacements(placements || api.placements.getPlacements())
38
35
  .load()
39
- renderCampaigns(data, api)
36
+ renderCampaigns(data)
40
37
  },
41
- [query, recommendationComponent, pageTypeUpdated]
42
- )
43
-
44
- return (
45
- <>
46
- <div className="nosto_page_type" style={{ display: "none" }}>
47
- search
48
- </div>
49
- <div className="nosto_search" style={{ display: "none" }}>
50
- {query}
51
- </div>
52
- </>
38
+ [query]
53
39
  )
40
+ return null
54
41
  }
@@ -1,7 +1,6 @@
1
- import { useNostoContext } from "./context"
1
+ import { useNostoContext, useDeepCompareEffect } from "../hooks"
2
2
  import { Cart, Customer } from "../types"
3
3
  import { snakeize } from "../utils/snakeize"
4
- import { useDeepCompareEffect } from "../utils/hooks"
5
4
 
6
5
  /**
7
6
  * Nosto React requires that you pass it the details of current cart contents and the details of the currently logged-in customer, if any, on every route change.
@@ -13,8 +12,8 @@ import { useDeepCompareEffect } from "../utils/hooks"
13
12
  *
14
13
  * @group Essential Functions
15
14
  */
16
- export default function NostoSession(props: { cart: Cart; customer: Customer }) {
17
- const { cart, customer } = props
15
+ export default function NostoSession(props?: { cart?: Cart; customer?: Customer }) {
16
+ const { cart, customer } = props ?? {}
18
17
  const { clientScriptLoaded } = useNostoContext()
19
18
 
20
19
  useDeepCompareEffect(() => {
@@ -0,0 +1,3 @@
1
+ export function isNostoLoaded() {
2
+ return typeof window.nosto !== "undefined"
3
+ }
@@ -8,7 +8,4 @@ export { default as NostoOrder } from "./NostoOrder"
8
8
  export { default as NostoHome } from "./NostoHome"
9
9
  export { default as NostoPlacement } from "./NostoPlacement"
10
10
  export { default as NostoProvider } from "./NostoProvider"
11
-
12
- export { NostoContext, useNostoContext } from "./context"
13
- export type { NostoContextType } from "./context"
14
11
  export { default as NostoSession } from "./NostoSession"
package/src/context.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { createContext } from "react"
2
+ import { Recommendation, RenderMode } from "./types"
3
+
4
+ type AnyFunction = (...args: unknown[]) => unknown
5
+
6
+ export type RecommendationComponent = React.ReactElement<{
7
+ nostoRecommendation: Recommendation
8
+ }>
9
+
10
+ /**
11
+ * @group Types
12
+ */
13
+ export interface NostoContextType {
14
+ account: string
15
+ clientScriptLoaded: boolean
16
+ currentVariation?: string
17
+ renderFunction?: AnyFunction
18
+ responseMode: RenderMode
19
+ recommendationComponent?: RecommendationComponent
20
+ }
21
+
22
+ /**
23
+ * @group Essential Functions
24
+ */
25
+ export const NostoContext = createContext<NostoContextType>({
26
+ account: "",
27
+ currentVariation: "",
28
+ responseMode: "HTML",
29
+ clientScriptLoaded: false
30
+ })
31
+
@@ -0,0 +1,4 @@
1
+ export { useDeepCompareEffect } from "./useDeepCompareEffect"
2
+ export { useNostoApi } from "./useNostoApi"
3
+ export { useNostoContext } from "./useNostoContext"
4
+ export { useRenderCampaigns } from "./useRenderCampaigns"
@@ -0,0 +1,21 @@
1
+ import { useEffect, useRef, useMemo } from "react"
2
+ import { deepCompare } from "../utils/compare"
3
+
4
+ export function useDeepCompareEffect(
5
+ callback: Parameters<typeof useEffect>[0],
6
+ dependencies: Parameters<typeof useEffect>[1]
7
+ ): ReturnType<typeof useEffect> {
8
+ return useEffect(callback, useDeepCompareMemoize(dependencies))
9
+ }
10
+
11
+ function useDeepCompareMemoize<T>(value: T) {
12
+ const ref = useRef<T>(value);
13
+ const signalRef = useRef<number>(0)
14
+
15
+ if (!deepCompare(value, ref.current)) {
16
+ ref.current = value
17
+ signalRef.current += 1
18
+ }
19
+
20
+ return useMemo(() => ref.current, [signalRef.current])
21
+ }
@@ -0,0 +1,22 @@
1
+ import { useEffect } from "react"
2
+ import { useNostoContext } from "./useNostoContext"
3
+ import { NostoClient } from "../types"
4
+ import { useDeepCompareEffect } from "./useDeepCompareEffect"
5
+
6
+ export function useNostoApi(
7
+ cb: (api: NostoClient) => void,
8
+ deps?: React.DependencyList,
9
+ flags?: { deep?: boolean }
10
+ ): void {
11
+ const { clientScriptLoaded, currentVariation, responseMode } = useNostoContext()
12
+ const useEffectFn = flags?.deep ? useDeepCompareEffect : useEffect
13
+
14
+ useEffectFn(() => {
15
+ if (clientScriptLoaded) {
16
+ window.nostojs(api => {
17
+ api.defaultSession().setVariation(currentVariation!).setResponseMode(responseMode)
18
+ cb(api)
19
+ })
20
+ }
21
+ }, [clientScriptLoaded, currentVariation, responseMode, ...(deps ?? [])])
22
+ }
@@ -0,0 +1,18 @@
1
+ import { useContext } from "react"
2
+ import { NostoContext, NostoContextType } from "../context"
3
+
4
+ /**
5
+ * A hook that allows you to access the NostoContext and retrieve Nosto-related data from it in React components.
6
+ *
7
+ * @group Essential Functions
8
+ */
9
+ export function useNostoContext(): NostoContextType {
10
+ const context = useContext(NostoContext)
11
+
12
+ if (!context) {
13
+ throw new Error("No nosto context found")
14
+ }
15
+
16
+ return context
17
+ }
18
+
@@ -0,0 +1,60 @@
1
+ import { useRef } from "react"
2
+ import { createRoot, Root } from "react-dom/client"
3
+ import { ActionResponse, Recommendation } from "../types"
4
+ import { useNostoContext } from "./useNostoContext"
5
+ import React from "react"
6
+ import { RecommendationComponent } from "../context"
7
+
8
+ // RecommendationComponent for client-side rendering:
9
+ function RecommendationComponentWrapper(props: {
10
+ recommendationComponent: RecommendationComponent,
11
+ nostoRecommendation: Recommendation }) {
12
+
13
+ return React.cloneElement(props.recommendationComponent, {
14
+ // eslint-disable-next-line react/prop-types
15
+ nostoRecommendation: props.nostoRecommendation,
16
+ })
17
+ }
18
+
19
+ function injectCampaigns(data: ActionResponse) {
20
+ if (!window.nostojs) {
21
+ throw new Error("Nosto has not yet been initialized")
22
+ }
23
+ window.nostojs(api => {
24
+ api.placements.injectCampaigns(data.recommendations)
25
+ })
26
+ }
27
+
28
+ export function useRenderCampaigns() {
29
+ const { responseMode, recommendationComponent } = useNostoContext()
30
+ const placementRefs = useRef<Record<string, Root>>({})
31
+
32
+ if (responseMode == "HTML") {
33
+ return { renderCampaigns: injectCampaigns }
34
+ }
35
+
36
+ function renderCampaigns(data: ActionResponse) {
37
+ // render recommendation component into placements:
38
+ const recommendations = data.campaigns?.recommendations ?? {}
39
+ for (const key in recommendations) {
40
+ const recommendation = recommendations[key] as Recommendation
41
+ const placementSelector = "#" + key
42
+ const placementElement = document.querySelector(placementSelector)
43
+
44
+ if (placementElement) {
45
+ if (!placementRefs.current[key]) {
46
+ placementRefs.current[key] = createRoot(placementElement)
47
+ }
48
+ const root = placementRefs.current[key]!
49
+ root.render(
50
+ <RecommendationComponentWrapper
51
+ recommendationComponent={recommendationComponent!}
52
+ nostoRecommendation={recommendation}
53
+ ></RecommendationComponentWrapper>
54
+ )
55
+ }
56
+ }
57
+ }
58
+
59
+ return { renderCampaigns }
60
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
- export type { Buyer, Cart, Customer, Item, Product, Purchase, Recommendation, SKU } from "./types"
2
-
1
+ export type { Cart, Customer, Product, Order, Recommendation } from "./types"
3
2
  export * from "./components"
3
+ export { NostoContext, type NostoContextType } from "./context"
4
+ export { useNostoContext } from "./hooks/useNostoContext"