Skip to content

Commit 8ba5dcb

Browse files
authored
feat(kno-7523): React 19 Support (#448)
* [JS] Support React 19 in React SDKs (#419) * removes react-popper dependency * allow react 19 * upgrades zustand * merge * fixes render loop bug * adjusts version number, removes placeholder * undo example changes for this branch * Better URL in comment * fixes zustand implementation * matches types on main * Use new store * merges in examples * Cleanup CRA example * fixes docs to remove zustand requirement * Tests pass locally * fixes dev dependency issues * runs lint * runs format * remove unused dependency * adds setup env variable * clean up PR * Improves selector typing and validates that it works * runs formatter * comment removes unnecessary info * Better import and comments * rips out rerender hack * better styling setup * updates to match main and easier merge * run lint and format * Updates a few readmes * Create red-owls-jog.md * adds version bumps for dependent packages and adds migration guide * adds package specific upgrade guides * updates upgrade guide to be more specific * better formatting on upgrade guide * downgrade zustand to support react 16 * undo manual version bump * update guides and small PR feedback updates * updates docs examples to stick with useNotificationStore * adds links to upgrade guides * fix(kno-7523): fixes separation of zustand vanilla and react (#429) * fixes type handling * testing new behavior * lint and format * Removes upgrade guide and validated store * remove console logs * removes test data * fix(kno-7971): adds autoRegister to useEffect deps (#437) * fix(kno-7523): fixes bad returns from useNotificationStore (#439) * fix(kno-7523): fixes bad returns from useNotificationStore * adds better types and comments: * fixes hook name * adds tests * bumps telegraph component versions in nextjs example * align turbo * lock eslint-config-turbo resolution
1 parent 8ace090 commit 8ba5dcb

37 files changed

+4024
-5973
lines changed

.changeset/red-owls-jog.md

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"client-example": minor
3+
"nextjs-example": minor
4+
"slack-kit-example": minor
5+
"@knocklabs/client": minor
6+
"@knocklabs/expo": minor
7+
"@knocklabs/react-core": minor
8+
"@knocklabs/react-native": minor
9+
"@knocklabs/react": minor
10+
---
11+
12+
[JS] Support React 19 in React SDKs

examples/client-example/package.json

+6-4
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
"dependencies": {
66
"@knocklabs/client": "workspace:^",
77
"@testing-library/jest-dom": "^6.6.3",
8-
"@testing-library/react": "^14.2.0",
9-
"@testing-library/user-event": "^14.6.1",
8+
"@testing-library/react": "^16.2.0",
9+
"@testing-library/user-event": "^14.5.2",
1010
"react": "^18.2.0",
1111
"react-dom": "^18.2.0",
1212
"react-scripts": "5.0.1",
13-
"web-vitals": "^4.2.4",
14-
"zustand": "^3.7.2"
13+
"web-vitals": "^4.2.4"
1514
},
1615
"scripts": {
1716
"dev": "PORT=3001 BROWSER=none react-scripts start",
@@ -37,5 +36,8 @@
3736
"last 1 firefox version",
3837
"last 1 safari version"
3938
]
39+
},
40+
"devDependencies": {
41+
"@testing-library/dom": "^10.4.0"
4042
}
4143
}

examples/client-example/src/App.js

+21-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Knock from "@knocklabs/client";
2-
import { useEffect, useMemo } from "react";
3-
import create from "zustand";
2+
import { useEffect, useMemo, useState } from "react";
43

54
import "./App.css";
65

@@ -17,10 +16,10 @@ const useNotificationFeed = (knockClient, feedId) => {
1716
auto_manage_socket_connection: true,
1817
auto_manage_socket_connection_delay: 500,
1918
});
20-
const notificationStore = create(notificationFeed.store);
19+
2120
notificationFeed.fetch();
2221

23-
return [notificationFeed, notificationStore];
22+
return [notificationFeed, notificationFeed.store];
2423
}, [knockClient, feedId]);
2524
};
2625

@@ -29,6 +28,7 @@ function App() {
2928
knockClient,
3029
process.env.REACT_APP_KNOCK_CHANNEL_ID,
3130
);
31+
const [feedState, setFeedState] = useState(feedStore.getState());
3232

3333
useEffect(() => {
3434
knockClient.preferences
@@ -41,6 +41,20 @@ function App() {
4141
});
4242
}, []);
4343

44+
// Consume the store
45+
useEffect(() => {
46+
// What to do on updates
47+
const render = (state) => {
48+
setFeedState(state);
49+
};
50+
51+
// What to do on initial load
52+
render(feedStore.getInitialState());
53+
54+
// Subscribe to updates
55+
feedStore.subscribe(render);
56+
}, [feedStore]);
57+
4458
useEffect(() => {
4559
const teardown = feedClient.listenForUpdates();
4660

@@ -59,7 +73,7 @@ function App() {
5973
return () => teardown?.();
6074
}, [feedClient]);
6175

62-
const { loading, items, pageInfo } = feedStore((state) => state);
76+
const { loading, items, pageInfo } = feedState;
6377

6478
return (
6579
<div className="App">
@@ -72,6 +86,8 @@ function App() {
7286
<div key={item.id} className="feed-item">
7387
ID: {item.id}
7488
<br />
89+
Has been read: {item.read_at ? "true" : "false"}
90+
<br />
7591
Actor ID: {item.actors?.[0]?.id}
7692
<br />
7793
Actor email: {item.actors?.[0]?.email}

examples/client-example/src/index.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import React from "react";
2-
import ReactDOM from "react-dom";
2+
import { createRoot } from "react-dom/client";
33

44
import App from "./App";
55
import "./index.css";
66
import reportWebVitals from "./reportWebVitals";
77

8-
ReactDOM.render(
8+
const container = document.getElementById("root");
9+
const root = createRoot(container);
10+
root.render(
911
<React.StrictMode>
1012
<App />
1113
</React.StrictMode>,
12-
document.getElementById("root"),
1314
);
1415

1516
// If you want to start measuring performance in your app, pass a function

examples/nextjs-example/.env.sample

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ KNOCK_SECRET_API_KEY=<Knock secret API key>
33

44
# Required for Knock's notification React feed component to render correctly
55
NEXT_PUBLIC_KNOCK_PUBLIC_API_KEY=<Knock public API key>
6-
NEXT_PUBLIC_KNOCK_FEED_CHANNEL_ID=<Knock in-app feed channel ID>
6+
NEXT_PUBLIC_KNOCK_FEED_CHANNEL_ID=<Knock in-app feed channel ID for loading the feed>
7+
NEXT_PUBLIC_WORKFLOW_KEY=<Knock workflow key that has the in-app channel, for sending notifications to this feed>
78

89
NEXT_PUBLIC_KNOCK_HOST=<Optional>

examples/nextjs-example/components/NotificationToasts.tsx

+17-23
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,29 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import { useToast } from "@chakra-ui/react";
1+
import { type FeedItem } from "@knocklabs/client";
32
import { useKnockFeed } from "@knocklabs/react";
43
import { useCallback, useEffect } from "react";
5-
6-
import Toast from "./Toast";
4+
import { toast } from "sonner";
75

86
const NotificationToasts = () => {
97
const { feedClient } = useKnockFeed();
10-
const toast = useToast();
118

129
const onNotificationsReceived = useCallback(
13-
({ items }: any) => {
10+
({ items }: { items: FeedItem[] }) => {
1411
// Whenever we receive a new notification from our real-time stream, show a toast
1512
// (note here that we can receive > 1 items in a batch)
16-
items.forEach((notification: any) => {
17-
if (notification.data.showToast === false) return;
18-
19-
toast({
20-
render: (props) => (
21-
// @ts-expect-error - difference in status type
22-
<Toast
23-
{...props}
24-
title={"New notification received"}
25-
description={notification.blocks[0].rendered}
26-
onClose={() => {
27-
feedClient.markAsSeen(notification);
28-
props.onClose();
29-
}}
30-
/>
31-
),
32-
position: "bottom-right",
13+
items.forEach((notification) => {
14+
if (notification.data?.showToast === false) return;
15+
16+
// You can access the Knock notification data
17+
const description = notification.data?.message;
18+
19+
// Handle the notification however you want
20+
toast.success("New Notification Received", {
21+
description: description,
22+
closeButton: true,
23+
dismissible: true,
24+
onDismiss: () => {
25+
feedClient.markAsSeen(notification);
26+
},
3327
});
3428
});
3529
},

examples/nextjs-example/components/SendNotificationForm.tsx

+76-57
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import {
2-
Button,
3-
Checkbox,
4-
FormControl,
5-
FormLabel,
6-
Select,
7-
Textarea,
8-
} from "@chakra-ui/react";
9-
import { FormEvent, useState } from "react";
1+
import { Button } from "@telegraph/button";
2+
import { Box, Stack } from "@telegraph/layout";
3+
import { Select } from "@telegraph/select";
4+
import { TextArea } from "@telegraph/textarea";
5+
import { Text } from "@telegraph/typography";
6+
import { type FormEvent, useState } from "react";
107

118
import { notify } from "../lib/api";
129

@@ -32,60 +29,82 @@ const SendNotificationForm = ({ userId, tenant }: Props) => {
3229
setIsLoading(true);
3330
await notify({ message, showToast, userId, tenant, templateType });
3431
setIsLoading(false);
35-
32+
setMessage("");
3633
(e.target as HTMLFormElement).reset();
3734
};
3835

3936
return (
4037
<form onSubmit={onSubmit}>
41-
<FormControl mb={3}>
42-
<FormLabel htmlFor="message" fontSize={14}>
43-
Message
44-
</FormLabel>
45-
<Textarea
46-
id="message"
47-
name="message"
48-
placeholder="Message to be shown in the notification"
49-
size="sm"
50-
onChange={(e) => setMessage(e.target.value)}
51-
/>
52-
</FormControl>
53-
<FormControl mb={4}>
54-
<FormLabel fontSize={14}>Template type</FormLabel>
55-
<Select
56-
mr={3}
57-
size="sm"
58-
value={templateType}
59-
onChange={(e) => setTemplateType(e.target.value as TemplateType)}
38+
<Stack direction="column" gap="4" marginTop="3">
39+
<Box>
40+
<Stack direction="column" gap="1">
41+
<Text as="label" htmlFor="message" size="2">
42+
Message
43+
</Text>
44+
<TextArea
45+
as="textarea"
46+
display="block"
47+
id="message"
48+
height="20"
49+
name="message"
50+
placeholder="Message to be shown in the notification"
51+
size="2"
52+
onChange={(e) => setMessage(e.target.value)}
53+
/>
54+
</Stack>
55+
</Box>
56+
<Box marginBottom="3">
57+
<Stack direction="column" gap="1">
58+
<Text as="label" size="2">
59+
Template type
60+
</Text>
61+
<Box marginRight="2">
62+
<Select.Root
63+
size="2"
64+
value={templateType}
65+
onValueChange={(value) =>
66+
setTemplateType(value as TemplateType)
67+
}
68+
>
69+
<Select.Option value={TemplateType.Standard}>
70+
Standard
71+
</Select.Option>
72+
<Select.Option value={TemplateType.SingleAction}>
73+
Single-action
74+
</Select.Option>
75+
<Select.Option value={TemplateType.MultiAction}>
76+
Multi-action
77+
</Select.Option>
78+
</Select.Root>
79+
</Box>
80+
</Stack>
81+
</Box>
82+
<Box marginBottom="3">
83+
<Text as="label" size="2">
84+
<Stack direction="row" alignItems="center">
85+
<input
86+
type="checkbox"
87+
name="showToast"
88+
checked={showToast}
89+
onChange={(e) => setShowToast(e.target.checked)}
90+
/>
91+
<Text as="span" size="2" marginLeft="1">
92+
Show a toast?
93+
</Text>
94+
</Stack>
95+
</Text>
96+
</Box>
97+
<Button
98+
type="submit"
99+
variant="solid"
100+
color="accent"
101+
size="2"
102+
disabled={message === ""}
103+
state={isLoading ? "loading" : undefined}
60104
>
61-
<option value={TemplateType.Standard}>Standard</option>
62-
<option value={TemplateType.SingleAction}>Single-action</option>
63-
<option value={TemplateType.MultiAction}>Multi-action</option>
64-
</Select>
65-
</FormControl>
66-
<FormControl mb={4}>
67-
<FormLabel fontSize={14} display="flex" alignItems="center">
68-
<Checkbox
69-
name="showToast"
70-
size="sm"
71-
isChecked={showToast}
72-
onChange={(e) => setShowToast(e.target.checked)}
73-
mr={2}
74-
/>{" "}
75-
Show a toast?{" "}
76-
</FormLabel>
77-
</FormControl>
78-
79-
<Button
80-
type="submit"
81-
variant="solid"
82-
colorScheme="gray"
83-
size="sm"
84-
isDisabled={message === ""}
85-
isLoading={isLoading}
86-
>
87-
Send notification
88-
</Button>
105+
Send Notification
106+
</Button>
107+
</Stack>
89108
</form>
90109
);
91110
};

0 commit comments

Comments
 (0)