@stytch/react 16.0.0 → 17.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @stytch/react
2
2
 
3
+ ## 17.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - a0fbe9f: Update minimum peer dependency on `@stytch/vanilla-js` to `^4.9.0`
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [a0fbe9f]
12
+ - Updated dependencies [a0fbe9f]
13
+ - @stytch/vanilla-js@4.9.0
14
+
3
15
  ## 16.0.0
4
16
 
5
17
  ### Patch Changes
@@ -11,7 +11,7 @@ import { Callbacks, StyleConfig, StytchB2BUIConfig } from "@stytch/vanilla-js";
11
11
  type StytchB2BClient = StytchB2BHeadlessClient | StytchB2BUIClient;
12
12
  type SWRMember = {
13
13
  /**
14
- * Either the active {@link Member} object, or null if the user is not logged in.
14
+ * Either the active {@link Member} object, or null if the member is not logged in.
15
15
  */
16
16
  member: Member | null;
17
17
  /**
@@ -21,7 +21,7 @@ type SWRMember = {
21
21
  };
22
22
  type SWRMemberSession = {
23
23
  /**
24
- * Either the active {@link MemberSession} object, or null if the user is not logged in.
24
+ * Either the active {@link MemberSession} object, or null if the member is not logged in.
25
25
  */
26
26
  session: MemberSession | null;
27
27
  /**
@@ -31,7 +31,7 @@ type SWRMemberSession = {
31
31
  };
32
32
  type SWROrganization = {
33
33
  /**
34
- * Either the active {@link Organization} object, or null if the user is not logged in.
34
+ * Either the active {@link Organization} object, or null if the member is not logged in.
35
35
  */
36
36
  organization: Organization | null;
37
37
  /**
@@ -11,7 +11,7 @@ import { Callbacks, StyleConfig, StytchB2BUIConfig } from "@stytch/vanilla-js";
11
11
  type StytchB2BClient = StytchB2BHeadlessClient | StytchB2BUIClient;
12
12
  type SWRMember = {
13
13
  /**
14
- * Either the active {@link Member} object, or null if the user is not logged in.
14
+ * Either the active {@link Member} object, or null if the member is not logged in.
15
15
  */
16
16
  member: Member | null;
17
17
  /**
@@ -21,7 +21,7 @@ type SWRMember = {
21
21
  };
22
22
  type SWRMemberSession = {
23
23
  /**
24
- * Either the active {@link MemberSession} object, or null if the user is not logged in.
24
+ * Either the active {@link MemberSession} object, or null if the member is not logged in.
25
25
  */
26
26
  session: MemberSession | null;
27
27
  /**
@@ -31,7 +31,7 @@ type SWRMemberSession = {
31
31
  };
32
32
  type SWROrganization = {
33
33
  /**
34
- * Either the active {@link Organization} object, or null if the user is not logged in.
34
+ * Either the active {@link Organization} object, or null if the member is not logged in.
35
35
  */
36
36
  organization: Organization | null;
37
37
  /**
@@ -1,5 +1,56 @@
1
1
  import React, { useRef, useState, useEffect, useCallback, createContext, useContext, useMemo, useLayoutEffect } from 'react';
2
2
 
3
+ const createDeepEqual = ({ KEYS_TO_EXCLUDE = [] } = {}) => {
4
+ // If comparing functions, this may need some work. Not sure the
5
+ // best path for this: compare instance (what it currently does),
6
+ // stringify and compare, etc.
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ const deepEqual = (a, b) => {
9
+ // Ensures type is the same
10
+ if (typeof a !== typeof b)
11
+ return false;
12
+ // arrays, null, and objects all have type 'object'
13
+ if (a === null || b === null)
14
+ return a === b;
15
+ if (typeof a === 'object') {
16
+ if (Object.keys(a).length !== Object.keys(b).length || Object.keys(a).some((k) => !(k in b)))
17
+ return false;
18
+ return Object.entries(a)
19
+ .filter(([k]) => !KEYS_TO_EXCLUDE.includes(k))
20
+ .every(([k, v]) => deepEqual(v, b[k]));
21
+ }
22
+ // boolean, string, number, undefined
23
+ return a === b;
24
+ };
25
+ return deepEqual;
26
+ };
27
+
28
+ const deepEqual = createDeepEqual();
29
+ /**
30
+ * Returns a version of `newValue` whose properties that are deeply equal to
31
+ * those in `oldValue` are replaced with those from `oldValue`. This provides a
32
+ * limited form of "structural sharing" that provides a stable reference for
33
+ * unchanged slices of the object.
34
+ *
35
+ * If `oldValue` and `newValue` are referentially equal, the same value is
36
+ * returned.
37
+ *
38
+ * @param oldValue The old value
39
+ * @param newValue The new value
40
+ */
41
+ const mergeWithStableProps = (oldValue, newValue) => {
42
+ // If the values are already referentially the same, just return the new value
43
+ if (oldValue === newValue) {
44
+ return newValue;
45
+ }
46
+ return Object.keys(oldValue).reduce((acc, key) => {
47
+ if (key in newValue && deepEqual(oldValue[key], newValue[key])) {
48
+ acc[key] = oldValue[key];
49
+ }
50
+ return acc;
51
+ }, Object.assign({}, newValue));
52
+ };
53
+
3
54
  const noProviderError = (item, provider = 'StytchProvider') => `${item} can only be used inside <${provider}>.`;
4
55
  const B2BProviderMustBeUniqueError = 'You cannot render a <StytchB2BProvider> inside another <StytchB2BProvider>.';
5
56
  const noSSRError = `The @stytch/react library is not meant for use with serverside environments like NextJS.
@@ -225,46 +276,25 @@ const StytchB2BProvider = ({ stytch, children }) => {
225
276
  invariant(!useIsMounted__INTERNAL(), B2BProviderMustBeUniqueError);
226
277
  invariant(typeof window !== 'undefined', noSSRError);
227
278
  const ctx = useMemo(() => ({ client: stytch, isMounted: true }), [stytch]);
228
- const [member, setMember] = useAsyncState({
229
- member: stytch.self.getSync(),
230
- fromCache: true,
231
- });
232
- const [session, setMemberSession] = useAsyncState({
233
- session: stytch.session.getSync(),
234
- fromCache: true,
235
- });
236
- const [organization, setOrganization] = useAsyncState({
237
- organization: stytch.organization.getSync(),
238
- fromCache: true,
279
+ const [{ member, session, organization }, setClientState] = useAsyncState({
280
+ session: stytch.session.getInfo(),
281
+ member: stytch.self.getInfo(),
282
+ organization: stytch.organization.getInfo(),
239
283
  });
240
- useEffect(() => {
241
- const unsubscribeMember = stytch.self.onChange((member) => setMember({
242
- member,
243
- fromCache: false,
244
- }));
245
- const unsubscribeSession = stytch.session.onChange((session) => setMemberSession({
246
- session,
247
- fromCache: false,
248
- }));
249
- const unsubscribeOrganization = stytch.organization.onChange((organization) => setOrganization({
250
- organization,
251
- fromCache: false,
252
- }));
253
- return () => {
254
- unsubscribeMember();
255
- unsubscribeSession();
256
- unsubscribeOrganization();
257
- };
258
- }, [stytch, setMember, setMemberSession, setOrganization]);
259
- // TODO (SDK-813): Remove this when we have a single top-level onChange handler
260
- const allValuesReady = !!member.member === !!session.session && !!session.session === !!organization.organization;
261
- const finalValues = allValuesReady
262
- ? { member, session, organization }
263
- : { member: initialMember, session: initialMemberSession, organization: initialOrganization };
284
+ useEffect(() => stytch.onStateChange(() => {
285
+ setClientState((oldState) => {
286
+ const newState = {
287
+ session: stytch.session.getInfo(),
288
+ member: stytch.self.getInfo(),
289
+ organization: stytch.organization.getInfo(),
290
+ };
291
+ return mergeWithStableProps(oldState, newState);
292
+ });
293
+ }), [setClientState, stytch]);
264
294
  return (React.createElement(StytchB2BContext.Provider, { value: ctx },
265
- React.createElement(StytchOrganizationContext.Provider, { value: finalValues.organization },
266
- React.createElement(StytchMemberContext.Provider, { value: finalValues.member },
267
- React.createElement(StytchMemberSessionContext.Provider, { value: finalValues.session }, children)))));
295
+ React.createElement(StytchOrganizationContext.Provider, { value: organization },
296
+ React.createElement(StytchMemberContext.Provider, { value: member },
297
+ React.createElement(StytchMemberSessionContext.Provider, { value: session }, children)))));
268
298
  };
269
299
 
270
300
  /**
package/dist/b2b/index.js CHANGED
@@ -8,6 +8,57 @@ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'defau
8
8
 
9
9
  var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
10
10
 
11
+ const createDeepEqual = ({ KEYS_TO_EXCLUDE = [] } = {}) => {
12
+ // If comparing functions, this may need some work. Not sure the
13
+ // best path for this: compare instance (what it currently does),
14
+ // stringify and compare, etc.
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ const deepEqual = (a, b) => {
17
+ // Ensures type is the same
18
+ if (typeof a !== typeof b)
19
+ return false;
20
+ // arrays, null, and objects all have type 'object'
21
+ if (a === null || b === null)
22
+ return a === b;
23
+ if (typeof a === 'object') {
24
+ if (Object.keys(a).length !== Object.keys(b).length || Object.keys(a).some((k) => !(k in b)))
25
+ return false;
26
+ return Object.entries(a)
27
+ .filter(([k]) => !KEYS_TO_EXCLUDE.includes(k))
28
+ .every(([k, v]) => deepEqual(v, b[k]));
29
+ }
30
+ // boolean, string, number, undefined
31
+ return a === b;
32
+ };
33
+ return deepEqual;
34
+ };
35
+
36
+ const deepEqual = createDeepEqual();
37
+ /**
38
+ * Returns a version of `newValue` whose properties that are deeply equal to
39
+ * those in `oldValue` are replaced with those from `oldValue`. This provides a
40
+ * limited form of "structural sharing" that provides a stable reference for
41
+ * unchanged slices of the object.
42
+ *
43
+ * If `oldValue` and `newValue` are referentially equal, the same value is
44
+ * returned.
45
+ *
46
+ * @param oldValue The old value
47
+ * @param newValue The new value
48
+ */
49
+ const mergeWithStableProps = (oldValue, newValue) => {
50
+ // If the values are already referentially the same, just return the new value
51
+ if (oldValue === newValue) {
52
+ return newValue;
53
+ }
54
+ return Object.keys(oldValue).reduce((acc, key) => {
55
+ if (key in newValue && deepEqual(oldValue[key], newValue[key])) {
56
+ acc[key] = oldValue[key];
57
+ }
58
+ return acc;
59
+ }, Object.assign({}, newValue));
60
+ };
61
+
11
62
  const noProviderError = (item, provider = 'StytchProvider') => `${item} can only be used inside <${provider}>.`;
12
63
  const B2BProviderMustBeUniqueError = 'You cannot render a <StytchB2BProvider> inside another <StytchB2BProvider>.';
13
64
  const noSSRError = `The @stytch/react library is not meant for use with serverside environments like NextJS.
@@ -233,46 +284,25 @@ const StytchB2BProvider = ({ stytch, children }) => {
233
284
  invariant(!useIsMounted__INTERNAL(), B2BProviderMustBeUniqueError);
234
285
  invariant(typeof window !== 'undefined', noSSRError);
235
286
  const ctx = React.useMemo(() => ({ client: stytch, isMounted: true }), [stytch]);
236
- const [member, setMember] = useAsyncState({
237
- member: stytch.self.getSync(),
238
- fromCache: true,
239
- });
240
- const [session, setMemberSession] = useAsyncState({
241
- session: stytch.session.getSync(),
242
- fromCache: true,
243
- });
244
- const [organization, setOrganization] = useAsyncState({
245
- organization: stytch.organization.getSync(),
246
- fromCache: true,
287
+ const [{ member, session, organization }, setClientState] = useAsyncState({
288
+ session: stytch.session.getInfo(),
289
+ member: stytch.self.getInfo(),
290
+ organization: stytch.organization.getInfo(),
247
291
  });
248
- React.useEffect(() => {
249
- const unsubscribeMember = stytch.self.onChange((member) => setMember({
250
- member,
251
- fromCache: false,
252
- }));
253
- const unsubscribeSession = stytch.session.onChange((session) => setMemberSession({
254
- session,
255
- fromCache: false,
256
- }));
257
- const unsubscribeOrganization = stytch.organization.onChange((organization) => setOrganization({
258
- organization,
259
- fromCache: false,
260
- }));
261
- return () => {
262
- unsubscribeMember();
263
- unsubscribeSession();
264
- unsubscribeOrganization();
265
- };
266
- }, [stytch, setMember, setMemberSession, setOrganization]);
267
- // TODO (SDK-813): Remove this when we have a single top-level onChange handler
268
- const allValuesReady = !!member.member === !!session.session && !!session.session === !!organization.organization;
269
- const finalValues = allValuesReady
270
- ? { member, session, organization }
271
- : { member: initialMember, session: initialMemberSession, organization: initialOrganization };
292
+ React.useEffect(() => stytch.onStateChange(() => {
293
+ setClientState((oldState) => {
294
+ const newState = {
295
+ session: stytch.session.getInfo(),
296
+ member: stytch.self.getInfo(),
297
+ organization: stytch.organization.getInfo(),
298
+ };
299
+ return mergeWithStableProps(oldState, newState);
300
+ });
301
+ }), [setClientState, stytch]);
272
302
  return (React__default['default'].createElement(StytchB2BContext.Provider, { value: ctx },
273
- React__default['default'].createElement(StytchOrganizationContext.Provider, { value: finalValues.organization },
274
- React__default['default'].createElement(StytchMemberContext.Provider, { value: finalValues.member },
275
- React__default['default'].createElement(StytchMemberSessionContext.Provider, { value: finalValues.session }, children)))));
303
+ React__default['default'].createElement(StytchOrganizationContext.Provider, { value: organization },
304
+ React__default['default'].createElement(StytchMemberContext.Provider, { value: member },
305
+ React__default['default'].createElement(StytchMemberSessionContext.Provider, { value: session }, children)))));
276
306
  };
277
307
 
278
308
  /**
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ import React from "react";
3
3
  import { ReactNode } from "react";
4
4
  // We need to import the StytchUIClient type to give the TSDoc parser a hint as to where it is from
5
5
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
6
- import { Callbacks, StytchLoginConfig, StyleConfig, User, Session, StytchUIClient } from "@stytch/vanilla-js";
6
+ import { Callbacks, StytchLoginConfig, StyleConfig, Session, StytchUIClient, User } from "@stytch/vanilla-js";
7
7
  import { StytchHeadlessClient } from "@stytch/vanilla-js/headless";
8
8
  interface StytchProps {
9
9
  /**
@@ -3,7 +3,7 @@ import React from "react";
3
3
  import { ReactNode } from "react";
4
4
  // We need to import the StytchUIClient type to give the TSDoc parser a hint as to where it is from
5
5
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
6
- import { Callbacks, StytchLoginConfig, StyleConfig, User, Session, StytchUIClient } from "@stytch/vanilla-js";
6
+ import { Callbacks, StytchLoginConfig, StyleConfig, Session, StytchUIClient, User } from "@stytch/vanilla-js";
7
7
  import { StytchHeadlessClient } from "@stytch/vanilla-js/headless";
8
8
  interface StytchProps {
9
9
  /**
package/dist/index.esm.js CHANGED
@@ -1,20 +1,55 @@
1
1
  import React, { useRef, useState, useEffect, useCallback, createContext, useContext, useMemo, useLayoutEffect } from 'react';
2
2
 
3
- const noProviderError = (item, provider = 'StytchProvider') => `${item} can only be used inside <${provider}>.`;
4
- const providerMustBeUniqueError = 'You cannot render a <StytchProvider> inside another <StytchProvider>.';
5
- const noSSRError = `The @stytch/react library is not meant for use with serverside environments like NextJS.
6
- Use the @stytch/nextjs library instead -
7
- npm remove @stytch/react && npm install @stytch/nextjs
8
- `;
9
- const noHeadlessClientError = `Tried to create a Stytch Login UI element using the Stytch Headless SDK.
10
- You must use the UI SDK to use UI elements.
11
- Please make sure you are importing from @stytch/vanilla-js and not from the @stytch/vanilla-js/headless.`;
3
+ const createDeepEqual = ({ KEYS_TO_EXCLUDE = [] } = {}) => {
4
+ // If comparing functions, this may need some work. Not sure the
5
+ // best path for this: compare instance (what it currently does),
6
+ // stringify and compare, etc.
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ const deepEqual = (a, b) => {
9
+ // Ensures type is the same
10
+ if (typeof a !== typeof b)
11
+ return false;
12
+ // arrays, null, and objects all have type 'object'
13
+ if (a === null || b === null)
14
+ return a === b;
15
+ if (typeof a === 'object') {
16
+ if (Object.keys(a).length !== Object.keys(b).length || Object.keys(a).some((k) => !(k in b)))
17
+ return false;
18
+ return Object.entries(a)
19
+ .filter(([k]) => !KEYS_TO_EXCLUDE.includes(k))
20
+ .every(([k, v]) => deepEqual(v, b[k]));
21
+ }
22
+ // boolean, string, number, undefined
23
+ return a === b;
24
+ };
25
+ return deepEqual;
26
+ };
12
27
 
13
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
- function invariant(cond, message) {
15
- if (!cond)
16
- throw new Error(message);
17
- }
28
+ const deepEqual = createDeepEqual();
29
+ /**
30
+ * Returns a version of `newValue` whose properties that are deeply equal to
31
+ * those in `oldValue` are replaced with those from `oldValue`. This provides a
32
+ * limited form of "structural sharing" that provides a stable reference for
33
+ * unchanged slices of the object.
34
+ *
35
+ * If `oldValue` and `newValue` are referentially equal, the same value is
36
+ * returned.
37
+ *
38
+ * @param oldValue The old value
39
+ * @param newValue The new value
40
+ */
41
+ const mergeWithStableProps = (oldValue, newValue) => {
42
+ // If the values are already referentially the same, just return the new value
43
+ if (oldValue === newValue) {
44
+ return newValue;
45
+ }
46
+ return Object.keys(oldValue).reduce((acc, key) => {
47
+ if (key in newValue && deepEqual(oldValue[key], newValue[key])) {
48
+ acc[key] = oldValue[key];
49
+ }
50
+ return acc;
51
+ }, Object.assign({}, newValue));
52
+ };
18
53
 
19
54
  // useState can cause memory leaks if it is set after the component unmounted. For example, if it is
20
55
  // set after `await`, or in a `then`, `catch`, or `finally`, or in a setTimout/setInterval.
@@ -33,6 +68,22 @@ const useAsyncState = (initialState) => {
33
68
  return [state, setStateAction];
34
69
  };
35
70
 
71
+ const noProviderError = (item, provider = 'StytchProvider') => `${item} can only be used inside <${provider}>.`;
72
+ const providerMustBeUniqueError = 'You cannot render a <StytchProvider> inside another <StytchProvider>.';
73
+ const noSSRError = `The @stytch/react library is not meant for use with serverside environments like NextJS.
74
+ Use the @stytch/nextjs library instead -
75
+ npm remove @stytch/react && npm install @stytch/nextjs
76
+ `;
77
+ const noHeadlessClientError = `Tried to create a Stytch Login UI element using the Stytch Headless SDK.
78
+ You must use the UI SDK to use UI elements.
79
+ Please make sure you are importing from @stytch/vanilla-js and not from the @stytch/vanilla-js/headless.`;
80
+
81
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
+ function invariant(cond, message) {
83
+ if (!cond)
84
+ throw new Error(message);
85
+ }
86
+
36
87
  const initialUser = {
37
88
  user: null,
38
89
  fromCache: false,
@@ -132,34 +183,22 @@ const StytchProvider = ({ stytch, children }) => {
132
183
  invariant(!useIsMounted__INTERNAL(), providerMustBeUniqueError);
133
184
  invariant(typeof window !== 'undefined', noSSRError);
134
185
  const ctx = useMemo(() => ({ client: stytch, isMounted: true }), [stytch]);
135
- const [user, setUser] = useAsyncState({
136
- user: stytch.user.getSync(),
137
- fromCache: true,
186
+ const [{ user, session }, setClientState] = useAsyncState({
187
+ session: stytch.session.getInfo(),
188
+ user: stytch.user.getInfo(),
138
189
  });
139
- const [session, setSession] = useAsyncState({
140
- session: stytch.session.getSync(),
141
- fromCache: true,
142
- });
143
- useEffect(() => {
144
- const unsubscribeUser = stytch.user.onChange((user) => setUser({
145
- user,
146
- fromCache: false,
147
- }));
148
- const unsubscribeSession = stytch.session.onChange((session) => setSession({
149
- session,
150
- fromCache: false,
151
- }));
152
- return () => {
153
- unsubscribeUser();
154
- unsubscribeSession();
155
- };
156
- }, [stytch, setUser, setSession]);
157
- // TODO (SDK-813): Remove this when we have a single top-level onChange handler
158
- const finalSess = !!session.session === !!user.user ? session : initialSession;
159
- const finalUser = !!session.session === !!user.user ? user : initialUser;
190
+ useEffect(() => stytch.onStateChange(() => {
191
+ setClientState((oldState) => {
192
+ const newState = {
193
+ session: stytch.session.getInfo(),
194
+ user: stytch.user.getInfo(),
195
+ };
196
+ return mergeWithStableProps(oldState, newState);
197
+ });
198
+ }), [setClientState, stytch]);
160
199
  return (React.createElement(StytchContext.Provider, { value: ctx },
161
- React.createElement(StytchUserContext.Provider, { value: finalUser },
162
- React.createElement(StytchSessionContext.Provider, { value: finalSess }, children))));
200
+ React.createElement(StytchUserContext.Provider, { value: user },
201
+ React.createElement(StytchSessionContext.Provider, { value: session }, children))));
163
202
  };
164
203
 
165
204
  /**
package/dist/index.js CHANGED
@@ -8,21 +8,56 @@ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'defau
8
8
 
9
9
  var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
10
10
 
11
- const noProviderError = (item, provider = 'StytchProvider') => `${item} can only be used inside <${provider}>.`;
12
- const providerMustBeUniqueError = 'You cannot render a <StytchProvider> inside another <StytchProvider>.';
13
- const noSSRError = `The @stytch/react library is not meant for use with serverside environments like NextJS.
14
- Use the @stytch/nextjs library instead -
15
- npm remove @stytch/react && npm install @stytch/nextjs
16
- `;
17
- const noHeadlessClientError = `Tried to create a Stytch Login UI element using the Stytch Headless SDK.
18
- You must use the UI SDK to use UI elements.
19
- Please make sure you are importing from @stytch/vanilla-js and not from the @stytch/vanilla-js/headless.`;
11
+ const createDeepEqual = ({ KEYS_TO_EXCLUDE = [] } = {}) => {
12
+ // If comparing functions, this may need some work. Not sure the
13
+ // best path for this: compare instance (what it currently does),
14
+ // stringify and compare, etc.
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ const deepEqual = (a, b) => {
17
+ // Ensures type is the same
18
+ if (typeof a !== typeof b)
19
+ return false;
20
+ // arrays, null, and objects all have type 'object'
21
+ if (a === null || b === null)
22
+ return a === b;
23
+ if (typeof a === 'object') {
24
+ if (Object.keys(a).length !== Object.keys(b).length || Object.keys(a).some((k) => !(k in b)))
25
+ return false;
26
+ return Object.entries(a)
27
+ .filter(([k]) => !KEYS_TO_EXCLUDE.includes(k))
28
+ .every(([k, v]) => deepEqual(v, b[k]));
29
+ }
30
+ // boolean, string, number, undefined
31
+ return a === b;
32
+ };
33
+ return deepEqual;
34
+ };
20
35
 
21
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
- function invariant(cond, message) {
23
- if (!cond)
24
- throw new Error(message);
25
- }
36
+ const deepEqual = createDeepEqual();
37
+ /**
38
+ * Returns a version of `newValue` whose properties that are deeply equal to
39
+ * those in `oldValue` are replaced with those from `oldValue`. This provides a
40
+ * limited form of "structural sharing" that provides a stable reference for
41
+ * unchanged slices of the object.
42
+ *
43
+ * If `oldValue` and `newValue` are referentially equal, the same value is
44
+ * returned.
45
+ *
46
+ * @param oldValue The old value
47
+ * @param newValue The new value
48
+ */
49
+ const mergeWithStableProps = (oldValue, newValue) => {
50
+ // If the values are already referentially the same, just return the new value
51
+ if (oldValue === newValue) {
52
+ return newValue;
53
+ }
54
+ return Object.keys(oldValue).reduce((acc, key) => {
55
+ if (key in newValue && deepEqual(oldValue[key], newValue[key])) {
56
+ acc[key] = oldValue[key];
57
+ }
58
+ return acc;
59
+ }, Object.assign({}, newValue));
60
+ };
26
61
 
27
62
  // useState can cause memory leaks if it is set after the component unmounted. For example, if it is
28
63
  // set after `await`, or in a `then`, `catch`, or `finally`, or in a setTimout/setInterval.
@@ -41,6 +76,22 @@ const useAsyncState = (initialState) => {
41
76
  return [state, setStateAction];
42
77
  };
43
78
 
79
+ const noProviderError = (item, provider = 'StytchProvider') => `${item} can only be used inside <${provider}>.`;
80
+ const providerMustBeUniqueError = 'You cannot render a <StytchProvider> inside another <StytchProvider>.';
81
+ const noSSRError = `The @stytch/react library is not meant for use with serverside environments like NextJS.
82
+ Use the @stytch/nextjs library instead -
83
+ npm remove @stytch/react && npm install @stytch/nextjs
84
+ `;
85
+ const noHeadlessClientError = `Tried to create a Stytch Login UI element using the Stytch Headless SDK.
86
+ You must use the UI SDK to use UI elements.
87
+ Please make sure you are importing from @stytch/vanilla-js and not from the @stytch/vanilla-js/headless.`;
88
+
89
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
+ function invariant(cond, message) {
91
+ if (!cond)
92
+ throw new Error(message);
93
+ }
94
+
44
95
  const initialUser = {
45
96
  user: null,
46
97
  fromCache: false,
@@ -140,34 +191,22 @@ const StytchProvider = ({ stytch, children }) => {
140
191
  invariant(!useIsMounted__INTERNAL(), providerMustBeUniqueError);
141
192
  invariant(typeof window !== 'undefined', noSSRError);
142
193
  const ctx = React.useMemo(() => ({ client: stytch, isMounted: true }), [stytch]);
143
- const [user, setUser] = useAsyncState({
144
- user: stytch.user.getSync(),
145
- fromCache: true,
194
+ const [{ user, session }, setClientState] = useAsyncState({
195
+ session: stytch.session.getInfo(),
196
+ user: stytch.user.getInfo(),
146
197
  });
147
- const [session, setSession] = useAsyncState({
148
- session: stytch.session.getSync(),
149
- fromCache: true,
150
- });
151
- React.useEffect(() => {
152
- const unsubscribeUser = stytch.user.onChange((user) => setUser({
153
- user,
154
- fromCache: false,
155
- }));
156
- const unsubscribeSession = stytch.session.onChange((session) => setSession({
157
- session,
158
- fromCache: false,
159
- }));
160
- return () => {
161
- unsubscribeUser();
162
- unsubscribeSession();
163
- };
164
- }, [stytch, setUser, setSession]);
165
- // TODO (SDK-813): Remove this when we have a single top-level onChange handler
166
- const finalSess = !!session.session === !!user.user ? session : initialSession;
167
- const finalUser = !!session.session === !!user.user ? user : initialUser;
198
+ React.useEffect(() => stytch.onStateChange(() => {
199
+ setClientState((oldState) => {
200
+ const newState = {
201
+ session: stytch.session.getInfo(),
202
+ user: stytch.user.getInfo(),
203
+ };
204
+ return mergeWithStableProps(oldState, newState);
205
+ });
206
+ }), [setClientState, stytch]);
168
207
  return (React__default['default'].createElement(StytchContext.Provider, { value: ctx },
169
- React__default['default'].createElement(StytchUserContext.Provider, { value: finalUser },
170
- React__default['default'].createElement(StytchSessionContext.Provider, { value: finalSess }, children))));
208
+ React__default['default'].createElement(StytchUserContext.Provider, { value: user },
209
+ React__default['default'].createElement(StytchSessionContext.Provider, { value: session }, children))));
171
210
  };
172
211
 
173
212
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stytch/react",
3
- "version": "16.0.0",
3
+ "version": "17.0.0",
4
4
  "description": "Stytch's official React Library",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.esm.js",
@@ -33,17 +33,19 @@
33
33
  ],
34
34
  "devDependencies": {
35
35
  "@babel/runtime": "7.18.6",
36
- "@stytch/vanilla-js": "4.7.0",
36
+ "@stytch/js-utils": "0.0.0",
37
+ "@stytch/vanilla-js": "4.9.0",
37
38
  "@testing-library/react": "14.0.0",
38
39
  "eslint-config-custom": "0.0.1",
39
40
  "react": "18.2.0",
40
41
  "react-dom": "18.2.0",
41
42
  "react-test-renderer": "18.0.0",
42
43
  "rollup": "2.56.3",
44
+ "type-fest": "4.15.0",
43
45
  "typescript": "5.3.3"
44
46
  },
45
47
  "peerDependencies": {
46
- "@stytch/vanilla-js": "^4.7.0",
48
+ "@stytch/vanilla-js": "^4.9.0",
47
49
  "react": ">= 17.0.2",
48
50
  "react-dom": ">= 17.0.2"
49
51
  }