diff --git a/README.md b/README.md index 6364a45..31d2cbc 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ npm install @artcom/mqtt-topping-react ### MqttProvider -Wrap your application with `MqttProvider`. You can provide a `suspenseFallback` to handle the initial connection state automatically. +Wrap your application with `MqttProvider`. When you provide a `suspenseFallback`, the provider will suspend rendering its children until the initial MQTT connection succeeds and show the fallback in the meantime. ```tsx import { MqttProvider } from "@artcom/mqtt-topping-react" @@ -137,5 +137,5 @@ function MyComponent() { | `options` | `MqttClientOptions` | Optional configuration for the MQTT client | | `httpBrokerUri` | `string` | Optional URI for the HTTP interface of the broker | | `httpOptions` | `HttpClientOptions` | Optional configuration for the HTTP client | -| `suspenseFallback` | `ReactNode` | Optional fallback UI to show while connecting | +| `suspenseFallback` | `ReactNode` | Optional fallback UI shown while the initial MQTT connection is pending | | `children` | `ReactNode` | Child components | diff --git a/src/MqttProvider/MqttProvider.tsx b/src/MqttProvider/MqttProvider.tsx index a52defc..2a450d1 100644 --- a/src/MqttProvider/MqttProvider.tsx +++ b/src/MqttProvider/MqttProvider.tsx @@ -4,8 +4,9 @@ import { type MqttClientOptions, } from "@artcom/mqtt-topping" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import React, { useMemo } from "react" +import React, { Suspense, useMemo } from "react" +import { useMqttSuspense } from "../useMqttSuspense" import { MqttContext } from "./MqttContext" import { useMqttConnection } from "./useMqttConnection" @@ -14,11 +15,17 @@ export interface MqttProviderProps { options?: MqttClientOptions httpBrokerUri?: string httpOptions?: HttpClientOptions + suspenseFallback?: React.ReactNode children: React.ReactNode } const defaultQueryClient = new QueryClient() +function MqttSuspenseBoundary({ children }: { children: React.ReactNode }) { + useMqttSuspense() + return <>{children} +} + /** * Provider component for MQTT context. * Manages the MQTT connection and provides the client instance to children. @@ -36,6 +43,7 @@ export function MqttProvider({ options, httpBrokerUri, httpOptions, + suspenseFallback, children, }: MqttProviderProps) { const stableOptions = useMemo(() => options, [options]) @@ -65,7 +73,13 @@ export function MqttProvider({ connectionPromise, }} > - {children} + {suspenseFallback ? ( + + {children} + + ) : ( + children + )} ) diff --git a/tests/MqttProvider.test.tsx b/tests/MqttProvider.test.tsx new file mode 100644 index 0000000..fcf497f --- /dev/null +++ b/tests/MqttProvider.test.tsx @@ -0,0 +1,67 @@ +import { MqttClient } from "@artcom/mqtt-topping" +import { render, screen, waitFor } from "@testing-library/react" +import { beforeEach, describe, expect, it, vi } from "vitest" + +import { MqttProvider } from "../src/MqttProvider/MqttProvider" + +vi.mock("@artcom/mqtt-topping", () => { + return { + HttpClient: vi.fn().mockImplementation(() => ({ queryJson: vi.fn() })), + MqttClient: { + connect: vi.fn(), + }, + } +}) + +describe("MqttProvider", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("renders suspenseFallback while the MQTT connection is pending", async () => { + let resolveConnection!: (client: MqttClient) => void + const connectionPromise = new Promise((resolve) => { + resolveConnection = resolve + }) + + // eslint-disable-next-line @typescript-eslint/unbound-method + vi.mocked(MqttClient.connect).mockReturnValue(connectionPromise) + + const disconnect = vi.fn() + + render( + Connecting to MQTT...} + > +
Ready
+
, + ) + + expect(screen.getByText("Connecting to MQTT...")).toBeInTheDocument() + expect(screen.queryByText("Ready")).not.toBeInTheDocument() + + resolveConnection({ disconnect } as unknown as MqttClient) + + await waitFor(() => { + expect(screen.getByText("Ready")).toBeInTheDocument() + }) + }) + + it("renders children immediately when suspenseFallback is not provided", () => { + const neverSettlesPromise = new Promise((_resolve) => { + // Intentionally unresolved: keeps the connection in-flight for this assertion. + }) + + // eslint-disable-next-line @typescript-eslint/unbound-method + vi.mocked(MqttClient.connect).mockReturnValue(neverSettlesPromise) + + render( + +
Ready
+
, + ) + + expect(screen.getByText("Ready")).toBeInTheDocument() + }) +})