@diskette/use-render 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -98
- package/dist/index.d.ts +2 -1
- package/dist/types.d.ts +13 -5
- package/dist/use-render.d.ts +3 -9
- package/dist/use-render.js +3 -3
- package/dist/use-render.test.jsx +1 -0
- package/dist/utils.d.ts +7 -6
- package/dist/utils.js +8 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# useRender
|
|
2
2
|
|
|
3
|
-
A React hook for component libraries that
|
|
3
|
+
A React hook for component libraries that lets consumers swap the rendered element—like rendering a `<a>` instead of a `<button>`—while keeping all your component's behavior intact. It handles the tricky parts: merging refs, combining props, and letting className and style respond to internal state.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,64 +8,54 @@ A React hook for component libraries that enables flexible render prop patterns,
|
|
|
8
8
|
pnpm add @diskette/use-render
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Quick Start
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
- **State-aware className** - Resolve classNames dynamically based on component state
|
|
15
|
-
- **State-aware style** - Resolve styles dynamically based on component state
|
|
16
|
-
- **Automatic ref merging** - Combines multiple refs seamlessly
|
|
17
|
-
- **Children as render function** - Pass children as a function receiving state
|
|
18
|
-
|
|
19
|
-
## Usage
|
|
20
|
-
|
|
21
|
-
### Building a Component
|
|
22
|
-
|
|
23
|
-
Use `useRender` inside your component to enable the render prop pattern:
|
|
13
|
+
Here's a simple button component that consumers can render as any element:
|
|
24
14
|
|
|
25
15
|
```tsx
|
|
26
16
|
import { useRender, type ComponentProps } from '@diskette/use-render'
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
isPressed: boolean
|
|
30
|
-
isHovered: boolean
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
type ButtonProps = ComponentProps<'button', ButtonState>
|
|
18
|
+
type ButtonProps = ComponentProps<'button', { isPressed: boolean }>
|
|
34
19
|
|
|
35
20
|
function Button(props: ButtonProps) {
|
|
36
21
|
const [isPressed, setIsPressed] = useState(false)
|
|
37
|
-
const [isHovered, setIsHovered] = useState(false)
|
|
38
|
-
|
|
39
|
-
const state: ButtonState = { isPressed, isHovered }
|
|
40
22
|
|
|
41
|
-
return useRender(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
23
|
+
return useRender(
|
|
24
|
+
'button',
|
|
25
|
+
{ isPressed },
|
|
26
|
+
{
|
|
27
|
+
baseProps: {
|
|
28
|
+
className: 'btn',
|
|
29
|
+
onMouseDown: () => setIsPressed(true),
|
|
30
|
+
onMouseUp: () => setIsPressed(false),
|
|
31
|
+
},
|
|
32
|
+
props,
|
|
48
33
|
},
|
|
49
|
-
|
|
50
|
-
})
|
|
34
|
+
)
|
|
51
35
|
}
|
|
52
36
|
```
|
|
53
37
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
#### Default Usage
|
|
38
|
+
Now consumers can use it normally, or swap the element entirely:
|
|
57
39
|
|
|
58
40
|
```tsx
|
|
41
|
+
// Renders a <button>
|
|
59
42
|
<Button className="primary">Click me</Button>
|
|
43
|
+
|
|
44
|
+
// Renders an <a> with all the button's behavior
|
|
45
|
+
<Button render={<a href="/path" />}>Go somewhere</Button>
|
|
60
46
|
```
|
|
61
47
|
|
|
62
|
-
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### Overriding the `Element`
|
|
51
|
+
|
|
52
|
+
Pass a JSX element to `render` and it will be cloned with your component's props:
|
|
63
53
|
|
|
64
54
|
```tsx
|
|
65
55
|
<Button render={<a href="/path" />}>Link styled as button</Button>
|
|
66
56
|
```
|
|
67
57
|
|
|
68
|
-
|
|
58
|
+
Or pass a function to get full control over rendering, with access to both props and state:
|
|
69
59
|
|
|
70
60
|
```tsx
|
|
71
61
|
<Button
|
|
@@ -77,9 +67,9 @@ function Button(props: ButtonProps) {
|
|
|
77
67
|
</Button>
|
|
78
68
|
```
|
|
79
69
|
|
|
80
|
-
### State-Aware className
|
|
70
|
+
### State-Aware `className`
|
|
81
71
|
|
|
82
|
-
Pass a function to
|
|
72
|
+
Pass a function to compute `className` based on component state. The second argument gives you access to the base `className` set by the component:
|
|
83
73
|
|
|
84
74
|
```tsx
|
|
85
75
|
<Button
|
|
@@ -91,15 +81,15 @@ Pass a function to resolve className based on component state:
|
|
|
91
81
|
</Button>
|
|
92
82
|
```
|
|
93
83
|
|
|
94
|
-
Or pass a string
|
|
84
|
+
Or just pass a string. It will be merged with the default `className`:
|
|
95
85
|
|
|
96
86
|
```tsx
|
|
97
87
|
<Button className="primary large">Click me</Button>
|
|
98
88
|
```
|
|
99
89
|
|
|
100
|
-
### State-Aware style
|
|
90
|
+
### State-Aware style `(CSSProperties)`
|
|
101
91
|
|
|
102
|
-
|
|
92
|
+
Same pattern works for inline styles:
|
|
103
93
|
|
|
104
94
|
```tsx
|
|
105
95
|
<Button
|
|
@@ -114,83 +104,53 @@ Pass a function to resolve styles based on component state:
|
|
|
114
104
|
|
|
115
105
|
### Children as Render Function
|
|
116
106
|
|
|
117
|
-
Access state
|
|
107
|
+
Access state by passing a function:
|
|
118
108
|
|
|
119
109
|
```tsx
|
|
120
110
|
<Button>{(state) => (state.isPressed ? 'Pressing...' : 'Click me')}</Button>
|
|
121
111
|
```
|
|
122
112
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
### `useRender(tag, options)`
|
|
126
|
-
|
|
127
|
-
```ts
|
|
128
|
-
function useRender<T extends ElementType, S>(
|
|
129
|
-
tag: T,
|
|
130
|
-
state,
|
|
131
|
-
options: UseRenderOptions<T, S>,
|
|
132
|
-
): ReactNode
|
|
133
|
-
```
|
|
113
|
+
### `render` vs `children` as Function
|
|
134
114
|
|
|
135
|
-
|
|
115
|
+
You might wonder why both exist—most libraries pick one or the other. They serve different purposes:
|
|
136
116
|
|
|
137
|
-
-
|
|
138
|
-
- `
|
|
117
|
+
- **`render`** swaps the element itself. Use it when you need a `<a>` instead of a `<button>`, or want to integrate with a router's `<Link>`.
|
|
118
|
+
- **`children` as function** changes what's inside the element. Use it when the content should react to internal state.
|
|
139
119
|
|
|
140
|
-
|
|
120
|
+
They compose naturally—you can use both at once:
|
|
141
121
|
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
ref?: React.Ref<any> | (React.Ref<any> | undefined)[]
|
|
147
|
-
}
|
|
122
|
+
```tsx
|
|
123
|
+
<Button render={<a href="/path" />}>
|
|
124
|
+
{(state) => (state.isPressed ? 'Going...' : 'Go somewhere')}
|
|
125
|
+
</Button>
|
|
148
126
|
```
|
|
149
127
|
|
|
150
|
-
|
|
151
|
-
| ----------- | ------------------------------------------------------------------ |
|
|
152
|
-
| `baseProps` | Base props applied to the element |
|
|
153
|
-
| `props` | Consumer-provided props (typically forwarded from component props) |
|
|
154
|
-
| `ref` | Ref(s) to merge with the consumer's ref |
|
|
155
|
-
|
|
156
|
-
### `ComponentProps<T, S>`
|
|
157
|
-
|
|
158
|
-
Type for component's external public props. Components using `useRender` should use this type (or extend from it) instead of `React.ComponentProps`. It's essentially `React.ComponentProps<T>` augmented with state-aware `className`, `style`, `children`, and the `render` prop:
|
|
159
|
-
|
|
160
|
-
```ts
|
|
161
|
-
type ComponentProps<T extends ElementType, S> = BaseComponentProps<T> & {
|
|
162
|
-
children?: ((state: S) => ReactNode) | ReactNode
|
|
163
|
-
className?: ClassNameResolver<S>
|
|
164
|
-
style?: StyleResolver<S>
|
|
165
|
-
render?: Renderer<T, S> | JSX.Element
|
|
166
|
-
}
|
|
167
|
-
```
|
|
128
|
+
## API
|
|
168
129
|
|
|
169
|
-
### `
|
|
130
|
+
### `useRender(tag, state, options)`
|
|
170
131
|
|
|
171
132
|
```ts
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
133
|
+
function useRender<T extends ElementType, S>(
|
|
134
|
+
tag: T,
|
|
135
|
+
state: S,
|
|
136
|
+
options: UseRenderOptions<T, S>,
|
|
137
|
+
): ReactNode
|
|
176
138
|
```
|
|
177
139
|
|
|
178
|
-
|
|
140
|
+
| Parameter | Description |
|
|
141
|
+
| ------------------- | ------------------------------------------------------------------ |
|
|
142
|
+
| `tag` | Default element type (e.g., `'button'`, `'div'`) |
|
|
143
|
+
| `state` | Component state passed to resolvers and render functions |
|
|
144
|
+
| `options.baseProps` | Base props applied to the element (your component's defaults) |
|
|
145
|
+
| `options.props` | Consumer-provided props (typically forwarded from component props) |
|
|
146
|
+
| `options.ref` | Ref(s) to merge with the consumer's ref |
|
|
179
147
|
|
|
180
|
-
|
|
181
|
-
type StyleResolver<S> =
|
|
182
|
-
| ((state: S, baseStyle?: CSSProperties) => CSSProperties | undefined)
|
|
183
|
-
| CSSProperties
|
|
184
|
-
| undefined
|
|
185
|
-
```
|
|
148
|
+
### `ComponentProps<ElementType, State>`
|
|
186
149
|
|
|
187
|
-
|
|
150
|
+
Use this type for your component's public props. It extends `React.ComponentProps<T>` and augments `className`, `style`, and `children` to be state-aware as well as provide the `render` prop:
|
|
188
151
|
|
|
189
152
|
```ts
|
|
190
|
-
type
|
|
191
|
-
props: ComponentPropsWithRef<T>,
|
|
192
|
-
state: S,
|
|
193
|
-
) => ReactNode
|
|
153
|
+
type ButtonProps = ComponentProps<'button', ButtonState>
|
|
194
154
|
```
|
|
195
155
|
|
|
196
156
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { useRender } from './use-render.ts';
|
|
2
|
-
export type {
|
|
2
|
+
export type { UseRenderOptions } from './use-render.ts';
|
|
3
|
+
export type { BaseComponentProps, ClassName, ComponentProps, ComponentRenderer, DataAttributes, Style, } from './types.ts';
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import type { ComponentPropsWithRef, CSSProperties, ElementType, ReactNode } from 'react';
|
|
2
|
-
export type
|
|
3
|
-
export type
|
|
4
|
-
export type
|
|
5
|
-
ref?:
|
|
1
|
+
import type { ComponentPropsWithRef, CSSProperties, ElementType, HTMLAttributes, JSX, ReactNode, Ref } from 'react';
|
|
2
|
+
export type ClassName<State> = ((state: State, baseClassName?: string) => string | undefined) | string;
|
|
3
|
+
export type Style<State> = ((state: State, baseStyle?: CSSProperties) => CSSProperties | undefined) | CSSProperties;
|
|
4
|
+
export type ComponentRenderer<S> = (props: HTMLAttributes<any> & {
|
|
5
|
+
ref?: Ref<any> | undefined;
|
|
6
6
|
}, state: S) => ReactNode;
|
|
7
7
|
export type DataAttributes = Record<`data-${string}`, string | number | boolean>;
|
|
8
8
|
export type BaseComponentProps<T extends ElementType> = Omit<ComponentPropsWithRef<T>, 'children' | 'className' | 'style'>;
|
|
9
|
+
export type ComponentProps<T extends ElementType, S> = BaseComponentProps<T> & {
|
|
10
|
+
children?: ReactNode | {
|
|
11
|
+
bivarianceHack(state: S): ReactNode;
|
|
12
|
+
}['bivarianceHack'] | undefined;
|
|
13
|
+
className?: ClassName<S> | undefined;
|
|
14
|
+
style?: Style<S> | undefined;
|
|
15
|
+
render?: ComponentRenderer<S> | JSX.Element;
|
|
16
|
+
};
|
package/dist/use-render.d.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import type { ElementType,
|
|
2
|
-
import type {
|
|
3
|
-
export type ComponentProps<T extends ElementType, S> = BaseComponentProps<T> & {
|
|
4
|
-
children?: ((state: S) => ReactNode) | ReactNode;
|
|
5
|
-
className?: ClassNameResolver<S>;
|
|
6
|
-
style?: StyleResolver<S>;
|
|
7
|
-
render?: Renderer<S> | JSX.Element;
|
|
8
|
-
};
|
|
1
|
+
import type { ElementType, ReactNode } from 'react';
|
|
2
|
+
import type { ComponentProps, DataAttributes } from './types.ts';
|
|
9
3
|
export interface UseRenderOptions<T extends ElementType, S> {
|
|
10
4
|
baseProps?: React.ComponentProps<T> & DataAttributes;
|
|
11
5
|
props?: ComponentProps<T, S> & DataAttributes;
|
|
@@ -31,4 +25,4 @@ export interface UseRenderOptions<T extends ElementType, S> {
|
|
|
31
25
|
* <Button render={<a href="#" />} />
|
|
32
26
|
* ```
|
|
33
27
|
*/
|
|
34
|
-
export declare function useRender<T extends ElementType, S>(tag: T, state: S, options
|
|
28
|
+
export declare function useRender<T extends ElementType, S>(tag: T, state: S, options?: UseRenderOptions<T, S>): ReactNode;
|
package/dist/use-render.js
CHANGED
|
@@ -21,14 +21,14 @@ import { isFunction, isString, mergeProps, resolveClassName, resolveStyle, } fro
|
|
|
21
21
|
* <Button render={<a href="#" />} />
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
export function useRender(tag, state, options) {
|
|
24
|
+
export function useRender(tag, state, options = {}) {
|
|
25
25
|
// Workarounds for getting the prop objects to be typed. But should still be ok as the properties we need is common to all elements
|
|
26
26
|
const baseProps = options.baseProps ?? {};
|
|
27
27
|
const props = (options.props ?? {});
|
|
28
28
|
const { className: baseClassName, style: baseStyle, children: baseChildren, ...base } = baseProps;
|
|
29
29
|
const { className, style, children, ref, render, ...rest } = props ?? {};
|
|
30
|
-
const resolvedClassName = resolveClassName(baseClassName, className
|
|
31
|
-
const resolvedStyle = resolveStyle(baseStyle, style
|
|
30
|
+
const resolvedClassName = resolveClassName(state, baseClassName, className);
|
|
31
|
+
const resolvedStyle = resolveStyle(state, baseStyle, style);
|
|
32
32
|
const refs = Array.isArray(options.ref)
|
|
33
33
|
? [ref, ...options.ref]
|
|
34
34
|
: [ref, options.ref];
|
package/dist/use-render.test.jsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { describe, expect, test, vi } from 'vitest';
|
|
3
3
|
import { render } from 'vitest-browser-react';
|
|
4
|
+
import {} from "./types.js";
|
|
4
5
|
import { useRender } from "./use-render.js";
|
|
5
6
|
function createBtn(defaultProps) {
|
|
6
7
|
return function Button(props) {
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CSSProperties } from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { ClassName, Style } from './types.ts';
|
|
3
3
|
export declare const isString: (value: unknown) => value is string;
|
|
4
4
|
export declare const isFunction: (value: unknown) => value is Function;
|
|
5
5
|
export declare const isUndefined: (value: unknown) => value is undefined;
|
|
@@ -15,14 +15,15 @@ export declare function mergeProps<T extends Record<string, unknown>>(defaultPro
|
|
|
15
15
|
* - Merges string values using clsx
|
|
16
16
|
* - Function props receive the resolved default value
|
|
17
17
|
*/
|
|
18
|
-
export declare function resolveClassName<State>(
|
|
18
|
+
export declare function resolveClassName<State>(state: State, defaultClassName?: ClassName<State>, propsClassName?: ClassName<State>): string | undefined;
|
|
19
19
|
/**
|
|
20
20
|
* Resolves and merges style values from defaultProps and props.
|
|
21
21
|
* - Resolves function values with the provided state
|
|
22
22
|
* - Merges object values using object spread
|
|
23
23
|
* - Function props receive the resolved default value
|
|
24
24
|
*/
|
|
25
|
-
export declare function resolveStyle<State>(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
export declare function resolveStyle<State>(state: State, defaultStyle?: Style<State>, propsStyle?: Style<State>): CSSProperties | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Returns a string with the truthy values of `args` separated by space.
|
|
28
|
+
*/
|
|
29
|
+
export declare function cx(...args: Array<string | null | false | 0 | undefined>): string | undefined;
|
package/dist/utils.js
CHANGED
|
@@ -27,7 +27,7 @@ export function mergeProps(defaultProps, props) {
|
|
|
27
27
|
* - Merges string values using clsx
|
|
28
28
|
* - Function props receive the resolved default value
|
|
29
29
|
*/
|
|
30
|
-
export function resolveClassName(defaultClassName, propsClassName
|
|
30
|
+
export function resolveClassName(state, defaultClassName, propsClassName) {
|
|
31
31
|
const resolvedDefault = isFunction(defaultClassName)
|
|
32
32
|
? defaultClassName(state)
|
|
33
33
|
: defaultClassName;
|
|
@@ -36,7 +36,7 @@ export function resolveClassName(defaultClassName, propsClassName, state) {
|
|
|
36
36
|
return propsClassName(state, resolvedDefault);
|
|
37
37
|
}
|
|
38
38
|
else {
|
|
39
|
-
return
|
|
39
|
+
return cx(resolvedDefault, propsClassName);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
else {
|
|
@@ -49,7 +49,7 @@ export function resolveClassName(defaultClassName, propsClassName, state) {
|
|
|
49
49
|
* - Merges object values using object spread
|
|
50
50
|
* - Function props receive the resolved default value
|
|
51
51
|
*/
|
|
52
|
-
export function resolveStyle(defaultStyle, propsStyle
|
|
52
|
+
export function resolveStyle(state, defaultStyle, propsStyle) {
|
|
53
53
|
const resolvedDefault = isFunction(defaultStyle)
|
|
54
54
|
? defaultStyle(state)
|
|
55
55
|
: defaultStyle;
|
|
@@ -67,15 +67,9 @@ export function resolveStyle(defaultStyle, propsStyle, state) {
|
|
|
67
67
|
return resolvedDefault;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (isString(tmp)) {
|
|
76
|
-
str += (str && ' ') + tmp;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return str;
|
|
70
|
+
/**
|
|
71
|
+
* Returns a string with the truthy values of `args` separated by space.
|
|
72
|
+
*/
|
|
73
|
+
export function cx(...args) {
|
|
74
|
+
return args.filter(Boolean).join(' ') || undefined;
|
|
81
75
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diskette/use-render",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.1",
|
|
5
5
|
"exports": "./dist/index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist"
|
|
@@ -52,6 +52,6 @@
|
|
|
52
52
|
"test": "vitest run --browser.headless",
|
|
53
53
|
"typecheck": "tsc",
|
|
54
54
|
"lint": "oxlint --type-aware src",
|
|
55
|
-
"release": "changeset publish && git push --follow-tags"
|
|
55
|
+
"release": "changeset version && changeset publish && git push --follow-tags"
|
|
56
56
|
}
|
|
57
57
|
}
|