Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 |
18 changes: 16 additions & 2 deletions src/MqttProvider/MqttProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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.
Expand All @@ -36,6 +43,7 @@ export function MqttProvider({
options,
httpBrokerUri,
httpOptions,
suspenseFallback,
children,
}: MqttProviderProps) {
const stableOptions = useMemo(() => options, [options])
Expand Down Expand Up @@ -65,7 +73,13 @@ export function MqttProvider({
connectionPromise,
}}
>
{children}
{suspenseFallback ? (
<Suspense fallback={suspenseFallback}>
<MqttSuspenseBoundary>{children}</MqttSuspenseBoundary>
</Suspense>
) : (
children
)}
</MqttContext.Provider>
</QueryClientProvider>
)
Expand Down
67 changes: 67 additions & 0 deletions tests/MqttProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -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<MqttClient>((resolve) => {
resolveConnection = resolve
})

// eslint-disable-next-line @typescript-eslint/unbound-method
vi.mocked(MqttClient.connect).mockReturnValue(connectionPromise)

const disconnect = vi.fn()

render(
<MqttProvider
uri="tcp://localhost:1883"
suspenseFallback={<div>Connecting to MQTT...</div>}
>
<div>Ready</div>
</MqttProvider>,
)

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<MqttClient>((_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(
<MqttProvider uri="tcp://localhost:1883">
<div>Ready</div>
</MqttProvider>,
)

expect(screen.getByText("Ready")).toBeInTheDocument()
})
})