diff --git a/samples/javascript/react_sample/.gitignore b/samples/javascript/react_sample/.gitignore
new file mode 100644
index 000000000..4d29575de
--- /dev/null
+++ b/samples/javascript/react_sample/.gitignore
@@ -0,0 +1,23 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/samples/javascript/react_sample/README.md b/samples/javascript/react_sample/README.md
new file mode 100644
index 000000000..23d5d4e6d
--- /dev/null
+++ b/samples/javascript/react_sample/README.md
@@ -0,0 +1,16 @@
+# Getting Started with Create React App
+
+This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
+
+## What changes from the original create-react-app
+1. Adding server.js using Express to serve the client app and providing a `negotiate` endpoint for the client to get the `Client Access URI`
+1. adding a npm command `npm run server` to build the client app and running the Expressserver
+1. update src/App.js to create a WebSocket connection
+
+
+## How to run
+```
+export WebPubSubConnectionString=”Your_Connection_String”
+npm install
+npm run server
+```
\ No newline at end of file
diff --git a/samples/javascript/react_sample/package.json b/samples/javascript/react_sample/package.json
new file mode 100644
index 000000000..6394cb653
--- /dev/null
+++ b/samples/javascript/react_sample/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "sample-app",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@azure/web-pubsub": "^1.1.1",
+ "@testing-library/jest-dom": "^5.17.0",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "express": "^4.18.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.4"
+ },
+ "scripts": {
+ "server": "npm run build && node server.js",
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/samples/javascript/react_sample/public/favicon.ico b/samples/javascript/react_sample/public/favicon.ico
new file mode 100644
index 000000000..a11777cc4
Binary files /dev/null and b/samples/javascript/react_sample/public/favicon.ico differ
diff --git a/samples/javascript/react_sample/public/index.html b/samples/javascript/react_sample/public/index.html
new file mode 100644
index 000000000..aa069f27c
--- /dev/null
+++ b/samples/javascript/react_sample/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/samples/javascript/react_sample/public/logo192.png b/samples/javascript/react_sample/public/logo192.png
new file mode 100644
index 000000000..fc44b0a37
Binary files /dev/null and b/samples/javascript/react_sample/public/logo192.png differ
diff --git a/samples/javascript/react_sample/public/logo512.png b/samples/javascript/react_sample/public/logo512.png
new file mode 100644
index 000000000..a4e47a654
Binary files /dev/null and b/samples/javascript/react_sample/public/logo512.png differ
diff --git a/samples/javascript/react_sample/public/manifest.json b/samples/javascript/react_sample/public/manifest.json
new file mode 100644
index 000000000..080d6c77a
--- /dev/null
+++ b/samples/javascript/react_sample/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/samples/javascript/react_sample/public/robots.txt b/samples/javascript/react_sample/public/robots.txt
new file mode 100644
index 000000000..e9e57dc4d
--- /dev/null
+++ b/samples/javascript/react_sample/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/samples/javascript/react_sample/server.js b/samples/javascript/react_sample/server.js
new file mode 100644
index 000000000..d56f1c3f9
--- /dev/null
+++ b/samples/javascript/react_sample/server.js
@@ -0,0 +1,24 @@
+const express = require("express");
+const { WebPubSubServiceClient } = require("@azure/web-pubsub");
+
+const app = express();
+const hubName = "hubtest1";
+const port = 8080;
+
+let serviceClient = new WebPubSubServiceClient(process.env.WebPubSubConnectionString, hubName);
+
+app.get("/negotiate", async (req, res) => {
+ // this is a super simple demo for how to do auth, normally you go through an auth flow
+ let id = req.query.id;
+ if (!id) {
+ res.status(400).send("missing user id");
+ return;
+ }
+ let token = await serviceClient.getClientAccessToken({ userId: id });
+ res.json({
+ url: token.url,
+ });
+});
+
+app.use(express.static("build"));
+app.listen(port, () => console.log("server started at localhost:" + port));
diff --git a/samples/javascript/react_sample/src/App.css b/samples/javascript/react_sample/src/App.css
new file mode 100644
index 000000000..74b5e0534
--- /dev/null
+++ b/samples/javascript/react_sample/src/App.css
@@ -0,0 +1,38 @@
+.App {
+ text-align: center;
+}
+
+.App-logo {
+ height: 40vmin;
+ pointer-events: none;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .App-logo {
+ animation: App-logo-spin infinite 20s linear;
+ }
+}
+
+.App-header {
+ background-color: #282c34;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ font-size: calc(10px + 2vmin);
+ color: white;
+}
+
+.App-link {
+ color: #61dafb;
+}
+
+@keyframes App-logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/samples/javascript/react_sample/src/App.js b/samples/javascript/react_sample/src/App.js
new file mode 100644
index 000000000..e5d3272ef
--- /dev/null
+++ b/samples/javascript/react_sample/src/App.js
@@ -0,0 +1,54 @@
+import logo from "./logo.svg";
+import "./App.css";
+import { useEffect, useState } from "react";
+
+function App() {
+ const [data, setData] = useState("Connecting");
+
+ useEffect(() => {
+ let socket;
+
+ async function createConnection() {
+ try {
+ const response = await fetch("/negotiate?id=userId1");
+ const token = await response.json();
+
+ socket = new WebSocket(token.url);
+
+ socket.onopen = () => {
+ console.log("WebSocket connected");
+ setData("Connected");
+ };
+
+ socket.onclose = (event) => {
+ console.log("WebSocket closed:", event);
+ setData("Connection closed");
+ };
+ } catch (error) {
+ console.error("Error creating WebSocket connection:", error);
+ setData("Connection error");
+ }
+ }
+
+ createConnection();
+
+ // Clean up the WebSocket connection when the component unmounts
+ return () => {
+ if (socket) {
+ socket.onclose = null; // Remove the onclose handler to avoid memory leaks
+ socket.close();
+ }
+ };
+ }, []); // Empty dependency array means this effect runs once when the component mounts
+
+ return (
+
+
+
+ {data}
+
+
+ );
+}
+
+export default App;
diff --git a/samples/javascript/react_sample/src/App.test.js b/samples/javascript/react_sample/src/App.test.js
new file mode 100644
index 000000000..1f03afeec
--- /dev/null
+++ b/samples/javascript/react_sample/src/App.test.js
@@ -0,0 +1,8 @@
+import { render, screen } from '@testing-library/react';
+import App from './App';
+
+test('renders learn react link', () => {
+ render();
+ const linkElement = screen.getByText(/learn react/i);
+ expect(linkElement).toBeInTheDocument();
+});
diff --git a/samples/javascript/react_sample/src/index.css b/samples/javascript/react_sample/src/index.css
new file mode 100644
index 000000000..ec2585e8c
--- /dev/null
+++ b/samples/javascript/react_sample/src/index.css
@@ -0,0 +1,13 @@
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
diff --git a/samples/javascript/react_sample/src/index.js b/samples/javascript/react_sample/src/index.js
new file mode 100644
index 000000000..770ee7d65
--- /dev/null
+++ b/samples/javascript/react_sample/src/index.js
@@ -0,0 +1,17 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import "./index.css";
+import App from "./App";
+import reportWebVitals from "./reportWebVitals";
+
+const root = ReactDOM.createRoot(document.getElementById("root"));
+root.render(
+
+
+
+);
+
+// If you want to start measuring performance in your app, pass a function
+// to log results (for example: reportWebVitals(console.log))
+// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
+reportWebVitals();
diff --git a/samples/javascript/react_sample/src/logo.svg b/samples/javascript/react_sample/src/logo.svg
new file mode 100644
index 000000000..9dfc1c058
--- /dev/null
+++ b/samples/javascript/react_sample/src/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/samples/javascript/react_sample/src/reportWebVitals.js b/samples/javascript/react_sample/src/reportWebVitals.js
new file mode 100644
index 000000000..5253d3ad9
--- /dev/null
+++ b/samples/javascript/react_sample/src/reportWebVitals.js
@@ -0,0 +1,13 @@
+const reportWebVitals = onPerfEntry => {
+ if (onPerfEntry && onPerfEntry instanceof Function) {
+ import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
+ getCLS(onPerfEntry);
+ getFID(onPerfEntry);
+ getFCP(onPerfEntry);
+ getLCP(onPerfEntry);
+ getTTFB(onPerfEntry);
+ });
+ }
+};
+
+export default reportWebVitals;
diff --git a/samples/javascript/react_sample/src/setupTests.js b/samples/javascript/react_sample/src/setupTests.js
new file mode 100644
index 000000000..8f2609b7b
--- /dev/null
+++ b/samples/javascript/react_sample/src/setupTests.js
@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom';