react-solidlike 2.3.0 → 2.5.0
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 +195 -70
- package/README.zh.md +456 -0
- package/dist/ClientOnly.d.ts +43 -0
- package/dist/Once.d.ts +43 -0
- package/dist/Repeat.d.ts +3 -3
- package/dist/Show.d.ts +9 -1
- package/dist/Split.d.ts +3 -3
- package/dist/Timeout.d.ts +45 -0
- package/dist/Visible.d.ts +51 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +71 -4
- package/package.json +5 -2
- package/README.en.md +0 -331
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Children, Component, Fragment, cloneElement, createElement, isValidElement, useEffect, useState } from "react";
|
|
1
|
+
import { Children, Component, Fragment, cloneElement, createElement, isValidElement, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
|
|
4
4
|
function Await({ promise, loading = null, error = null, children }) {
|
|
@@ -112,8 +112,11 @@ function Repeat({ times, children, ...props }) {
|
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function Show({ when, children, fallback = null }) {
|
|
116
|
-
if (!when || isEmpty(when))
|
|
115
|
+
function Show({ when, children, fallback = null, onFallback }) {
|
|
116
|
+
if (!when || isEmpty(when)) {
|
|
117
|
+
onFallback?.();
|
|
118
|
+
return fallback;
|
|
119
|
+
}
|
|
117
120
|
if (typeof children === "function") return children(when);
|
|
118
121
|
return children;
|
|
119
122
|
}
|
|
@@ -181,4 +184,68 @@ function Switch({ children, fallback = null }) {
|
|
|
181
184
|
return defaultContent;
|
|
182
185
|
}
|
|
183
186
|
|
|
184
|
-
|
|
187
|
+
function Timeout({ ms, children, mode = "after", fallback = null, onTimeout }) {
|
|
188
|
+
const [ready, setReady] = useState(mode === "before");
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
const timer = setTimeout(() => {
|
|
191
|
+
setReady((prev) => !prev);
|
|
192
|
+
onTimeout?.();
|
|
193
|
+
}, ms);
|
|
194
|
+
return () => clearTimeout(timer);
|
|
195
|
+
}, [ms, onTimeout]);
|
|
196
|
+
if (mode === "after") return ready ? children : fallback;
|
|
197
|
+
return ready ? children : null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function Visible({ children, fallback = null, rootMargin = "0px", threshold = 0, once = true, onVisibilityChange }) {
|
|
201
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
202
|
+
const [hasBeenVisible, setHasBeenVisible] = useState(false);
|
|
203
|
+
const [isClient, setIsClient] = useState(false);
|
|
204
|
+
const ref = useRef(null);
|
|
205
|
+
const isSupported = typeof window !== "undefined" && typeof IntersectionObserver !== "undefined";
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
setIsClient(true);
|
|
208
|
+
}, []);
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (!isClient) return;
|
|
211
|
+
if (!isSupported) {
|
|
212
|
+
setIsVisible(true);
|
|
213
|
+
setHasBeenVisible(true);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const element = ref.current;
|
|
217
|
+
if (!element) return;
|
|
218
|
+
const observer = new IntersectionObserver((entries) => {
|
|
219
|
+
const entry = entries[0];
|
|
220
|
+
if (entry) {
|
|
221
|
+
const visible = entry.isIntersecting;
|
|
222
|
+
setIsVisible(visible);
|
|
223
|
+
onVisibilityChange?.(visible);
|
|
224
|
+
if (visible) {
|
|
225
|
+
setHasBeenVisible(true);
|
|
226
|
+
if (once) observer.disconnect();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}, {
|
|
230
|
+
rootMargin,
|
|
231
|
+
threshold
|
|
232
|
+
});
|
|
233
|
+
observer.observe(element);
|
|
234
|
+
return () => observer.disconnect();
|
|
235
|
+
}, [
|
|
236
|
+
isClient,
|
|
237
|
+
isSupported,
|
|
238
|
+
rootMargin,
|
|
239
|
+
threshold,
|
|
240
|
+
once,
|
|
241
|
+
onVisibilityChange
|
|
242
|
+
]);
|
|
243
|
+
if (!isClient) return children;
|
|
244
|
+
if (!isSupported) return children;
|
|
245
|
+
return /* @__PURE__ */ jsx("div", {
|
|
246
|
+
ref,
|
|
247
|
+
children: (once ? hasBeenVisible : isVisible) ? children : fallback
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export { Await, Default, Dynamic, ErrorBoundary, For, Match, QueryBoundary, Repeat, Show, Split, Switch, Timeout, Visible };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-solidlike",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Declarative React control flow components inspired by Solid.js, replacing ternary expressions and array.map() in JSX",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "rm -rf dist && rolldown -c && tsc -p tsconfig.build.json",
|
|
20
20
|
"test": "bun test",
|
|
21
|
+
"test:e2e": "playwright test && playwright show-report",
|
|
22
|
+
"pretest:e2e": "bun e2e/check-browsers.ts",
|
|
21
23
|
"lint": "biome check .",
|
|
22
24
|
"lint:fix": "biome check --write .",
|
|
23
25
|
"format": "biome format --write .",
|
|
24
|
-
"prepublishOnly": "bun test && bun run build"
|
|
26
|
+
"prepublishOnly": "bun format && bun test && bun run build"
|
|
25
27
|
},
|
|
26
28
|
"keywords": [
|
|
27
29
|
"react",
|
|
@@ -43,6 +45,7 @@
|
|
|
43
45
|
"devDependencies": {
|
|
44
46
|
"@biomejs/biome": "^2.3.11",
|
|
45
47
|
"@happy-dom/global-registrator": "^20.1.0",
|
|
48
|
+
"@playwright/test": "^1.58.0",
|
|
46
49
|
"@testing-library/react": "^16.3.1",
|
|
47
50
|
"@testing-library/user-event": "^14.6.1",
|
|
48
51
|
"@types/bun": "latest",
|
package/README.en.md
DELETED
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
# react-solidlike
|
|
2
|
-
|
|
3
|
-
English | [中文](./README.md)
|
|
4
|
-
|
|
5
|
-
Declarative React control flow components inspired by Solid.js. Replaces ternary expressions and `array.map()` in JSX, making your component code cleaner and more readable. Supports React and React Native.
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install react-solidlike
|
|
11
|
-
# or
|
|
12
|
-
bun add react-solidlike
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Components
|
|
16
|
-
|
|
17
|
-
### `<Show>` - Conditional Rendering
|
|
18
|
-
|
|
19
|
-
Replace ternary expressions for conditional rendering.
|
|
20
|
-
|
|
21
|
-
```tsx
|
|
22
|
-
import { Show } from "react-solidlike";
|
|
23
|
-
|
|
24
|
-
// Basic usage
|
|
25
|
-
<Show when={isLoggedIn}>
|
|
26
|
-
<UserProfile />
|
|
27
|
-
</Show>
|
|
28
|
-
|
|
29
|
-
// With fallback
|
|
30
|
-
<Show when={isLoggedIn} fallback={<LoginButton />}>
|
|
31
|
-
<UserProfile />
|
|
32
|
-
</Show>
|
|
33
|
-
|
|
34
|
-
// Using render props for type-safe value access
|
|
35
|
-
<Show when={user}>
|
|
36
|
-
{(user) => <UserProfile name={user.name} />}
|
|
37
|
-
</Show>
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### `<For>` - List Rendering
|
|
41
|
-
|
|
42
|
-
Replace `array.map()` for list rendering.
|
|
43
|
-
|
|
44
|
-
```tsx
|
|
45
|
-
import { For } from "react-solidlike";
|
|
46
|
-
|
|
47
|
-
// Basic usage
|
|
48
|
-
<For each={items}>
|
|
49
|
-
{(item) => <ListItem {...item} />}
|
|
50
|
-
</For>
|
|
51
|
-
|
|
52
|
-
// With keyExtractor
|
|
53
|
-
<For each={users} keyExtractor={(user) => user.id}>
|
|
54
|
-
{(user) => <UserCard user={user} />}
|
|
55
|
-
</For>
|
|
56
|
-
|
|
57
|
-
// With fallback for empty arrays
|
|
58
|
-
<For each={items} fallback={<EmptyState />}>
|
|
59
|
-
{(item, index) => <ListItem item={item} index={index} />}
|
|
60
|
-
</For>
|
|
61
|
-
|
|
62
|
-
// With wrapper element
|
|
63
|
-
<For each={items} wrapper={<ul className="list" />}>
|
|
64
|
-
{(item) => <li>{item.name}</li>}
|
|
65
|
-
</For>
|
|
66
|
-
|
|
67
|
-
// Reverse rendering
|
|
68
|
-
<For each={messages} reverse>
|
|
69
|
-
{(msg) => <Message {...msg} />}
|
|
70
|
-
</For>
|
|
71
|
-
|
|
72
|
-
// Using array parameter for context
|
|
73
|
-
<For each={steps}>
|
|
74
|
-
{(step, index, array) => (
|
|
75
|
-
<Step
|
|
76
|
-
data={step}
|
|
77
|
-
isFirst={index === 0}
|
|
78
|
-
isLast={index === array.length - 1}
|
|
79
|
-
/>
|
|
80
|
-
)}
|
|
81
|
-
</For>
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### `<Switch>` / `<Match>` / `<Default>` - Multi-branch Rendering
|
|
85
|
-
|
|
86
|
-
Replace multiple `if-else` or `switch` statements.
|
|
87
|
-
|
|
88
|
-
```tsx
|
|
89
|
-
import { Switch, Match, Default } from "react-solidlike";
|
|
90
|
-
|
|
91
|
-
<Switch>
|
|
92
|
-
<Match when={status === "loading"}>
|
|
93
|
-
<LoadingSpinner />
|
|
94
|
-
</Match>
|
|
95
|
-
<Match when={status === "error"}>
|
|
96
|
-
<ErrorMessage />
|
|
97
|
-
</Match>
|
|
98
|
-
<Match when={status === "success"}>
|
|
99
|
-
<SuccessContent />
|
|
100
|
-
</Match>
|
|
101
|
-
<Default>
|
|
102
|
-
<IdleState />
|
|
103
|
-
</Default>
|
|
104
|
-
</Switch>
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### `<Await>` - Async Rendering
|
|
108
|
-
|
|
109
|
-
Wait for Promise to resolve before rendering.
|
|
110
|
-
|
|
111
|
-
```tsx
|
|
112
|
-
import { Await } from "react-solidlike";
|
|
113
|
-
|
|
114
|
-
// Basic usage
|
|
115
|
-
<Await promise={fetchUser()} loading={<Spinner />}>
|
|
116
|
-
{(user) => <UserProfile user={user} />}
|
|
117
|
-
</Await>
|
|
118
|
-
|
|
119
|
-
// With error handling
|
|
120
|
-
<Await
|
|
121
|
-
promise={fetchData()}
|
|
122
|
-
loading={<Loading />}
|
|
123
|
-
error={(err) => <ErrorMessage message={err.message} />}
|
|
124
|
-
>
|
|
125
|
-
{(data) => <DataView data={data} />}
|
|
126
|
-
</Await>
|
|
127
|
-
|
|
128
|
-
// Supports non-Promise values (for caching scenarios)
|
|
129
|
-
<Await promise={cache ?? fetchData()} loading={<Spinner />}>
|
|
130
|
-
{(data) => <DataView data={data} />}
|
|
131
|
-
</Await>
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
### `<Repeat>` - Repeat Rendering
|
|
135
|
-
|
|
136
|
-
Replace `Array.from({ length: n }).map()`.
|
|
137
|
-
|
|
138
|
-
```tsx
|
|
139
|
-
import { Repeat } from "react-solidlike";
|
|
140
|
-
|
|
141
|
-
// Render star ratings
|
|
142
|
-
<Repeat times={5}>
|
|
143
|
-
{(i) => <Star key={i} filled={i < rating} />}
|
|
144
|
-
</Repeat>
|
|
145
|
-
|
|
146
|
-
// Generate skeleton placeholders
|
|
147
|
-
<Repeat times={3}>
|
|
148
|
-
{(i) => <SkeletonCard key={i} />}
|
|
149
|
-
</Repeat>
|
|
150
|
-
|
|
151
|
-
// With wrapper element
|
|
152
|
-
<Repeat times={5} wrapper={<div className="stars" />}>
|
|
153
|
-
{(i) => <Star key={i} />}
|
|
154
|
-
</Repeat>
|
|
155
|
-
|
|
156
|
-
// Reverse rendering
|
|
157
|
-
<Repeat times={5} reverse>
|
|
158
|
-
{(i) => <div key={i}>Reversed {i}</div>}
|
|
159
|
-
</Repeat>
|
|
160
|
-
|
|
161
|
-
// Using length parameter for progress
|
|
162
|
-
<Repeat times={totalSteps}>
|
|
163
|
-
{(i, length) => (
|
|
164
|
-
<Step key={i} current={i + 1} total={length} />
|
|
165
|
-
)}
|
|
166
|
-
</Repeat>
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### `<Split>` - String Split Rendering
|
|
170
|
-
|
|
171
|
-
Split a string by separator and render each part.
|
|
172
|
-
|
|
173
|
-
```tsx
|
|
174
|
-
import { Split } from "react-solidlike";
|
|
175
|
-
|
|
176
|
-
// Basic usage - splits without keeping separator
|
|
177
|
-
<Split string="a,b,c" separator=",">
|
|
178
|
-
{(part) => <span>{part}</span>}
|
|
179
|
-
</Split>
|
|
180
|
-
// Renders: ["a", "b", "c"]
|
|
181
|
-
|
|
182
|
-
// Keep separator in result
|
|
183
|
-
<Split string="9+5=(9+1)+4" separator="=" keepSeparator>
|
|
184
|
-
{(part) => <span>{part}</span>}
|
|
185
|
-
</Split>
|
|
186
|
-
// Renders: ["9+5", "=", "(9+1)+4"]
|
|
187
|
-
|
|
188
|
-
// Using RegExp separator
|
|
189
|
-
<Split string="a1b2c3" separator={/\d/} keepSeparator>
|
|
190
|
-
{(part) => <span>{part}</span>}
|
|
191
|
-
</Split>
|
|
192
|
-
// Renders: ["a", "1", "b", "2", "c", "3"]
|
|
193
|
-
|
|
194
|
-
// With wrapper element
|
|
195
|
-
<Split string="hello world" separator=" " wrapper={<div className="words" />}>
|
|
196
|
-
{(word) => <span>{word}</span>}
|
|
197
|
-
</Split>
|
|
198
|
-
|
|
199
|
-
// With fallback for empty string
|
|
200
|
-
<Split string={text} separator="," fallback={<EmptyState />}>
|
|
201
|
-
{(part) => <Tag>{part}</Tag>}
|
|
202
|
-
</Split>
|
|
203
|
-
|
|
204
|
-
// Reverse rendering
|
|
205
|
-
<Split string="a,b,c" separator="," reverse>
|
|
206
|
-
{(part) => <span>{part}</span>}
|
|
207
|
-
</Split>
|
|
208
|
-
// Render order: ["c", "b", "a"]
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### `<Dynamic>` - Dynamic Component
|
|
212
|
-
|
|
213
|
-
Dynamically select component type based on conditions.
|
|
214
|
-
|
|
215
|
-
```tsx
|
|
216
|
-
import { Dynamic } from "react-solidlike";
|
|
217
|
-
|
|
218
|
-
// Dynamic button or link
|
|
219
|
-
<Dynamic
|
|
220
|
-
component={href ? 'a' : 'button'}
|
|
221
|
-
href={href}
|
|
222
|
-
onClick={onClick}
|
|
223
|
-
>
|
|
224
|
-
{label}
|
|
225
|
-
</Dynamic>
|
|
226
|
-
|
|
227
|
-
// With custom components
|
|
228
|
-
<Dynamic
|
|
229
|
-
component={isAdmin ? AdminPanel : UserPanel}
|
|
230
|
-
user={currentUser}
|
|
231
|
-
/>
|
|
232
|
-
|
|
233
|
-
// React Native usage
|
|
234
|
-
<Dynamic
|
|
235
|
-
component={isPressable ? Pressable : View}
|
|
236
|
-
onPress={handlePress}
|
|
237
|
-
>
|
|
238
|
-
<Text>Content</Text>
|
|
239
|
-
</Dynamic>
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### `<ErrorBoundary>` - Error Boundary
|
|
243
|
-
|
|
244
|
-
Catch JavaScript errors in child component tree.
|
|
245
|
-
|
|
246
|
-
```tsx
|
|
247
|
-
import { ErrorBoundary } from "react-solidlike";
|
|
248
|
-
|
|
249
|
-
// Basic usage
|
|
250
|
-
<ErrorBoundary fallback={<ErrorPage />}>
|
|
251
|
-
<App />
|
|
252
|
-
</ErrorBoundary>
|
|
253
|
-
|
|
254
|
-
// With render props for error info and reset function
|
|
255
|
-
<ErrorBoundary
|
|
256
|
-
fallback={(error, reset) => (
|
|
257
|
-
<div>
|
|
258
|
-
<p>Error: {error.message}</p>
|
|
259
|
-
<button onClick={reset}>Retry</button>
|
|
260
|
-
</div>
|
|
261
|
-
)}
|
|
262
|
-
>
|
|
263
|
-
<App />
|
|
264
|
-
</ErrorBoundary>
|
|
265
|
-
|
|
266
|
-
// Auto-reset when resetKey changes
|
|
267
|
-
<ErrorBoundary fallback={<Error />} resetKey={userId}>
|
|
268
|
-
<UserProfile />
|
|
269
|
-
</ErrorBoundary>
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### `<QueryBoundary>` - Query Boundary
|
|
273
|
-
|
|
274
|
-
Handle async query states (loading, error, empty, success). Works with `@tanstack/react-query`, SWR, RTK Query, etc.
|
|
275
|
-
|
|
276
|
-
```tsx
|
|
277
|
-
import { QueryBoundary } from "react-solidlike";
|
|
278
|
-
import { useQuery } from "@tanstack/react-query";
|
|
279
|
-
|
|
280
|
-
function UserList() {
|
|
281
|
-
const query = useQuery({ queryKey: ["users"], queryFn: fetchUsers });
|
|
282
|
-
|
|
283
|
-
return (
|
|
284
|
-
<QueryBoundary
|
|
285
|
-
query={query}
|
|
286
|
-
loading={<Spinner />}
|
|
287
|
-
error={<ErrorMessage />}
|
|
288
|
-
empty={<NoData />}
|
|
289
|
-
>
|
|
290
|
-
{(users) => (
|
|
291
|
-
<ul>
|
|
292
|
-
{users.map((user) => (
|
|
293
|
-
<li key={user.id}>{user.name}</li>
|
|
294
|
-
))}
|
|
295
|
-
</ul>
|
|
296
|
-
)}
|
|
297
|
-
</QueryBoundary>
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
#### Props
|
|
303
|
-
|
|
304
|
-
| Prop | Type | Description |
|
|
305
|
-
|------|------|-------------|
|
|
306
|
-
| `query` | `QueryResult<T>` | Query result object |
|
|
307
|
-
| `loading` | `ReactNode` | Loading state content |
|
|
308
|
-
| `error` | `ReactNode` | Error state content |
|
|
309
|
-
| `empty` | `ReactNode` | Empty state content |
|
|
310
|
-
| `children` | `ReactNode \| (data: T) => ReactNode` | Success content |
|
|
311
|
-
| `isEmptyFn` | `(data: T) => boolean` | Custom empty check |
|
|
312
|
-
|
|
313
|
-
## Development
|
|
314
|
-
|
|
315
|
-
```bash
|
|
316
|
-
# Install dependencies
|
|
317
|
-
bun install
|
|
318
|
-
|
|
319
|
-
# Run tests
|
|
320
|
-
bun test
|
|
321
|
-
|
|
322
|
-
# Lint
|
|
323
|
-
bun run lint
|
|
324
|
-
|
|
325
|
-
# Build
|
|
326
|
-
bun run build
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
## License
|
|
330
|
-
|
|
331
|
-
MIT
|