diff --git a/samples/frameworks/react/arm-template.json b/samples/frameworks/react/arm-template.json
new file mode 100644
index 000000000000..e10d4cbf44d8
--- /dev/null
+++ b/samples/frameworks/react/arm-template.json
@@ -0,0 +1,154 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "baseName": {
+ "type": "string",
+ "defaultValue": "[resourceGroup().name]",
+ "metadata": {
+ "description": "The base resource name."
+ }
+ },
+ "origin": {
+ "type": "string",
+ "defaultValue": "http://localhost:3000",
+ "metadata": {
+ "description": "The application origin for configuring CORS policies. By default it'll be set to http://localhost:3000 locally."
+ }
+ }
+ },
+ "variables": {
+ "eventHubsName": "events",
+ "eventHubsNamespace": "[concat(parameters('baseName'), 'reactevents')]",
+ "storageAccount": "[concat(parameters('baseName'), 'reactstorage')]",
+ "storageContainer": "[concat(parameters('baseName'), 'reactstorage', '/default/blobs')]",
+ "location": "[resourceGroup().location]"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.EventHub/namespaces",
+ "apiVersion": "2018-01-01-preview",
+ "name": "[variables('eventHubsNamespace')]",
+ "location": "[variables('location')]",
+ "sku": {
+ "name": "Basic",
+ "tier": "Basic",
+ "capacity": 1
+ },
+ "properties": {
+ "zoneRedundant": false,
+ "isAutoInflateEnabled": false,
+ "maximumThroughputUnits": 0,
+ "kafkaEnabled": false
+ }
+ },
+ {
+ "type": "Microsoft.Storage/storageAccounts",
+ "apiVersion": "2020-08-01-preview",
+ "name": "[variables('storageAccount')]",
+ "location": "[variables('location')]",
+ "sku": {
+ "name": "Standard_RAGRS",
+ "tier": "Standard"
+ },
+ "kind": "StorageV2",
+ "properties": {
+ "networkAcls": {
+ "bypass": "AzureServices",
+ "virtualNetworkRules": [],
+ "ipRules": [],
+ "defaultAction": "Allow"
+ },
+ "supportsHttpsTrafficOnly": true,
+ "encryption": {
+ "services": {
+ "file": {
+ "keyType": "Account",
+ "enabled": true
+ },
+ "blob": {
+ "keyType": "Account",
+ "enabled": true
+ }
+ },
+ "keySource": "Microsoft.Storage"
+ },
+ "accessTier": "Hot"
+ }
+ },
+ {
+ "type": "Microsoft.EventHub/namespaces/eventhubs",
+ "apiVersion": "2017-04-01",
+ "name": "[concat(variables('eventHubsNamespace'), '/', variables('eventHubsName'))]",
+ "location": "[variables('location')]",
+ "dependsOn": [
+ "[resourceId('Microsoft.EventHub/namespaces', variables('eventHubsNamespace'))]"
+ ],
+ "properties": {
+ "messageRetentionInDays": 1,
+ "partitionCount": 2,
+ "status": "Active"
+ }
+ },
+ {
+ "type": "Microsoft.Storage/storageAccounts/blobServices",
+ "apiVersion": "2020-08-01-preview",
+ "name": "[concat(variables('storageAccount'), '/default')]",
+ "dependsOn": [
+ "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccount'))]"
+ ],
+ "sku": {
+ "name": "Standard_RAGRS",
+ "tier": "Standard"
+ },
+ "properties": {
+ "cors": {
+ "corsRules": [
+ {
+ "allowedOrigins": ["[parameters('origin')]"],
+ "allowedMethods": ["GET", "OPTIONS", "PUT", "POST"],
+ "maxAgeInSeconds": 0,
+ "exposedHeaders": ["*"],
+ "allowedHeaders": ["*"]
+ }
+ ]
+ },
+ "deleteRetentionPolicy": {
+ "enabled": false
+ }
+ }
+ },
+ {
+ "type": "Microsoft.Storage/storageAccounts/blobServices/containers",
+ "apiVersion": "2020-08-01-preview",
+ "name": "[variables('storageContainer')]",
+ "dependsOn": [
+ "[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storageAccount'), 'default')]",
+ "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccount'))]"
+ ],
+ "properties": {
+ "defaultEncryptionScope": "$account-encryption-key",
+ "denyEncryptionScopeOverride": false,
+ "publicAccess": "None"
+ }
+ }
+ ],
+ "outputs": {
+ "react_app_event_hubs_namespace": {
+ "type": "string",
+ "value": "[concat(variables('eventHubsNamespace'), '.servicebus.windows.net')]"
+ },
+ "react_app_event_hubs_name": {
+ "type": "string",
+ "value": "[variables('eventHubsName')]"
+ },
+ "react_app_blob_uri": {
+ "type": "string",
+ "value": "[reference(variables('storageAccount')).primaryEndpoints.blob]"
+ },
+ "react_app_blob_container": {
+ "type": "string",
+ "value": "[last(split(variables('storageContainer'), '/'))]"
+ }
+ }
+}
diff --git a/samples/frameworks/react/ts/.eslintrc b/samples/frameworks/react/ts/.eslintrc
new file mode 100644
index 000000000000..739bfa7e542a
--- /dev/null
+++ b/samples/frameworks/react/ts/.eslintrc
@@ -0,0 +1,24 @@
+{
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaVersion": 2020,
+ "sourceType": "module",
+ "ecmaFeatures": {
+ "jsx": true
+ }
+ },
+ "rules": {
+ "react/prop-types": 0
+ },
+ "settings": {
+ "react": {
+ "version": "detect"
+ }
+ },
+ "extends": [
+ "plugin:react/recommended",
+ "plugin:@typescript-eslint/recommended",
+ "prettier/@typescript-eslint",
+ "plugin:prettier/recommended"
+ ]
+}
diff --git a/samples/frameworks/react/ts/README.md b/samples/frameworks/react/ts/README.md
new file mode 100644
index 000000000000..e4b81200f2d8
--- /dev/null
+++ b/samples/frameworks/react/ts/README.md
@@ -0,0 +1,74 @@
+# Azure SDK samples for React (TypeScript)
+
+This sample application shows how to use the TypeScript client libraries for Azure in some common scenarios.
+
+In this sample, we build a simple Todo application in React using [create-react-app][react] and integrating with various Azure services.
+
+- Integration with Azure Event Hubs to support real-time updates across multiple instances of the application.
+- Integration with Azure Storage Blob for persisting ToDo items.
+
+## Prerequisites
+
+The samples are compatible with Node.js >= 8.0.0.
+
+Before running the samples in Node, they must be compiled to JavaScript using the TypeScript compiler. For more information on TypeScript, see the [TypeScript documentation][typescript].
+
+You need [an Azure subscription][freesub] and the following resources created to run this sample:
+
+- An Azure Event Hubs namespace. Please refer to the [Event Hubs documentation][eventhubs] for additional information on Event Hubs.
+- An Azure Storage Blob container. Please refer to the [Storage Blob documentation][storageblob] for additional information on Azure Storage Blob. This file will be fetched from Azure Storage Blob and displayed on the screen.
+- Finally, you'll need a way to authenticate the application with Azure. Please refer to the [@azure/identity][identity] package for information on authentication. The instructions below will walk you through the necessary steps.
+
+To quickly create the needed resources in Azure and to receive the necessary environment variables for them, you can deploy our sample template by clicking:
+
+[![](http://azuredeploy.net/deploybutton.png)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-sdk-for-js%2F6e519929f154b919e8a47245715076e73cd7915c%2Fsamples%2Fframeworks%2Freact%2Farm-template.json)
+
+The above template will create the necessary resources for you and the output tab will contain the exact environment variables that you'll need as soon as deployment succeeds. When the deployment is finished, head over to the "outputs" tab and copy the outputs to a local file - you'll need them in the next step.
+
+### Register a new application in AAD and assign the "Azure Event Hubs Data Owner" and "Azure Storage Blob Data Contributor" role to it.
+
+Authentication will still need to be set-up manually using the following instructions:
+
+- See https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app
+ to register a new application in the Azure Active Directory.
+- Note down the client id and tenant id from the above step.
+ You will need to set these in the .env file below.
+
+Ensure your app registration has been configured properly to allow the [implicit grant flow][implicitgrantflow]
+and allow both `Access tokens` and `ID tokens` to be issued by the authorization endpoint.
+In your app registration, you will also need to add a permission for the `Microsoft.EventHubs` and `Azure Storage` apps.
+When adding permission for `Microsoft.EventHubs` and `Azure Storage`, the type should be `delegated permissions` and the permission should be `user_impersonation`.
+
+## Running the sample
+
+Once the above created you'll want to ensure React has the necessary environment variables. To do this, copy `sample.env` as `.env` and provide the necessary environment variables to configure the application. You can get most values from the output tab of the deployment, and the client and tenant ID from the App registration step. Please note that environment variables should be upper case. For example: REACT_APP_TENANT_ID and REACT_APP_EVENT_HUBS_NAME.
+
+Install the various packages as well as the TypeScript compiler using:
+
+```bash
+npm install
+```
+
+Run the sample app:
+
+```bash
+npm start
+```
+
+Since this is a contrived example, you can only create new Todos and complete existing Todos. As you interact with the application, you'll notice EventHubs messages get written out to the console in the browser's developer tools. You may attach a note which will get uploaded as Blobs or fetch an existing note to display it on the screen.
+
+Additionally, you may open multiple instances of this sample application and watch as Todos synchronize in real-time.
+
+## Next Steps
+
+Take a look at our [API Documentation][apiref] for more information about the APIs that are avaiable.
+
+[react]: https://create-react-app.dev/
+[typescript]: https://www.typescriptlang.org/docs/home.html
+[freesub]: https://azure.microsoft.com/free
+[eventhubs]: https://docs.microsoft.com/javascript/api/@azure/event-hubs
+[servicebus]: https://docs.microsoft.com/javascript/api/@azure/service-bus
+[storageblob]: https://docs.microsoft.com/javascript/api/@azure/storage-blob
+[identity]: https://docs.microsoft.com/javascript/api/@azure/identity
+[apiref]: https://docs.microsoft.com/javascript/api/
+[implicitgrantflow]: https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-implicit-grant-flow
diff --git a/samples/frameworks/react/ts/package.json b/samples/frameworks/react/ts/package.json
new file mode 100644
index 000000000000..f75edf834f79
--- /dev/null
+++ b/samples/frameworks/react/ts/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "typescript-react-sample",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@azure/event-hubs": "^5.3.1",
+ "@azure/identity": "^1.2.0",
+ "@azure/storage-blob": "^12.3.0",
+ "react": "^17.0.1",
+ "react-dom": "^17.0.1",
+ "react-scripts": "4.0.1",
+ "uuid": "^8.3.1"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "@types/node": "^12.0.0",
+ "@types/react": "^16.9.53",
+ "@types/react-dom": "^16.9.8",
+ "@types/uuid": "^8.3.0",
+ "@typescript-eslint/eslint-plugin": "^4.9.0",
+ "@typescript-eslint/parser": "^4.9.0",
+ "eslint-config-prettier": "^6.15.0",
+ "eslint-plugin-prettier": "^3.1.4",
+ "typescript": "^4.0.3",
+ "prettier": "^1.16.4"
+ }
+}
diff --git a/samples/frameworks/react/ts/public/index.html b/samples/frameworks/react/ts/public/index.html
new file mode 100644
index 000000000000..c5045c5cb19f
--- /dev/null
+++ b/samples/frameworks/react/ts/public/index.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+ Todos React Sample
+
+
+
+
+
+
+
+
+
diff --git a/samples/frameworks/react/ts/public/manifest.json b/samples/frameworks/react/ts/public/manifest.json
new file mode 100644
index 000000000000..f9051fe711f4
--- /dev/null
+++ b/samples/frameworks/react/ts/public/manifest.json
@@ -0,0 +1,8 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/samples/frameworks/react/ts/public/robots.txt b/samples/frameworks/react/ts/public/robots.txt
new file mode 100644
index 000000000000..e9e57dc4d41b
--- /dev/null
+++ b/samples/frameworks/react/ts/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/samples/frameworks/react/ts/sample.env b/samples/frameworks/react/ts/sample.env
new file mode 100644
index 000000000000..2bfd1509f458
--- /dev/null
+++ b/samples/frameworks/react/ts/sample.env
@@ -0,0 +1,21 @@
+# Rename this to .env in order to have React automatically
+# pull in all these variables (anything that starts with REACT_APP_ will be
+# made available. See the documentation for adding custom environment variables
+# at the following link: https://create-react-app.dev/docs/adding-custom-environment-variables/)
+
+# fully qualified namespace for event hubs, typically: .servicebus.windows.net
+REACT_APP_EVENT_HUBS_NAMESPACE=""
+# The name of the Event Hubs hub
+REACT_APP_EVENT_HUBS_NAME=""
+
+# URI for the Azure Blob Storage, typically https://.blob.core.windows.net/
+REACT_APP_BLOB_URI=""
+# the name of the Azure Blob Storage container
+REACT_APP_BLOB_CONTAINER=""
+
+# Used to authenticate using Azure AD as a service principal for role-based authentication.
+#
+# See the documentation for `InteractiveBrowserCredential` at the following link:
+# https://docs.microsoft.com/javascript/api/@azure/identity/interactivebrowsercredential
+REACT_APP_CLIENT_ID=""
+REACT_APP_TENANT_ID=""
diff --git a/samples/frameworks/react/ts/src/App.tsx b/samples/frameworks/react/ts/src/App.tsx
new file mode 100644
index 000000000000..637cb4ac389b
--- /dev/null
+++ b/samples/frameworks/react/ts/src/App.tsx
@@ -0,0 +1,17 @@
+/*
+ Copyright (c) Microsoft Corporation.
+ Licensed under the MIT license.
+*/
+
+import React from "react";
+import TodoList from "./components/TodoList";
+
+function App(): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+export default App;
diff --git a/samples/frameworks/react/ts/src/components/TodoItem.tsx b/samples/frameworks/react/ts/src/components/TodoItem.tsx
new file mode 100644
index 000000000000..60db53854d9c
--- /dev/null
+++ b/samples/frameworks/react/ts/src/components/TodoItem.tsx
@@ -0,0 +1,42 @@
+/*
+ Copyright (c) Microsoft Corporation.
+ Licensed under the MIT license.
+*/
+
+import React from "react";
+import { Todo } from "../hooks/useTodos";
+
+const TodoItem: React.FC<{
+ todo: Todo;
+ onToggleComplete: (event: React.ChangeEvent) => void;
+ onNotesClick: (event: React.MouseEvent) => void;
+ onNoteUploadClick: (event: React.MouseEvent) => void;
+}> = ({ todo, onToggleComplete, onNotesClick, onNoteUploadClick }) => {
+ return (
+
+ );
+};
+
+export default TodoItem;
diff --git a/samples/frameworks/react/ts/src/components/TodoList.tsx b/samples/frameworks/react/ts/src/components/TodoList.tsx
new file mode 100644
index 000000000000..b6464b107b71
--- /dev/null
+++ b/samples/frameworks/react/ts/src/components/TodoList.tsx
@@ -0,0 +1,100 @@
+/*
+ Copyright (c) Microsoft Corporation.
+ Licensed under the MIT license.
+*/
+
+import React, { useState } from "react";
+import { v4 as guid } from "uuid";
+import TodoItem from "./TodoItem";
+import { useTodos, Todo } from "../hooks/useTodos";
+
+export default function TodoList(): JSX.Element {
+ // Initialize the hooks needed for integrating with our Azure services.
+ const [todos, addTodo, updateTodo, getNote, uploadNote] = useTodos();
+ const [note, setNote] = useState();
+
+ const [newTodoLabel, setNewTodoLabel] = useState("");
+
+ // Handle data binding for the new todo label.
+ const onNewLabelChange = (el: React.ChangeEvent) => {
+ setNewTodoLabel(el.target.value);
+ };
+
+ // Handle the Enter key press which will add a new Todo.
+ const onKeyPress = async (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ const newTodo = {
+ done: false,
+ id: guid(),
+ label: newTodoLabel
+ };
+
+ await addTodo(newTodo);
+ setNewTodoLabel("");
+ }
+ };
+
+ // Handle completion of a Todo item.
+ const onToggleComplete = (todo: Todo) => async () => {
+ const updatedTodo: Todo = { ...todo, done: !todo.done };
+ await updateTodo(updatedTodo);
+ };
+
+ // Handle fetching a Todo's note and displaying its text.
+ const onNotesClick = (todo: Todo) => async (e: React.MouseEvent) => {
+ e.preventDefault();
+ let text: string | undefined = "";
+ if (todo.noteFileName) {
+ text = await getNote(todo);
+ }
+ setNote(text);
+ };
+
+ // Handle uploading the current timestamp as a note.
+ const onNoteUploadClick = (todo: Todo) => async (
+ e: React.MouseEvent
+ ) => {
+ e.preventDefault();
+ await uploadNote(todo, `Note created on ${new Date()}`);
+ };
+
+ return (
+
+
+
+
Todos React Sample
+
+
+
+
+
+
+
+
+ {todos.map((todo) => (
+
+ ))}
+
+ {note && (
+
+
+ Note contents: {note}
+
+
+ )}
+
+ );
+}
diff --git a/samples/frameworks/react/ts/src/hooks/useBlobs.ts b/samples/frameworks/react/ts/src/hooks/useBlobs.ts
new file mode 100644
index 000000000000..71286f03789e
--- /dev/null
+++ b/samples/frameworks/react/ts/src/hooks/useBlobs.ts
@@ -0,0 +1,75 @@
+/*
+ Copyright (c) Microsoft Corporation.
+ Licensed under the MIT license.
+
+ This sample demonstrates how to create a React hook integrating
+ with Azure Blob Storage.
+
+ For more information on Azure Blob Storage please see
+ https://www.npmjs.com/package/@azure/storage-blob
+*/
+
+import { useEffect, useRef } from "react";
+import { BlobServiceClient, ContainerClient } from "@azure/storage-blob";
+import { credential, getEnvironmentVariable } from "../utils";
+
+type GetBlob = (blobName: string) => Promise;
+type UploadBlob = (blobName: string, content: string) => Promise;
+type Hook = () => [GetBlob, UploadBlob];
+
+/**
+ * The Azure Blob hook exposes a methods to interact
+ * With Azure Blob Storage.
+ */
+const useBlobs: Hook = () => {
+ // Keep a reference to a client for a Blob Container
+ // in order to lazy-load it as needed.
+ // For more information about Azure Blob Storage container
+ // Please refer to https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blobs-introduction#blob-storage-resources
+ const instance = useRef();
+
+ /**
+ * Fetch a Blob from Azure Blob Storage, returning its body if the blob exists.
+ * @param blobName The name of the blob within the container.
+ */
+ const getBlob = async (blobName: string): Promise => {
+ if (!instance.current) {
+ throw new Error("[useBlobs]: Instance never initialized.");
+ }
+ const blob = instance.current.getBlobClient(blobName);
+ const response = await blob.download();
+ return response.blobBody;
+ };
+
+ /**
+ * Upload a string to Azure blob Stroage, overwriting it if it already exists.
+ * @param blobName The name of the blob within the container.
+ * @param content The file contents.
+ */
+ const uploadBlob = async (blobName: string, content: string): Promise => {
+ if (!instance.current) {
+ throw new Error("[useBLobs]: Instance never initialized.");
+ }
+ const blockBlobClient = instance.current.getBlockBlobClient(blobName);
+ await blockBlobClient.upload(content, content.length);
+ };
+
+ useEffect(() => {
+ if (!instance.current) {
+ const uri = getEnvironmentVariable("REACT_APP_BLOB_URI");
+ const containerName = getEnvironmentVariable("REACT_APP_BLOB_CONTAINER");
+
+ // In this sample we fetch all blobs from a single container, creating
+ // a client for that container to be used for fetching Blobs
+ const blobStorageClient = new BlobServiceClient(uri, credential);
+
+ const containerClient = blobStorageClient.getContainerClient(containerName);
+
+ instance.current = containerClient;
+ }
+ }, []);
+
+ return [getBlob, uploadBlob];
+};
+
+export { useBlobs };
diff --git a/samples/frameworks/react/ts/src/hooks/useEventHubs.ts b/samples/frameworks/react/ts/src/hooks/useEventHubs.ts
new file mode 100644
index 000000000000..e02bfe2da59c
--- /dev/null
+++ b/samples/frameworks/react/ts/src/hooks/useEventHubs.ts
@@ -0,0 +1,104 @@
+/*
+ Copyright (c) Microsoft Corporation.
+ Licensed under the MIT license.
+
+ This sample demonstrates how to create a React hook integrating
+ with Azure Event Hubs.
+
+ For more information on Azure Event Hubs please see
+ https://www.npmjs.com/package/@azure/event-hubs
+*/
+
+import { useEffect, useRef } from "react";
+import {
+ EventData,
+ EventHubConsumerClient,
+ EventHubProducerClient,
+ SubscriptionEventHandlers
+} from "@azure/event-hubs";
+import { credential, getEnvironmentVariable } from "../utils";
+
+type Hook = (
+ callBack: (EventData: EventData) => Promise
+) => (event: EventData) => Promise;
+
+/**
+ * The EventHubs hook accepts a callback function and returns a function
+ * that allows you to publish events to EventHubs.
+ * @param callback The function to be called for every EventHubs message
+ */
+const useEventHubs: Hook = (callback) => {
+ // Keep a reference on our consumer and producer EventHubs
+ // clients in order to lazy-load them as needed.
+ const consumer = useRef();
+ const producer = useRef();
+
+ /**
+ * Publish an event to an EventHubs instance using the
+ * settings defined in environment variables.
+ * @param event The event to publish to EventHubs.
+ */
+ const publishEvent = async (event: EventData) => {
+ if (!producer.current) {
+ throw new Error("[EventHubs]: Producer never initialized!");
+ }
+ console.log("[EventHubs]: Publishing Event", event);
+ await producer.current.sendBatch([event]);
+ };
+
+ // Define various handlers for processing events and errors
+ // For simplicity we just pass each event to the consumer
+ // client of this hook.
+ const consumerHandlers: SubscriptionEventHandlers = {
+ processEvents: async (events) => {
+ for (const event of events) {
+ console.log("[EventHubs]: Received Event", event);
+ await callback(event);
+ }
+ },
+ processError: async (err) => {
+ console.log("[EventHubs]: Received Error", err);
+ }
+ };
+
+ useEffect(() => {
+ const namespace = getEnvironmentVariable("REACT_APP_EVENT_HUBS_NAMESPACE");
+ const eventHubsName = getEnvironmentVariable("REACT_APP_EVENT_HUBS_NAME");
+
+ if (!consumer.current) {
+ // Create a new consumer using the default consumer group.
+ // For more information about consumer groups please refer to
+ // https://docs.microsoft.com/en-us/azure/event-hubs/event-hubs-features#consumer-groups
+ consumer.current = new EventHubConsumerClient(
+ "$Default",
+ namespace,
+ eventHubsName,
+ credential
+ );
+
+ // Alternatively you may decide to ignore any events that
+ // occurred before the consumer subscribed using `latestEventPosition`.
+ // In this sample, since we want to ensure we are synchronized to all other
+ // consumers we can start at the latest event position, but use the `isInclusive`
+ // flag to receive the last event that was posted to the server.
+ consumer.current.subscribe(consumerHandlers, {
+ startPosition: { isInclusive: true, offset: "@latest" }
+ });
+ }
+
+ if (!producer.current) {
+ producer.current = new EventHubProducerClient(namespace, eventHubsName, credential);
+ }
+
+ // Close the connections to EventHubs when this hook is
+ // cleaned up.
+ return () => {
+ producer.current?.close();
+ consumer.current?.close();
+ };
+ }, []);
+
+ return publishEvent;
+};
+
+export { useEventHubs };
diff --git a/samples/frameworks/react/ts/src/hooks/useTodos.ts b/samples/frameworks/react/ts/src/hooks/useTodos.ts
new file mode 100644
index 000000000000..5fa1f26da770
--- /dev/null
+++ b/samples/frameworks/react/ts/src/hooks/useTodos.ts
@@ -0,0 +1,103 @@
+/*
+ Copyright (c) Microsoft Corporation.
+ Licensed under the MIT license.
+
+ The central hook that components will integrate with and
+ manages both the state of Todos and integration with various
+ Azure services such as ServiceBus and EventHubs.
+*/
+
+import { EventData } from "@azure/event-hubs";
+import { useState } from "react";
+import { useBlobs } from "./useBlobs";
+import { useEventHubs } from "./useEventHubs";
+
+export interface Todo {
+ done: boolean;
+ id: string;
+ label: string;
+ noteFileName?: string;
+}
+
+type Todos = Todo[];
+type AddTodo = (todo: Todo) => Promise;
+type UpdateTodo = (todo: Todo) => Promise;
+type GetNote = (todo: Todo) => Promise;
+type UploadNote = (todo: Todo, content: string) => Promise;
+type Hook = [Todos, AddTodo, UpdateTodo, GetNote, UploadNote];
+
+/**
+ * The useTodos hook is the main entrypoint for most of the todo
+ * lifecycle management. It exposes the current todos state, a method
+ * to add a new todo, a method to update an existing todo, and methods
+ * to interact with notes.
+ */
+export const useTodos: () => Hook = () => {
+ // Start with an empty list of Todos for this example.
+ // In a production application you might fetch these from
+ // a database like Cosmos DB on render.
+ // If there was any activity within the last EventHubs retention period
+ // it'll be reflected and read in and the state updated.
+ const [todos, setTodos] = useState([]);
+
+ // Add functionality to integrate with Azure Storage Blobs.
+ const [getBlob, uploadBlob] = useBlobs();
+
+ // An example of using EventHubs to process events which will
+ // be published to every consumer. For simplicity we just override
+ // our internal state with whatever the event data is. In a production
+ // application you'll likely want to use some database as the source
+ // of truth.
+ const onEventHubMessage = async (message: EventData) => {
+ const todos = message.body as Array;
+ if (todos) {
+ setTodos(todos);
+ }
+ };
+ const publishToEventHubs = useEventHubs(onEventHubMessage);
+
+ /**
+ * Adds a new todo to the collection.
+ * @param todo The todo item to add.
+ */
+ const addTodo = async (todo: Todo): Promise => onChange([todo, ...todos]);
+
+ /**
+ * Updates an existing todo by replacing it with the argument.
+ * @param todo The updated todo data.
+ */
+ const updateTodo = async (todo: Todo): Promise => {
+ const newTodos = todos.map((t) => (t.id === todo.id ? todo : t));
+ onChange(newTodos);
+ };
+
+ /**
+ * Uploads a note to Azure Blob Storage and attaches it to an existing todo.
+ * @param todo The todo to associate this note with.
+ * @param content The note's contents.
+ */
+ const uploadNote = async (todo: Todo, content: string): Promise => {
+ todo.noteFileName = `${todo.id}.txt`;
+ await uploadBlob(todo.noteFileName, content);
+ updateTodo(todo);
+ };
+
+ /**
+ * Fetches a note from Azure Blob Storage and returns its contents.
+ * @param todo The todo item that owns this note.
+ */
+ const getNote = async (todo: Todo): Promise => {
+ if (todo.noteFileName) {
+ const blob = await getBlob(todo.noteFileName);
+ return await blob?.text();
+ }
+ };
+
+ // onChange will publish the new set of todos to all the clients.
+ const onChange = async (newTodos: Todo[]): Promise => {
+ setTodos(newTodos);
+ publishToEventHubs({ body: newTodos });
+ };
+
+ return [todos, addTodo, updateTodo, getNote, uploadNote];
+};
diff --git a/samples/frameworks/react/ts/src/index.tsx b/samples/frameworks/react/ts/src/index.tsx
new file mode 100644
index 000000000000..9de45854452b
--- /dev/null
+++ b/samples/frameworks/react/ts/src/index.tsx
@@ -0,0 +1,15 @@
+/*
+ Copyright (c) Microsoft Corporation.
+ Licensed under the MIT license.
+*/
+
+import React from "react";
+import ReactDOM from "react-dom";
+import App from "./App";
+
+ReactDOM.render(
+
+
+ ,
+ document.getElementById("root")
+);
diff --git a/samples/frameworks/react/ts/src/react-app-env.d.ts b/samples/frameworks/react/ts/src/react-app-env.d.ts
new file mode 100644
index 000000000000..03bacdd655fe
--- /dev/null
+++ b/samples/frameworks/react/ts/src/react-app-env.d.ts
@@ -0,0 +1,5 @@
+/*
+ Copyright (c) Microsoft Corporation.
+ Licensed under the MIT license.
+*/
+///
diff --git a/samples/frameworks/react/ts/src/utils/auth.ts b/samples/frameworks/react/ts/src/utils/auth.ts
new file mode 100644
index 000000000000..afdd7854a1c0
--- /dev/null
+++ b/samples/frameworks/react/ts/src/utils/auth.ts
@@ -0,0 +1,18 @@
+/*
+ Copyright (c) Microsoft Corporation.
+ Licensed under the MIT license.
+
+ This sample demonstrates how to create and share an `InteractiveBrowserCredential`
+ to authenticate client-side requests in a single-page application.
+
+ For more information on the authentication strategies available for
+ client-side applications, please refer to
+ https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/identity/identity/samples/ClientSideUserAuthentication.md.
+*/
+
+import { InteractiveBrowserCredential } from "@azure/identity";
+import { getEnvironmentVariable } from ".";
+
+const clientId = getEnvironmentVariable("REACT_APP_CLIENT_ID");
+const tenantId = getEnvironmentVariable("REACT_APP_TENANT_ID");
+export const credential = new InteractiveBrowserCredential({ clientId, tenantId });
diff --git a/samples/frameworks/react/ts/src/utils/index.ts b/samples/frameworks/react/ts/src/utils/index.ts
new file mode 100644
index 000000000000..9a41ff14c94d
--- /dev/null
+++ b/samples/frameworks/react/ts/src/utils/index.ts
@@ -0,0 +1,15 @@
+/*
+ Copyright (c) Microsoft Corporation.
+ Licensed under the MIT license.
+*/
+
+import { credential } from "./auth";
+
+function getEnvironmentVariable(name: string): string {
+ const value = process.env[name.toUpperCase()] || process.env[name.toLowerCase()];
+ if (!value) {
+ throw new Error(`Environment variable ${name} is not defined.`);
+ }
+ return value;
+}
+export { getEnvironmentVariable, credential };
diff --git a/samples/frameworks/react/ts/tsconfig.json b/samples/frameworks/react/ts/tsconfig.json
new file mode 100644
index 000000000000..41646dcfeda0
--- /dev/null
+++ b/samples/frameworks/react/ts/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx"
+ },
+ "include": ["src"]
+}