@bind-ts/react-bind 0.0.2 → 0.0.3
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 +222 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# @bind-ts/react-bind
|
|
2
|
+
|
|
3
|
+
**Headless UI for building type-safe compound components.** Stop writing repetitive context boilerplate for tabs, accordions, wizards, and other UI patterns that need "one active item at a time" logic.
|
|
4
|
+
|
|
5
|
+
Built on top of [@tanstack/store](https://tanstack.com/store), heavily inspired by [@tanstack/form](https://tanstack.com/form).
|
|
6
|
+
|
|
7
|
+
## Just want to see a full example?
|
|
8
|
+
|
|
9
|
+
Check out [`page.tsx`](apps/web/app/page.tsx) and [`bind-context.ts`](apps/web/app/bind-context.ts) for example usage.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @bind-ts/react-bind
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## The Problem
|
|
18
|
+
|
|
19
|
+
Building compound components like tabs requires creating context providers, hooks, and managing state manually:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
const TabsContext = React.createContext<
|
|
23
|
+
| {
|
|
24
|
+
tab: string;
|
|
25
|
+
changeTab: (tab: string) => void;
|
|
26
|
+
}
|
|
27
|
+
| undefined
|
|
28
|
+
>(undefined);
|
|
29
|
+
|
|
30
|
+
function useTabsContext() {
|
|
31
|
+
const context = React.useContext(TabsContext);
|
|
32
|
+
if (!context) {
|
|
33
|
+
throw new Error("useTabsContext must be used within a TabsContextProvider");
|
|
34
|
+
}
|
|
35
|
+
return context;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function Tabs({ children, defaultTab }: { children: React.ReactNode; defaultTab: string }) {
|
|
39
|
+
const [tab, setTab] = React.useState(defaultTab);
|
|
40
|
+
return (
|
|
41
|
+
<TabsContext.Provider value={{ tab, changeTab: setTab }}>{children}</TabsContext.Provider>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Tab({ children, tab }: { children: React.ReactNode; tab: string }) {
|
|
46
|
+
const { tab: currentTab, changeTab } = useTabsContext();
|
|
47
|
+
if (tab !== currentTab) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return <div>{children}</div>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function TabPanel({ children, tab }: { children: React.ReactNode; tab: string }) {
|
|
54
|
+
const { tab: currentTab } = useTabsContext();
|
|
55
|
+
if (tab !== currentTab) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return <div>{children}</div>;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This is a lot of boilerplate for a common pattern and you'll need to repeat it for accordions, wizards, radio groups, and more.
|
|
63
|
+
|
|
64
|
+
## The Solution: `useBind`
|
|
65
|
+
|
|
66
|
+
`useBind` provides all the state management and element binding you need in a single hook:
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { useBind } from "@bind-ts/react-bind";
|
|
70
|
+
|
|
71
|
+
function Example() {
|
|
72
|
+
const bind = useBind({
|
|
73
|
+
defaultValue: "tab1",
|
|
74
|
+
values: ["tab1", "tab2"],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<>
|
|
79
|
+
<div className="flex gap-2">
|
|
80
|
+
<bind.Element value="tab1">
|
|
81
|
+
{(bindApi) => {
|
|
82
|
+
return <button onClick={bindApi.handleChange}>Tab 1</button>;
|
|
83
|
+
}}
|
|
84
|
+
</bind.Element>
|
|
85
|
+
<bind.Element value="tab2">
|
|
86
|
+
{(bindApi) => {
|
|
87
|
+
return <button onClick={bindApi.handleChange}>Tab 2</button>;
|
|
88
|
+
}}
|
|
89
|
+
</bind.Element>
|
|
90
|
+
</div>
|
|
91
|
+
<bind.Element value="tab1">
|
|
92
|
+
{(bindApi) => {
|
|
93
|
+
if (!bindApi.meta.isActive) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return <div>Viewing {bindApi.state.value}</div>;
|
|
97
|
+
}}
|
|
98
|
+
</bind.Element>
|
|
99
|
+
<bind.Element value="tab2">
|
|
100
|
+
{(bindApi) => {
|
|
101
|
+
if (!bindApi.meta.isActive) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return <div>Viewing {bindApi.state.value}</div>;
|
|
105
|
+
}}
|
|
106
|
+
</bind.Element>
|
|
107
|
+
</>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Each `bind.Element` receives a render prop with full access to:
|
|
113
|
+
|
|
114
|
+
- `bindApi.state.value` — the element's value
|
|
115
|
+
- `bindApi.meta.isActive` — whether this element is currently active
|
|
116
|
+
- `bindApi.handleChange` — a handler to activate this element
|
|
117
|
+
|
|
118
|
+
## Subscribing to State
|
|
119
|
+
|
|
120
|
+
Need to display or react to the current active value outside of an `Element`? Use `bind.Subscribe`:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
function Example() {
|
|
124
|
+
const bind = useBind({
|
|
125
|
+
defaultValue: "tab1",
|
|
126
|
+
values: ["tab1", "tab2"],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<>
|
|
131
|
+
{/* Subscribe with a selector for fine-grained reactivity */}
|
|
132
|
+
<bind.Subscribe selector={(state) => state.value}>
|
|
133
|
+
{(activeValue) => <span>Active: {activeValue}</span>}
|
|
134
|
+
</bind.Subscribe>
|
|
135
|
+
|
|
136
|
+
{/* Or subscribe to the full state */}
|
|
137
|
+
<bind.Subscribe>
|
|
138
|
+
{(state) => (
|
|
139
|
+
<span>
|
|
140
|
+
{state.value} of {state.values.length} tabs
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
143
|
+
</bind.Subscribe>
|
|
144
|
+
</>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The `selector` prop lets you pick exactly what state you need, preventing unnecessary re-renders when unrelated state changes.
|
|
150
|
+
|
|
151
|
+
## Component Composition API
|
|
152
|
+
|
|
153
|
+
Redefining these render props for every use can become cumbersome. The **Composition API** lets you define reusable components that automatically receive bind context:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
import { createBindContexts, createBindHook } from "@bind-ts/react-bind";
|
|
157
|
+
|
|
158
|
+
// Create shared contexts
|
|
159
|
+
export const { bindContext, elementContext, useElementContext } = createBindContexts();
|
|
160
|
+
|
|
161
|
+
// Define reusable components
|
|
162
|
+
function Tab({ children }: { children: React.ReactNode }) {
|
|
163
|
+
const element = useElementContext();
|
|
164
|
+
return <button onClick={element.handleChange}>{children}</button>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function TabPanel({ children }: { children: React.ReactNode }) {
|
|
168
|
+
const element = useElementContext();
|
|
169
|
+
|
|
170
|
+
if (!element.meta.isActive) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return <div>{children}</div>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Create a typed hook with your components
|
|
178
|
+
const { useAppBind } = createBindHook({
|
|
179
|
+
elementContext,
|
|
180
|
+
bindContext,
|
|
181
|
+
components: {
|
|
182
|
+
Tab: {
|
|
183
|
+
Tab,
|
|
184
|
+
TabPanel,
|
|
185
|
+
},
|
|
186
|
+
// define other groups of components here such as Wizard, Accordion, etc.
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Usage becomes clean and declarative
|
|
191
|
+
function CompositionExample() {
|
|
192
|
+
const bind = useAppBind("Tab", {
|
|
193
|
+
defaultValue: "tab1",
|
|
194
|
+
values: ["tab1", "tab2"],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<>
|
|
199
|
+
<div className="flex gap-2">
|
|
200
|
+
<bind.Element value="tab1">
|
|
201
|
+
{(bindApi) => <bindApi.Tab>Tab 1</bindApi.Tab>}
|
|
202
|
+
</bind.Element>
|
|
203
|
+
<bind.Element value="tab2">
|
|
204
|
+
{(bindApi) => <bindApi.Tab>Tab 2</bindApi.Tab>}
|
|
205
|
+
</bind.Element>
|
|
206
|
+
</div>
|
|
207
|
+
<bind.Element value="tab1">
|
|
208
|
+
{(bindApi) => <bindApi.TabPanel>Tab 1 Content</bindApi.TabPanel>}
|
|
209
|
+
</bind.Element>
|
|
210
|
+
<bind.Element value="tab2">
|
|
211
|
+
{(bindApi) => <bindApi.TabPanel>Tab 2 Content</bindApi.TabPanel>}
|
|
212
|
+
</bind.Element>
|
|
213
|
+
</>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
With the Composition API, you define your component library once and get type-safe access to the right components for each use case.
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT
|