@rplx/react 0.2.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 ADDED
@@ -0,0 +1,74 @@
1
+ # @rplx/react
2
+
3
+ React bindings for Ripple state management library.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @rplx/core @rplx/react
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```tsx
14
+ import { createStore } from '@rplx/core'
15
+ import { StoreProvider, useStoreState, useDispatch } from '@rplx/react'
16
+
17
+ // Create your store
18
+ const store = createStore({
19
+ initialState: { count: 0 }
20
+ })
21
+
22
+ // Wrap your app with StoreProvider
23
+ function App() {
24
+ return (
25
+ <StoreProvider store={store}>
26
+ <Counter />
27
+ </StoreProvider>
28
+ )
29
+ }
30
+
31
+ // Use hooks in components
32
+ function Counter() {
33
+ const state = useStoreState()
34
+ const dispatch = useDispatch()
35
+
36
+ return (
37
+ <div>
38
+ <p>Count: {state.count}</p>
39
+ <button onClick={() => dispatch('increment')}>
40
+ Increment
41
+ </button>
42
+ </div>
43
+ )
44
+ }
45
+ ```
46
+
47
+ ## API
48
+
49
+ ### `StoreProvider`
50
+
51
+ Provider component that makes the store available to all child components.
52
+
53
+ **Props:**
54
+ - `store: StoreAPI<State>` - The store instance (created with `createStore()`)
55
+ - `children: ReactNode` - Child components
56
+
57
+ ### `useStoreState<State>()`
58
+
59
+ Hook that subscribes to store state changes and returns the current state. Component will re-render when state changes.
60
+
61
+ **Returns:** `State` - The current store state
62
+
63
+ ### `useDispatch<Payload>()`
64
+
65
+ Hook that returns a memoized dispatch function for dispatching events.
66
+
67
+ **Returns:** `(eventKey: string, payload?: Payload) => Promise<void>`
68
+
69
+ ### `useStore<State>()`
70
+
71
+ Hook that returns the store instance directly.
72
+
73
+ **Returns:** `StoreAPI<State>` - The store instance (created with `createStore()`)
74
+
@@ -0,0 +1,71 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { StoreAPI } from '@rplx/core';
3
+
4
+ interface StoreProviderProps<State> {
5
+ store: StoreAPI<State>;
6
+ children: ReactNode;
7
+ }
8
+ declare function StoreProvider<State>({ store, children }: StoreProviderProps<State>): React.JSX.Element;
9
+ declare function useStore<State>(): StoreAPI<State>;
10
+
11
+ /**
12
+ * Hook to subscribe to store state changes and trigger re-renders
13
+ * Returns the current state and re-renders when state changes
14
+ */
15
+ declare function useStoreState<State>(): State;
16
+
17
+ /**
18
+ * Hook to get a dispatch function for the store
19
+ * Returns a memoized dispatch function
20
+ */
21
+ declare function useDispatch<Payload = any>(): <P = Payload>(eventKey: string, payload?: P | undefined) => Promise<void>;
22
+
23
+ /**
24
+ * Hook that leverages shared Subscription objects for optimal React memoization.
25
+ *
26
+ * The key insight: Subscription objects are shared (same instance for same key+params),
27
+ * so we can use the Subscription object itself as a stable reference in dependency arrays.
28
+ * This eliminates the need to memoize params arrays or use JSON.stringify.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * function TodoItem({ id }: { id: string }) {
33
+ * const todo = useSubscription('todoById', id)
34
+ * return <div>{todo.title}</div>
35
+ * }
36
+ *
37
+ * function TodoList() {
38
+ * const todos = useSubscription('todos')
39
+ * return <div>{todos.map(...)}</div>
40
+ * }
41
+ * ```
42
+ */
43
+ declare function useSubscription<Result, Params extends any[] = []>(subscriptionKey: string, ...params: Params): Result;
44
+ /**
45
+ * Factory function that creates a reusable hook for a specific subscription key.
46
+ *
47
+ * This is useful when you want to create a typed hook for a specific subscription,
48
+ * making it easier to use in components without passing the key each time.
49
+ * The store is automatically obtained from context, so no store parameter is needed.
50
+ *
51
+ * @example
52
+ * ```tsx
53
+ * // Create the hook once (outside component) - no store needed!
54
+ * const useTodos = createSubscriptionHook('todos')
55
+ * const useTodoById = createSubscriptionHook('todoById')
56
+ *
57
+ * // Use in components
58
+ * function TodoList() {
59
+ * const todos = useTodos() // No params needed
60
+ * return <div>{todos.map(...)}</div>
61
+ * }
62
+ *
63
+ * function TodoItem({ id }: { id: string }) {
64
+ * const todo = useTodoById(id) // Just params
65
+ * return <div>{todo.title}</div>
66
+ * }
67
+ * ```
68
+ */
69
+ declare function createSubscriptionHook<Result, Params extends any[] = []>(subscriptionKey: string): (...params: Params) => Result;
70
+
71
+ export { StoreProvider, createSubscriptionHook, useDispatch, useStore, useStoreState, useSubscription };
@@ -0,0 +1,71 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { StoreAPI } from '@rplx/core';
3
+
4
+ interface StoreProviderProps<State> {
5
+ store: StoreAPI<State>;
6
+ children: ReactNode;
7
+ }
8
+ declare function StoreProvider<State>({ store, children }: StoreProviderProps<State>): React.JSX.Element;
9
+ declare function useStore<State>(): StoreAPI<State>;
10
+
11
+ /**
12
+ * Hook to subscribe to store state changes and trigger re-renders
13
+ * Returns the current state and re-renders when state changes
14
+ */
15
+ declare function useStoreState<State>(): State;
16
+
17
+ /**
18
+ * Hook to get a dispatch function for the store
19
+ * Returns a memoized dispatch function
20
+ */
21
+ declare function useDispatch<Payload = any>(): <P = Payload>(eventKey: string, payload?: P | undefined) => Promise<void>;
22
+
23
+ /**
24
+ * Hook that leverages shared Subscription objects for optimal React memoization.
25
+ *
26
+ * The key insight: Subscription objects are shared (same instance for same key+params),
27
+ * so we can use the Subscription object itself as a stable reference in dependency arrays.
28
+ * This eliminates the need to memoize params arrays or use JSON.stringify.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * function TodoItem({ id }: { id: string }) {
33
+ * const todo = useSubscription('todoById', id)
34
+ * return <div>{todo.title}</div>
35
+ * }
36
+ *
37
+ * function TodoList() {
38
+ * const todos = useSubscription('todos')
39
+ * return <div>{todos.map(...)}</div>
40
+ * }
41
+ * ```
42
+ */
43
+ declare function useSubscription<Result, Params extends any[] = []>(subscriptionKey: string, ...params: Params): Result;
44
+ /**
45
+ * Factory function that creates a reusable hook for a specific subscription key.
46
+ *
47
+ * This is useful when you want to create a typed hook for a specific subscription,
48
+ * making it easier to use in components without passing the key each time.
49
+ * The store is automatically obtained from context, so no store parameter is needed.
50
+ *
51
+ * @example
52
+ * ```tsx
53
+ * // Create the hook once (outside component) - no store needed!
54
+ * const useTodos = createSubscriptionHook('todos')
55
+ * const useTodoById = createSubscriptionHook('todoById')
56
+ *
57
+ * // Use in components
58
+ * function TodoList() {
59
+ * const todos = useTodos() // No params needed
60
+ * return <div>{todos.map(...)}</div>
61
+ * }
62
+ *
63
+ * function TodoItem({ id }: { id: string }) {
64
+ * const todo = useTodoById(id) // Just params
65
+ * return <div>{todo.title}</div>
66
+ * }
67
+ * ```
68
+ */
69
+ declare function createSubscriptionHook<Result, Params extends any[] = []>(subscriptionKey: string): (...params: Params) => Result;
70
+
71
+ export { StoreProvider, createSubscriptionHook, useDispatch, useStore, useStoreState, useSubscription };
package/dist/index.js ADDED
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ StoreProvider: () => StoreProvider,
34
+ createSubscriptionHook: () => createSubscriptionHook,
35
+ useDispatch: () => useDispatch,
36
+ useStore: () => useStore,
37
+ useStoreState: () => useStoreState,
38
+ useSubscription: () => useSubscription
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/StoreContext.tsx
43
+ var import_react = __toESM(require("react"));
44
+ var StoreContext = (0, import_react.createContext)(null);
45
+ function StoreProvider({ store, children }) {
46
+ const subscribersRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
47
+ const storeRef = (0, import_react.useRef)(store);
48
+ (0, import_react.useEffect)(() => {
49
+ storeRef.current = store;
50
+ const subscriptionKey = "__ripple_react_state_tracker__";
51
+ try {
52
+ store.registerSubscription(subscriptionKey, {
53
+ compute: (state) => state
54
+ });
55
+ } catch (error) {
56
+ }
57
+ const unsubscribe = store.subscribe(
58
+ subscriptionKey,
59
+ [],
60
+ (newState) => {
61
+ subscribersRef.current.forEach((callback) => {
62
+ try {
63
+ callback(newState);
64
+ } catch (error) {
65
+ console.error("Error in store subscriber:", error);
66
+ }
67
+ });
68
+ }
69
+ );
70
+ return () => {
71
+ unsubscribe();
72
+ };
73
+ }, [store]);
74
+ const subscribe = import_react.default.useCallback((callback) => {
75
+ subscribersRef.current.add(callback);
76
+ return () => {
77
+ subscribersRef.current.delete(callback);
78
+ };
79
+ }, []);
80
+ return /* @__PURE__ */ import_react.default.createElement(StoreContext.Provider, { value: { store, subscribe } }, children);
81
+ }
82
+ function useStore() {
83
+ const context = (0, import_react.useContext)(StoreContext);
84
+ if (!context) {
85
+ throw new Error("useStore must be used within a StoreProvider");
86
+ }
87
+ return context.store;
88
+ }
89
+
90
+ // src/useStoreState.ts
91
+ var import_react2 = require("react");
92
+ function useStoreState() {
93
+ const context = (0, import_react2.useContext)(StoreContext);
94
+ if (!context) {
95
+ throw new Error("useStoreState must be used within a StoreProvider");
96
+ }
97
+ const { store, subscribe } = context;
98
+ const [state, setState] = (0, import_react2.useState)(() => store.getState());
99
+ (0, import_react2.useEffect)(() => {
100
+ const unsubscribe = subscribe((newState) => {
101
+ setState((prevState) => {
102
+ if (prevState !== newState) {
103
+ return newState;
104
+ }
105
+ return prevState;
106
+ });
107
+ });
108
+ const currentState = store.getState();
109
+ setState(currentState);
110
+ return unsubscribe;
111
+ }, [store, subscribe]);
112
+ return state;
113
+ }
114
+
115
+ // src/useDispatch.ts
116
+ var import_react3 = require("react");
117
+ function useDispatch() {
118
+ const store = useStore();
119
+ const dispatch = (0, import_react3.useCallback)(
120
+ (eventKey, payload) => {
121
+ return store.dispatch(eventKey, payload);
122
+ },
123
+ [store]
124
+ );
125
+ return dispatch;
126
+ }
127
+
128
+ // src/useSubscription.ts
129
+ var import_react4 = require("react");
130
+ function useSubscription(subscriptionKey, ...params) {
131
+ const store = useStore();
132
+ const subscription = (0, import_react4.useMemo)(
133
+ () => store.getSubscription(subscriptionKey, params),
134
+ [store, subscriptionKey, ...params]
135
+ // Spread params - React will handle equality
136
+ );
137
+ const getSnapshot = (0, import_react4.useCallback)(
138
+ () => store.query(subscription.key, subscription.params),
139
+ [store, subscription]
140
+ // Subscription object is stable, so this is stable too
141
+ );
142
+ const subscribe = (0, import_react4.useCallback)(
143
+ (onStoreChange) => {
144
+ return store.subscribe(
145
+ subscription.key,
146
+ subscription.params,
147
+ () => {
148
+ onStoreChange();
149
+ }
150
+ );
151
+ },
152
+ [store, subscription]
153
+ // Subscription object is stable, so this is stable too
154
+ );
155
+ return (0, import_react4.useSyncExternalStore)(
156
+ subscribe,
157
+ getSnapshot,
158
+ getSnapshot
159
+ // Server snapshot (same as client for now)
160
+ );
161
+ }
162
+ function createSubscriptionHook(subscriptionKey) {
163
+ return function useSubscriptionHook(...params) {
164
+ const store = useStore();
165
+ const subscription = (0, import_react4.useMemo)(
166
+ () => store.getSubscription(subscriptionKey, params),
167
+ [store, subscriptionKey, ...params]
168
+ );
169
+ const getSnapshot = (0, import_react4.useCallback)(
170
+ () => store.query(subscription.key, subscription.params),
171
+ [store, subscription]
172
+ );
173
+ const subscribe = (0, import_react4.useCallback)(
174
+ (onStoreChange) => {
175
+ return store.subscribe(
176
+ subscription.key,
177
+ subscription.params,
178
+ () => {
179
+ onStoreChange();
180
+ }
181
+ );
182
+ },
183
+ [store, subscription]
184
+ );
185
+ return (0, import_react4.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
186
+ };
187
+ }
188
+ // Annotate the CommonJS export names for ESM import in node:
189
+ 0 && (module.exports = {
190
+ StoreProvider,
191
+ createSubscriptionHook,
192
+ useDispatch,
193
+ useStore,
194
+ useStoreState,
195
+ useSubscription
196
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,154 @@
1
+ // src/StoreContext.tsx
2
+ import React, { createContext, useContext, useRef, useEffect } from "react";
3
+ var StoreContext = createContext(null);
4
+ function StoreProvider({ store, children }) {
5
+ const subscribersRef = useRef(/* @__PURE__ */ new Set());
6
+ const storeRef = useRef(store);
7
+ useEffect(() => {
8
+ storeRef.current = store;
9
+ const subscriptionKey = "__ripple_react_state_tracker__";
10
+ try {
11
+ store.registerSubscription(subscriptionKey, {
12
+ compute: (state) => state
13
+ });
14
+ } catch (error) {
15
+ }
16
+ const unsubscribe = store.subscribe(
17
+ subscriptionKey,
18
+ [],
19
+ (newState) => {
20
+ subscribersRef.current.forEach((callback) => {
21
+ try {
22
+ callback(newState);
23
+ } catch (error) {
24
+ console.error("Error in store subscriber:", error);
25
+ }
26
+ });
27
+ }
28
+ );
29
+ return () => {
30
+ unsubscribe();
31
+ };
32
+ }, [store]);
33
+ const subscribe = React.useCallback((callback) => {
34
+ subscribersRef.current.add(callback);
35
+ return () => {
36
+ subscribersRef.current.delete(callback);
37
+ };
38
+ }, []);
39
+ return /* @__PURE__ */ React.createElement(StoreContext.Provider, { value: { store, subscribe } }, children);
40
+ }
41
+ function useStore() {
42
+ const context = useContext(StoreContext);
43
+ if (!context) {
44
+ throw new Error("useStore must be used within a StoreProvider");
45
+ }
46
+ return context.store;
47
+ }
48
+
49
+ // src/useStoreState.ts
50
+ import { useState, useEffect as useEffect2, useContext as useContext2 } from "react";
51
+ function useStoreState() {
52
+ const context = useContext2(StoreContext);
53
+ if (!context) {
54
+ throw new Error("useStoreState must be used within a StoreProvider");
55
+ }
56
+ const { store, subscribe } = context;
57
+ const [state, setState] = useState(() => store.getState());
58
+ useEffect2(() => {
59
+ const unsubscribe = subscribe((newState) => {
60
+ setState((prevState) => {
61
+ if (prevState !== newState) {
62
+ return newState;
63
+ }
64
+ return prevState;
65
+ });
66
+ });
67
+ const currentState = store.getState();
68
+ setState(currentState);
69
+ return unsubscribe;
70
+ }, [store, subscribe]);
71
+ return state;
72
+ }
73
+
74
+ // src/useDispatch.ts
75
+ import { useCallback } from "react";
76
+ function useDispatch() {
77
+ const store = useStore();
78
+ const dispatch = useCallback(
79
+ (eventKey, payload) => {
80
+ return store.dispatch(eventKey, payload);
81
+ },
82
+ [store]
83
+ );
84
+ return dispatch;
85
+ }
86
+
87
+ // src/useSubscription.ts
88
+ import { useSyncExternalStore, useMemo, useCallback as useCallback2 } from "react";
89
+ function useSubscription(subscriptionKey, ...params) {
90
+ const store = useStore();
91
+ const subscription = useMemo(
92
+ () => store.getSubscription(subscriptionKey, params),
93
+ [store, subscriptionKey, ...params]
94
+ // Spread params - React will handle equality
95
+ );
96
+ const getSnapshot = useCallback2(
97
+ () => store.query(subscription.key, subscription.params),
98
+ [store, subscription]
99
+ // Subscription object is stable, so this is stable too
100
+ );
101
+ const subscribe = useCallback2(
102
+ (onStoreChange) => {
103
+ return store.subscribe(
104
+ subscription.key,
105
+ subscription.params,
106
+ () => {
107
+ onStoreChange();
108
+ }
109
+ );
110
+ },
111
+ [store, subscription]
112
+ // Subscription object is stable, so this is stable too
113
+ );
114
+ return useSyncExternalStore(
115
+ subscribe,
116
+ getSnapshot,
117
+ getSnapshot
118
+ // Server snapshot (same as client for now)
119
+ );
120
+ }
121
+ function createSubscriptionHook(subscriptionKey) {
122
+ return function useSubscriptionHook(...params) {
123
+ const store = useStore();
124
+ const subscription = useMemo(
125
+ () => store.getSubscription(subscriptionKey, params),
126
+ [store, subscriptionKey, ...params]
127
+ );
128
+ const getSnapshot = useCallback2(
129
+ () => store.query(subscription.key, subscription.params),
130
+ [store, subscription]
131
+ );
132
+ const subscribe = useCallback2(
133
+ (onStoreChange) => {
134
+ return store.subscribe(
135
+ subscription.key,
136
+ subscription.params,
137
+ () => {
138
+ onStoreChange();
139
+ }
140
+ );
141
+ },
142
+ [store, subscription]
143
+ );
144
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
145
+ };
146
+ }
147
+ export {
148
+ StoreProvider,
149
+ createSubscriptionHook,
150
+ useDispatch,
151
+ useStore,
152
+ useStoreState,
153
+ useSubscription
154
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@rplx/react",
3
+ "version": "0.2.0",
4
+ "description": "React bindings for Ripple state management",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format cjs,esm --dts",
21
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
+ "clean": "rm -rf dist"
23
+ },
24
+ "peerDependencies": {
25
+ "react": ">=16.8.0"
26
+ },
27
+ "dependencies": {
28
+ "@rplx/core": "^0.2.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/react": "^18.2.43",
32
+ "react": "^18.2.0",
33
+ "tsup": "^8.0.0",
34
+ "typescript": "^5.2.2"
35
+ },
36
+ "keywords": [
37
+ "state-management",
38
+ "react",
39
+ "re-frame",
40
+ "ripple"
41
+ ],
42
+ "license": "MIT",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/anonymeye/ripple.git",
46
+ "directory": "packages/ripple-react"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/anonymeye/ripple/issues"
50
+ },
51
+ "homepage": "https://github.com/anonymeye/ripple#readme",
52
+ "publishConfig": {
53
+ "access": "public"
54
+ }
55
+ }