Skip to content
Merged
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
43 changes: 43 additions & 0 deletions .changeset/eager-lies-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
"@michthemaker/vanjs": minor
---

Add Context API for logical state sharing across component trees

Introduces `createContext` and `useContext` functions that enable sharing reactive state without prop drilling. Context uses logical scoping (not DOM-based) and integrates seamlessly with VanJS's reactive state system.

**New exports:**

- `createContext<T>()`: Creates a new context object with a Provider method
- `useContext<T>(context)`: Retrieves the current context value (must be called within a Provider)

**Example usage:**

```typescript
import van, { createContext, useContext } from "@michthemaker/vanjs";

const { div, button } = van.tags;
const ThemeContext = createContext<{ color: string }>();

const theme = van.state({ color: "blue" });

ThemeContext.Provider(theme, () => {
const currentTheme = useContext(ThemeContext);
return div(
() => `Theme: ${currentTheme.val.color}`,
button(
{
onclick: () => (theme.val = { color: "red" }),
},
"Change Theme"
)
);
});
```

**Features:**

- Shallow reactivity: context values must be VanJS state objects
- Supports nested providers of the same context
- Type-safe with TypeScript generics
- Throws helpful errors when used incorrectly
5 changes: 5 additions & 0 deletions .changeset/funky-humans-open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@michthemaker/vanjs": patch
---

Add JSDoc documentation for Context API
35 changes: 34 additions & 1 deletion packages/vanjs/src/utils/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import type { ChildDom, State, StateView } from "../van.ts";

const IS_CONTEXT_OBJECT = Symbol("vanjs_is_context_object");

/**
* A context object for sharing reactive state across the component tree.
*
* @template T The type of the context's value.
*/
export type Context<T> = {
Provider: (
stateValue: State<T>,
Expand All @@ -11,12 +16,28 @@ export type Context<T> = {

const contextStacks = new Map<Context<any>, StateView<any>[]>();

/**
* Creates a context object for sharing state without prop drilling.
*
* @template T The type of value this context will hold
*
* @example
*
* ```ts
* const ThemeContext = createContext<{ color: string }>();
* const theme = van.state({ color: "blue" });
*
* ThemeContext.Provider(theme, () => {
* const currentTheme = useContext(ThemeContext);
* return div(() => `Color: ${currentTheme.val.color}`);
* });
* ```
*/
export let createContext = <T>(): Context<T> => {
return {
// @ts-ignore
[IS_CONTEXT_OBJECT]: true,
Provider: function provider(stateValue, childrenFn) {
// 1. Get or create stack for this context
if (!contextStacks.has(this)) {
contextStacks.set(this, []);
}
Expand All @@ -29,6 +50,18 @@ export let createContext = <T>(): Context<T> => {
};
};

/**
* Returns the current context value from the nearest Provider.
*
* @template T The type of the context's value
*
* @example
*
* ```ts
* const theme = useContext(ThemeContext);
* div(() => `Color: ${theme.val.color}`);
* ```
*/
export let useContext = <T>(context: Context<T>): State<T> => {
// @ts-ignore
if (!context[IS_CONTEXT_OBJECT]) throw new Error("Object is not a `Context`");
Expand Down
117 changes: 58 additions & 59 deletions packages/vanjs/src/van.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,5 @@
import type { ElementEventHandlers } from "./event-handlers.ts";

/*
Examples of React jsdoc comment types that clearly states what the API does
/**
* Created by {@link createRef}, or {@link useRef} when passed `null`.
*
* @template T The type of the ref's value.
*
* @example
*
* ```tsx
* const ref = createRef<HTMLDivElement>();
*
* ref.current = document.createElement('div'); // Error
* ```
*/
// interface RefObject<T> {
// /**
// * The current value of the ref.
//
// current: T;
// }
// */
//
//
// // This will technically work if you give a Consumer<T> or Provider<T> but it's deprecated and warns
// /**
// * Accepts a context object (the value returned from `React.createContext`) and returns the current
// * context value, as given by the nearest context provider for the given context.
// *
// * @version 16.8.0
// * @see {@link https://react.dev/reference/react/useContext}
// */
// function useContext<T>(context: Context<T> /*, (not public API) observedBits?: number|boolean */): T;
// /**
// * Returns a stateful value, and a function to update it.
// *
// * @version 16.8.0
// * @see {@link https://react.dev/reference/react/useState}
// */
// function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
//

export type {
ElementEventHandlers,
ReactiveEventHandler,
Expand All @@ -56,34 +14,29 @@ export type {
*
* ```ts
* const count = van.state(0);
*
* count.val; // read — tracked as dependency
* count.val = 5; // write — triggers reactive updates
* count.oldVal; // previous value before last update
* count.rawVal; // raw value, no dependency tracking
* count.val = 5; // triggers reactive updates
* ```
*/
export interface State<T> {
/**
* The current value of the state. Reading this property inside a binding or derive
* tracks it as a dependency. Writing to this property triggers reactive updates.
* The current value of the state. Reading tracks it as a dependency, writing triggers updates.
*/
val: T;

/**
* The previous value before the last update. Reading this property tracks it as a dependency.
* The previous value before the last update.
*/
readonly oldVal: T;

/**
* The raw value without dependency tracking. Use this to read the value without
* creating a reactive dependency.
* The raw value without dependency tracking.
*/
readonly rawVal: T;
}

// Defining readonly view of State<T> for covariance.
// Basically we want StateView<string> to implement StateView<string | number>
/**
* Readonly view of State<T> for covariance.
*/
export type StateView<T> = Readonly<State<T>>;

export type Val<T> = State<T> | T;
Expand All @@ -110,6 +63,11 @@ export type PropsWithKnownKeys<ElementType> = Partial<{
: K]: PropValueOrDerived;
}>;

/**
* A mutable ref object that holds a current value.
*
* @template T The type of the ref's value.
*/
export type Ref<T> = { current: T | null };

export type RefProp<T> = { ref?: Ref<T> };
Expand Down Expand Up @@ -137,43 +95,84 @@ export type TagFunc<Result extends Element> = (
...rest: readonly ChildDom[]
) => Result;

// HTML Tags - typed for all known HTML elements
/**
* HTML tag functions typed for all known HTML elements.
*/
type HTMLTags = Readonly<Record<string, TagFunc<Element>>> & {
[K in keyof HTMLElementTagNameMap]: TagFunc<HTMLElementTagNameMap[K]>;
};

// SVG Tags - typed for all known SVG elements
/**
* SVG tag functions typed for all known SVG elements.
*/
type SVGTags = Readonly<Record<string, TagFunc<SVGElement>>> & {
[K in keyof SVGElementTagNameMap]: TagFunc<SVGElementTagNameMap[K]>;
};

// MathML Tags - typed for all known MathML elements
/**
* MathML tag functions typed for all known MathML elements.
*/
type MathMLTags = Readonly<Record<string, TagFunc<MathMLElement>>> & {
[K in keyof MathMLElementTagNameMap]: TagFunc<MathMLElementTagNameMap[K]>;
};

// Namespace URIs
type SVGNamespaceURI = "http://www.w3.org/2000/svg";
type MathMLNamespaceURI = "http://www.w3.org/1998/Math/MathML";

// Namespace function overloads
/**
* Creates tag functions for elements in a specific namespace.
*/
type NamespacedTags = {
(namespaceURI: SVGNamespaceURI): SVGTags;
(namespaceURI: MathMLNamespaceURI): MathMLTags;
(namespaceURI: string): Readonly<Record<string, TagFunc<Element>>>;
};

/**
* Creates a reactive state object.
*
* @template T The type of the state's value.
*
* @example
*
* ```ts
* const count = van.state(0);
* const name = van.state("Alice");
* ```
*/
declare function state<T>(): State<T>;
declare function state<T>(initVal: T): State<T>;

/**
* The main VanJS interface.
*/
export interface Van {
/**
* Creates a reactive state object.
*/
readonly state: typeof state;

/**
* Creates derived state from a function.
*/
readonly derive: <T>(f: () => T) => State<T>;

/**
* Adds child elements to a DOM node.
*/
readonly add: (
dom: Element | DocumentFragment,
...children: readonly ChildDom[]
) => Element;

/**
* Tag functions for creating HTML, SVG, and MathML elements.
*/
readonly tags: HTMLTags & NamespacedTags;

/**
* Hydrates existing DOM with VanJS reactivity.
*/
readonly hydrate: <T extends Node>(
dom: T,
f: (dom: T) => T | null | undefined
Expand Down
Loading