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
10 changes: 8 additions & 2 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,9 @@ registers a second `shiny.reactOutput` binding.
- `useShinyOutputValue(id, defaultValue?, options?)` → `value`
- `defaultValue` is returned until the server delivers the first value, and
defaults to `undefined`
- a server `NULL` / `None` arrives as `null`, and the return type is
`T | null | undefined` so TypeScript makes callers handle it (#329)
- passing a `defaultValue` narrows it to `T | null` (an overload)
- it subscribes to the value channel only — status and error changes do not
re-render it
- the held value **is** reset when the id or namespace changes, so the
Expand Down Expand Up @@ -1381,8 +1384,11 @@ initial page.
- it exports `PROTOCOL_VERSION`
- it does **not** re-export React / ReactDOM
- the stylesheet ships as `dist-npm/style.css`, reachable as
`@posit-dev/shinyreact/styles` (#229; the build emitted no CSS at all before,
leaving `ImageOutput`'s spinner without its `@keyframes spin`)
`@posit-dev/shinyreact/styles.css` and `@posit-dev/shinyreact/styles`
- the `.css` name lets TypeScript match a `*.css` module declaration
(e.g. `vite/client`) with no hand-written `declare module` (#329)
- before #229 the build emitted no CSS at all, leaving `ImageOutput`'s
spinner without its `@keyframes spin`
- Vite does not inject a CSS import into a lib-mode ESM bundle, so
consumers opt in rather than having it forced on them
- a missing `#shinyreact-config` tag is tolerated, as in the IIFE bundle, so
Expand Down
7 changes: 7 additions & 0 deletions pkg-js/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
IDs were found" warning grew and the console logged `Output <id> not found`
(#330).

- `useShinyOutputValue()` is now typed `T | null | undefined`. A server
`NULL` / `None` arrives as `null`, which the old `T | undefined` type hid.
Passing a `defaultValue` narrows it to `T | null` (#329).

- The stylesheet is also exported as `@posit-dev/shinyreact/styles.css`, so
TypeScript resolves the import with no `declare module` (#329).

## 0.1.1

- First release published through CI with npm trusted publishing
Expand Down
2 changes: 1 addition & 1 deletion pkg-js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Skip that and the app still works — the registries are page-scoped, so both co

```jsx
import { useShinyInput, useShinyOutputValue } from "@posit-dev/shinyreact";
import "@posit-dev/shinyreact/styles";
import "@posit-dev/shinyreact/styles.css";
import { createRoot } from "react-dom/client";

function App() {
Expand Down
3 changes: 2 additions & 1 deletion pkg-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"types": "./dist-npm/types/npm.d.ts",
"import": "./dist-npm/index.js"
},
"./styles": "./dist-npm/style.css"
"./styles": "./dist-npm/style.css",
"./styles.css": "./dist-npm/style.css"
},
"scripts": {
"build": "npm run build:iife && npm run build:npm",
Expand Down
46 changes: 45 additions & 1 deletion pkg-js/src/shiny-react/__tests__/use-shiny-output-value.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
afterEach,
beforeEach,
describe,
expect,
expectTypeOf,
it,
vi,
} from "vitest";
import {
_resetReactRegistryForTesting,
getReactRegistry,
Expand Down Expand Up @@ -104,3 +112,39 @@ describe("useShinyOutputValue when the id changes", () => {
expect(result.current).toBe("second value");
});
});

describe("useShinyOutputValue when the server sends NULL", () => {
it("returns null, and its type says so", async () => {
// #329. A server `NULL` / `None` arrives as `null`. The return type used to
// be `T | undefined`, so `x !== undefined` narrowed to `T` and `x.n`
// crashed at runtime.
const registry = getReactRegistry();
const { result } = renderHook(() =>
useShinyOutputValue<{ n: number }>("x"),
);
await flush();

registry.outputs.get("x")!.setValue(null);
await flush();

expect(result.current).toBeNull();
expectTypeOf(result.current).toEqualTypeOf<
{ n: number } | null | undefined
>();
});
});

describe("useShinyOutputValue return type", () => {
it("drops undefined when a defaultValue is passed", () => {
// Type-only: a default means the value is never `undefined`, but a server
// NULL can still arrive as `null`.
expectTypeOf(
renderHook(() => useShinyOutputValue("x", 0)).result.current,
).toEqualTypeOf<number | null>();
expectTypeOf(
renderHook(() =>
useShinyOutputValue<number>("x", undefined, { namespace: null }),
).result.current,
).toEqualTypeOf<number | null | undefined>();
});
});
24 changes: 21 additions & 3 deletions pkg-js/src/shiny-react/use-shiny.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,27 @@ const NOOP_SETTER = () => {};
* @param options Optional configuration object.
* @param options.namespace Module namespace to apply (or `null` to suppress
* the surrounding `ShinyModuleProvider` namespace).
* @returns The current output value.
* @returns The current output value. `null` when the server sent `NULL` /
* `None` (or a silent `req()` error), `undefined` (or `defaultValue`) before
* the first value arrives. Passing a `defaultValue` drops `undefined` from the
* return type.
* @group Outputs
*/
export function useShinyOutputValue<T>(
outputId: string,
defaultValue?: undefined,
options?: { namespace?: string | null },
): T | null | undefined;
/**
* With a `defaultValue`, the value is never `undefined`: it is `defaultValue`
* until the server answers, then the server's value (`null` for `NULL` /
* `None`).
*/
export function useShinyOutputValue<T>(
outputId: string,
defaultValue: T,
options?: { namespace?: string | null },
): T | null;
export function useShinyOutputValue<T>(
outputId: string,
defaultValue: T | undefined = undefined,
Expand All @@ -226,8 +244,8 @@ export function useShinyOutputValue<T>(
}: {
namespace?: string | null;
} = {},
): T | undefined {
const [value, setValue] = useState<T | undefined>(defaultValue);
): T | null | undefined {
const [value, setValue] = useState<T | null | undefined>(defaultValue);
const shinyInitialized = useShinyInitialized();

ensureShinyReactInitialized();
Expand Down
Loading