@riktajs/react 0.10.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 ADDED
@@ -0,0 +1,368 @@
1
+ # @riktajs/react
2
+
3
+ React utilities for Rikta SSR framework. Provides hooks and components for routing, navigation, SSR data access, and server interactions.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @riktajs/react
9
+ # or
10
+ pnpm add @riktajs/react
11
+ # or
12
+ yarn add @riktajs/react
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ Wrap your app with `RiktaProvider` to enable all features:
18
+
19
+ ```tsx
20
+ // entry-client.tsx
21
+ import { hydrateRoot } from 'react-dom/client';
22
+ import { RiktaProvider } from '@riktajs/react';
23
+ import App from './App';
24
+
25
+ hydrateRoot(
26
+ document.getElementById('root')!,
27
+ <RiktaProvider>
28
+ <App />
29
+ </RiktaProvider>
30
+ );
31
+ ```
32
+
33
+ ## Components
34
+
35
+ ### `<Link>`
36
+
37
+ Client-side navigation link component that uses the History API instead of full page reloads.
38
+
39
+ ```tsx
40
+ import { Link } from '@riktajs/react';
41
+
42
+ function Navigation() {
43
+ return (
44
+ <nav>
45
+ <Link href="/">Home</Link>
46
+ <Link href="/about">About</Link>
47
+ <Link href="/items/123">Item 123</Link>
48
+
49
+ {/* With options */}
50
+ <Link href="/dashboard" replace scroll={false}>
51
+ Dashboard
52
+ </Link>
53
+ </nav>
54
+ );
55
+ }
56
+ ```
57
+
58
+ **Props:**
59
+ - `href` (required): Target URL
60
+ - `replace`: Replace history entry instead of push
61
+ - `scroll`: Scroll to top after navigation (default: `true`)
62
+ - `state`: Additional state to pass to navigation
63
+ - All standard `<a>` props are supported
64
+
65
+ ### `<RiktaProvider>`
66
+
67
+ Root provider component that enables all Rikta React utilities.
68
+
69
+ ```tsx
70
+ import { RiktaProvider } from '@riktajs/react';
71
+
72
+ function App() {
73
+ return (
74
+ <RiktaProvider>
75
+ <YourApp />
76
+ </RiktaProvider>
77
+ );
78
+ }
79
+ ```
80
+
81
+ ## Hooks
82
+
83
+ ### `useNavigation()`
84
+
85
+ Programmatic navigation hook.
86
+
87
+ ```tsx
88
+ import { useNavigation } from '@riktajs/react';
89
+
90
+ function MyComponent() {
91
+ const { navigate, pathname } = useNavigation();
92
+
93
+ const handleSubmit = async () => {
94
+ await saveData();
95
+ navigate('/success');
96
+ };
97
+
98
+ // With options
99
+ navigate('/login', { replace: true });
100
+ navigate('/next', { scroll: false });
101
+ navigate('/edit', { state: { from: 'list' } });
102
+
103
+ return <button onClick={handleSubmit}>Submit</button>;
104
+ }
105
+ ```
106
+
107
+ ### `useParams()`
108
+
109
+ Access route parameters extracted from dynamic URLs.
110
+
111
+ ```tsx
112
+ import { useParams } from '@riktajs/react';
113
+
114
+ // For route /item/:id
115
+ function ItemPage() {
116
+ const { id } = useParams<{ id: string }>();
117
+
118
+ return <h1>Item {id}</h1>;
119
+ }
120
+
121
+ // Multiple params - /users/:userId/posts/:postId
122
+ function PostPage() {
123
+ const { userId, postId } = useParams<{ userId: string; postId: string }>();
124
+
125
+ return <h1>Post {postId} by User {userId}</h1>;
126
+ }
127
+ ```
128
+
129
+ ### `useSearchParams()`
130
+
131
+ Access and update URL search parameters.
132
+
133
+ ```tsx
134
+ import { useSearchParams } from '@riktajs/react';
135
+
136
+ function SearchPage() {
137
+ const [searchParams, setSearchParams] = useSearchParams();
138
+ const query = searchParams.get('q') ?? '';
139
+ const page = parseInt(searchParams.get('page') ?? '1', 10);
140
+
141
+ const handleSearch = (newQuery: string) => {
142
+ setSearchParams({ q: newQuery, page: '1' });
143
+ };
144
+
145
+ return (
146
+ <input
147
+ value={query}
148
+ onChange={(e) => handleSearch(e.target.value)}
149
+ />
150
+ );
151
+ }
152
+ ```
153
+
154
+ ### `useLocation()`
155
+
156
+ Get current location information.
157
+
158
+ ```tsx
159
+ import { useLocation } from '@riktajs/react';
160
+
161
+ function Breadcrumbs() {
162
+ const { pathname, search, searchParams } = useLocation();
163
+
164
+ return (
165
+ <nav>
166
+ Current path: {pathname}
167
+ {search && <span>?{search}</span>}
168
+ </nav>
169
+ );
170
+ }
171
+ ```
172
+
173
+ ### `useSsrData()`
174
+
175
+ Access SSR data passed from server via `window.__SSR_DATA__`.
176
+
177
+ ```tsx
178
+ import { useSsrData } from '@riktajs/react';
179
+
180
+ interface PageData {
181
+ title: string;
182
+ items: Array<{ id: string; name: string }>;
183
+ }
184
+
185
+ function ItemList() {
186
+ const ssrData = useSsrData<PageData>();
187
+
188
+ if (!ssrData) return <div>Loading...</div>;
189
+
190
+ return (
191
+ <div>
192
+ <h1>{ssrData.data.title}</h1>
193
+ <ul>
194
+ {ssrData.data.items.map(item => (
195
+ <li key={item.id}>{item.name}</li>
196
+ ))}
197
+ </ul>
198
+ </div>
199
+ );
200
+ }
201
+ ```
202
+
203
+ ### `useHydration()`
204
+
205
+ Track hydration state for client-only rendering.
206
+
207
+ ```tsx
208
+ import { useHydration } from '@riktajs/react';
209
+
210
+ function TimeDisplay() {
211
+ const { isHydrated, isServer } = useHydration();
212
+
213
+ // Avoid hydration mismatch with dynamic content
214
+ if (!isHydrated) {
215
+ return <span>Loading time...</span>;
216
+ }
217
+
218
+ return <span>{new Date().toLocaleTimeString()}</span>;
219
+ }
220
+
221
+ function ClientOnlyComponent() {
222
+ const { isHydrated } = useHydration();
223
+
224
+ if (!isHydrated) return null;
225
+
226
+ return <SomeClientOnlyLibrary />;
227
+ }
228
+ ```
229
+
230
+ ### `useFetch()`
231
+
232
+ Data fetching hook with loading and error states.
233
+
234
+ ```tsx
235
+ import { useFetch } from '@riktajs/react';
236
+
237
+ interface User {
238
+ id: string;
239
+ name: string;
240
+ }
241
+
242
+ function UserProfile({ userId }: { userId: string }) {
243
+ const { data, loading, error, refetch } = useFetch<User>(
244
+ `/api/users/${userId}`
245
+ );
246
+
247
+ if (loading) return <Spinner />;
248
+ if (error) return <Error message={error} />;
249
+ if (!data) return null;
250
+
251
+ return (
252
+ <div>
253
+ <h1>{data.name}</h1>
254
+ <button onClick={refetch}>Refresh</button>
255
+ </div>
256
+ );
257
+ }
258
+
259
+ // With options
260
+ const { data } = useFetch<Item[]>('/api/items', {
261
+ headers: { 'Authorization': `Bearer ${token}` },
262
+ deps: [token], // Refetch when token changes
263
+ skip: !token, // Don't fetch until we have a token
264
+ transform: (res) => res.results, // Transform response
265
+ });
266
+ ```
267
+
268
+ ### `useAction()`
269
+
270
+ Execute server actions (form submissions, mutations).
271
+
272
+ ```tsx
273
+ import { useAction } from '@riktajs/react';
274
+
275
+ interface CreateItemInput {
276
+ name: string;
277
+ price: number;
278
+ }
279
+
280
+ interface Item {
281
+ id: string;
282
+ name: string;
283
+ price: number;
284
+ }
285
+
286
+ function CreateItemForm() {
287
+ const { execute, pending, result } = useAction<CreateItemInput, Item>(
288
+ '/api/items',
289
+ {
290
+ onSuccess: (item) => console.log('Created:', item),
291
+ onError: (error) => console.error('Failed:', error),
292
+ }
293
+ );
294
+
295
+ const handleSubmit = async (e: FormEvent) => {
296
+ e.preventDefault();
297
+ const formData = new FormData(e.target as HTMLFormElement);
298
+ await execute({
299
+ name: formData.get('name') as string,
300
+ price: Number(formData.get('price')),
301
+ });
302
+ };
303
+
304
+ return (
305
+ <form onSubmit={handleSubmit}>
306
+ <input name="name" required />
307
+ <input name="price" type="number" required />
308
+ <button disabled={pending}>
309
+ {pending ? 'Creating...' : 'Create Item'}
310
+ </button>
311
+ {result?.error && <p className="error">{result.error}</p>}
312
+ </form>
313
+ );
314
+ }
315
+
316
+ // DELETE action
317
+ const { execute, pending } = useAction<{ id: string }, void>(
318
+ '/api/items',
319
+ { method: 'DELETE' }
320
+ );
321
+ ```
322
+
323
+ ## TypeScript
324
+
325
+ All exports are fully typed. Import types as needed:
326
+
327
+ ```tsx
328
+ import type {
329
+ SsrData,
330
+ RouterContextValue,
331
+ NavigateOptions,
332
+ ActionResult,
333
+ FetchState,
334
+ ActionState,
335
+ LinkProps,
336
+ HydrationState,
337
+ Location,
338
+ } from '@riktajs/react';
339
+ ```
340
+
341
+ ## Integration with @riktajs/ssr
342
+
343
+ This package is designed to work seamlessly with `@riktajs/ssr`. The SSR plugin automatically injects `window.__SSR_DATA__` which `RiktaProvider` picks up.
344
+
345
+ ```tsx
346
+ // Server: page.controller.ts
347
+ @Controller()
348
+ export class PageController {
349
+ @Get('/item/:id')
350
+ @Render()
351
+ getItem(@Param('id') id: string) {
352
+ const item = getItemById(id);
353
+ return { item, params: { id } };
354
+ }
355
+ }
356
+
357
+ // Client: ItemPage.tsx
358
+ function ItemPage() {
359
+ const ssrData = useSsrData<{ item: Item; params: { id: string } }>();
360
+ const { id } = useParams();
361
+
362
+ return <h1>{ssrData?.data.item.name} (ID: {id})</h1>;
363
+ }
364
+ ```
365
+
366
+ ## License
367
+
368
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,316 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/context/RouterContext.ts
7
+ var defaultRouterContext = {
8
+ pathname: "/",
9
+ search: "",
10
+ href: "/",
11
+ navigate: () => {
12
+ console.warn("[RiktaReact] Router context not initialized. Wrap your app with <RiktaProvider>");
13
+ },
14
+ params: {},
15
+ setParams: () => {
16
+ }
17
+ };
18
+ var RouterContext = react.createContext(defaultRouterContext);
19
+ RouterContext.displayName = "RiktaRouterContext";
20
+ var SsrContext = react.createContext(null);
21
+ SsrContext.displayName = "RiktaSsrContext";
22
+ function getLocationInfo() {
23
+ if (typeof window === "undefined") {
24
+ return { pathname: "/", search: "", href: "/" };
25
+ }
26
+ return {
27
+ pathname: window.location.pathname,
28
+ search: window.location.search.slice(1),
29
+ // Remove leading ?
30
+ href: window.location.href
31
+ };
32
+ }
33
+ function getSsrData() {
34
+ if (typeof window === "undefined") return void 0;
35
+ return window.__SSR_DATA__;
36
+ }
37
+ var RiktaProvider = ({
38
+ ssrData: initialSsrData,
39
+ initialParams = {},
40
+ children
41
+ }) => {
42
+ const [ssrData] = react.useState(() => {
43
+ return initialSsrData ?? getSsrData() ?? null;
44
+ });
45
+ const [location, setLocation] = react.useState(getLocationInfo);
46
+ const [params, setParams] = react.useState(initialParams);
47
+ const navigate = react.useCallback((url, options = {}) => {
48
+ const { replace = false, scroll = true, state } = options;
49
+ if (typeof window === "undefined") return;
50
+ let targetUrl;
51
+ try {
52
+ targetUrl = new URL(url, window.location.origin);
53
+ } catch {
54
+ console.error(`[RiktaReact] Invalid URL: ${url}`);
55
+ return;
56
+ }
57
+ if (targetUrl.origin !== window.location.origin) {
58
+ window.location.href = url;
59
+ return;
60
+ }
61
+ if (replace) {
62
+ window.history.replaceState(state ?? null, "", targetUrl.href);
63
+ } else {
64
+ window.history.pushState(state ?? null, "", targetUrl.href);
65
+ }
66
+ setLocation({
67
+ pathname: targetUrl.pathname,
68
+ search: targetUrl.search.slice(1),
69
+ href: targetUrl.href
70
+ });
71
+ if (scroll) {
72
+ window.scrollTo(0, 0);
73
+ }
74
+ window.dispatchEvent(new PopStateEvent("popstate", { state }));
75
+ }, []);
76
+ react.useEffect(() => {
77
+ if (typeof window === "undefined") return;
78
+ const handlePopState = () => {
79
+ setLocation(getLocationInfo());
80
+ };
81
+ window.addEventListener("popstate", handlePopState);
82
+ return () => window.removeEventListener("popstate", handlePopState);
83
+ }, []);
84
+ const routerValue = react.useMemo(() => ({
85
+ pathname: location.pathname,
86
+ search: location.search,
87
+ href: location.href,
88
+ navigate,
89
+ params,
90
+ setParams
91
+ }), [location.pathname, location.search, location.href, navigate, params]);
92
+ return /* @__PURE__ */ jsxRuntime.jsx(SsrContext.Provider, { value: ssrData, children: /* @__PURE__ */ jsxRuntime.jsx(RouterContext.Provider, { value: routerValue, children }) });
93
+ };
94
+ RiktaProvider.displayName = "RiktaProvider";
95
+ function useNavigation() {
96
+ const context = react.useContext(RouterContext);
97
+ const navigate = react.useCallback(
98
+ (url, options) => {
99
+ context.navigate(url, options);
100
+ },
101
+ [context.navigate]
102
+ );
103
+ return {
104
+ /** Navigate to a new URL */
105
+ navigate,
106
+ /** Current pathname */
107
+ pathname: context.pathname,
108
+ /** Current search string (without ?) */
109
+ search: context.search,
110
+ /** Full href */
111
+ href: context.href
112
+ };
113
+ }
114
+ var Link = ({
115
+ href,
116
+ replace = false,
117
+ scroll = true,
118
+ prefetch = false,
119
+ state,
120
+ children,
121
+ onClick,
122
+ ...restProps
123
+ }) => {
124
+ const { navigate } = useNavigation();
125
+ const handleClick = react.useCallback(
126
+ (e) => {
127
+ onClick?.(e);
128
+ if (e.defaultPrevented) return;
129
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
130
+ if (e.button !== 0) return;
131
+ const target = e.currentTarget.target;
132
+ if (target && target !== "_self") return;
133
+ try {
134
+ const url = new URL(href, window.location.origin);
135
+ if (url.origin !== window.location.origin) return;
136
+ } catch {
137
+ return;
138
+ }
139
+ e.preventDefault();
140
+ navigate(href, { replace, scroll, state });
141
+ },
142
+ [href, replace, scroll, state, navigate, onClick]
143
+ );
144
+ return /* @__PURE__ */ jsxRuntime.jsx("a", { href, onClick: handleClick, ...restProps, children });
145
+ };
146
+ Link.displayName = "Link";
147
+ function useParams() {
148
+ const context = react.useContext(RouterContext);
149
+ return context.params;
150
+ }
151
+ function useSearchParams() {
152
+ const context = react.useContext(RouterContext);
153
+ const searchParams = react.useMemo(() => {
154
+ return new URLSearchParams(context.search);
155
+ }, [context.search]);
156
+ const setSearchParams = react.useMemo(() => {
157
+ return (params) => {
158
+ const newParams = params instanceof URLSearchParams ? params : new URLSearchParams(params);
159
+ const search = newParams.toString();
160
+ const newUrl = search ? `${context.pathname}?${search}` : context.pathname;
161
+ context.navigate(newUrl, { scroll: false });
162
+ };
163
+ }, [context.pathname, context.navigate]);
164
+ return [searchParams, setSearchParams];
165
+ }
166
+ function useLocation() {
167
+ const context = react.useContext(RouterContext);
168
+ return {
169
+ pathname: context.pathname,
170
+ search: context.search,
171
+ href: context.href,
172
+ searchParams: new URLSearchParams(context.search)
173
+ };
174
+ }
175
+ function useSsrData() {
176
+ const context = react.useContext(SsrContext);
177
+ return context;
178
+ }
179
+ function useHydration() {
180
+ const isServer = typeof window === "undefined";
181
+ const [isHydrated, setIsHydrated] = react.useState(false);
182
+ react.useEffect(() => {
183
+ setIsHydrated(true);
184
+ }, []);
185
+ return {
186
+ isHydrated,
187
+ isServer
188
+ };
189
+ }
190
+ function useFetch(url, options = {}) {
191
+ const { skip = false, deps = [], transform, ...fetchOptions } = options;
192
+ const [state, setState] = react.useState({
193
+ data: null,
194
+ loading: !skip,
195
+ error: null
196
+ });
197
+ const mountedRef = react.useRef(true);
198
+ const fetchIdRef = react.useRef(0);
199
+ const fetchData = react.useCallback(async () => {
200
+ if (skip) return;
201
+ const fetchId = ++fetchIdRef.current;
202
+ setState((prev) => ({ ...prev, loading: true, error: null }));
203
+ try {
204
+ const response = await fetch(url, fetchOptions);
205
+ if (!response.ok) {
206
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
207
+ }
208
+ let data = await response.json();
209
+ if (transform) {
210
+ data = transform(data);
211
+ }
212
+ if (fetchId === fetchIdRef.current && mountedRef.current) {
213
+ setState({ data, loading: false, error: null });
214
+ }
215
+ } catch (err) {
216
+ if (fetchId === fetchIdRef.current && mountedRef.current) {
217
+ const message = err instanceof Error ? err.message : "An error occurred";
218
+ setState({ data: null, loading: false, error: message });
219
+ }
220
+ }
221
+ }, [url, skip, JSON.stringify(fetchOptions), transform]);
222
+ react.useEffect(() => {
223
+ mountedRef.current = true;
224
+ fetchData();
225
+ return () => {
226
+ mountedRef.current = false;
227
+ };
228
+ }, [fetchData, ...deps]);
229
+ const refetch = react.useCallback(async () => {
230
+ await fetchData();
231
+ }, [fetchData]);
232
+ return {
233
+ ...state,
234
+ refetch
235
+ };
236
+ }
237
+ function useAction(url, options = {}) {
238
+ const {
239
+ onSuccess,
240
+ onError,
241
+ method = "POST",
242
+ headers: customHeaders = {}
243
+ } = options;
244
+ const [pending, setPending] = react.useState(false);
245
+ const [result, setResult] = react.useState(null);
246
+ const execute = react.useCallback(
247
+ async (input) => {
248
+ setPending(true);
249
+ setResult(null);
250
+ try {
251
+ const response = await fetch(url, {
252
+ method,
253
+ headers: {
254
+ "Content-Type": "application/json",
255
+ ...customHeaders
256
+ },
257
+ body: JSON.stringify(input)
258
+ });
259
+ const data = await response.json();
260
+ if (!response.ok) {
261
+ const actionResult2 = {
262
+ success: false,
263
+ error: data.message || data.error || `HTTP ${response.status}`,
264
+ fieldErrors: data.fieldErrors
265
+ };
266
+ setResult(actionResult2);
267
+ onError?.(actionResult2.error);
268
+ return actionResult2;
269
+ }
270
+ const actionResult = {
271
+ success: true,
272
+ data
273
+ };
274
+ setResult(actionResult);
275
+ onSuccess?.(data);
276
+ return actionResult;
277
+ } catch (err) {
278
+ const message = err instanceof Error ? err.message : "An error occurred";
279
+ const actionResult = {
280
+ success: false,
281
+ error: message
282
+ };
283
+ setResult(actionResult);
284
+ onError?.(message);
285
+ return actionResult;
286
+ } finally {
287
+ setPending(false);
288
+ }
289
+ },
290
+ [url, method, JSON.stringify(customHeaders), onSuccess, onError]
291
+ );
292
+ const reset = react.useCallback(() => {
293
+ setResult(null);
294
+ }, []);
295
+ return {
296
+ execute,
297
+ pending,
298
+ result,
299
+ reset
300
+ };
301
+ }
302
+
303
+ exports.Link = Link;
304
+ exports.RiktaProvider = RiktaProvider;
305
+ exports.RouterContext = RouterContext;
306
+ exports.SsrContext = SsrContext;
307
+ exports.useAction = useAction;
308
+ exports.useFetch = useFetch;
309
+ exports.useHydration = useHydration;
310
+ exports.useLocation = useLocation;
311
+ exports.useNavigation = useNavigation;
312
+ exports.useParams = useParams;
313
+ exports.useSearchParams = useSearchParams;
314
+ exports.useSsrData = useSsrData;
315
+ //# sourceMappingURL=index.cjs.map
316
+ //# sourceMappingURL=index.cjs.map