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/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)) return fallback;
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
- export { Await, Default, Dynamic, ErrorBoundary, For, Match, QueryBoundary, Repeat, Show, Split, Switch };
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.0",
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