what-core 0.4.2 → 0.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/hooks.js CHANGED
@@ -1,299 +1,206 @@
1
- // What Framework - Hooks
2
- // React-familiar hooks backed by signals. Zero overhead when deps don't change.
3
-
4
1
  import { signal, computed, effect, batch, untrack, __DEV__ } from './reactive.js';
5
2
  import { getCurrentComponent } from './dom.js';
6
-
7
3
  function getCtx() {
8
- const ctx = getCurrentComponent();
9
- if (!ctx) {
10
- throw new Error(
11
- '[what] Hooks must be called inside a component function. ' +
12
- 'If you need reactive state outside a component, use signal() directly.'
13
- );
14
- }
15
- return ctx;
16
- }
17
-
4
+ const ctx = getCurrentComponent();
5
+ if (!ctx) {
6
+ throw new Error(
7
+ '[what] Hooks must be called inside a component function. ' +
8
+ 'If you need reactive state outside a component, use signal() directly.'
9
+ );
10
+ }
11
+ return ctx;
12
+ }
18
13
  function getHook(ctx) {
19
- const index = ctx.hookIndex++;
20
- return { index, exists: index < ctx.hooks.length };
14
+ const index = ctx.hookIndex++;
15
+ return { index, exists: index < ctx.hooks.length };
21
16
  }
22
-
23
- // --- useState ---
24
- // Returns [value, setter]. Setter triggers re-render of this component only.
25
-
17
+ let _useMemoNoDepsWarned = false;
26
18
  export function useState(initial) {
27
- const ctx = getCtx();
28
- const { index, exists } = getHook(ctx);
29
-
30
- if (!exists) {
31
- const s = signal(typeof initial === 'function' ? initial() : initial);
32
- ctx.hooks[index] = s;
33
- }
34
-
35
- const s = ctx.hooks[index];
36
- return [s(), s.set];
37
- }
38
-
39
- // --- useSignal ---
40
- // Returns the raw signal. More powerful: read with sig(), write with sig.set(v).
41
- // Avoids array destructuring overhead.
42
-
19
+ const ctx = getCtx();
20
+ const { index, exists } = getHook(ctx);
21
+ if (!exists) {
22
+ const s = signal(typeof initial === 'function' ? initial() : initial);
23
+ ctx.hooks[index] = s;
24
+ }
25
+ const s = ctx.hooks[index];
26
+ return [s(), s.set];
27
+ }
43
28
  export function useSignal(initial) {
44
- const ctx = getCtx();
45
- const { index, exists } = getHook(ctx);
46
-
47
- if (!exists) {
48
- ctx.hooks[index] = signal(typeof initial === 'function' ? initial() : initial);
49
- }
50
-
51
- return ctx.hooks[index];
52
- }
53
-
54
- // --- useComputed ---
55
- // Derived value. Only recomputes when signal deps change.
56
-
29
+ const ctx = getCtx();
30
+ const { index, exists } = getHook(ctx);
31
+ if (!exists) {
32
+ ctx.hooks[index] = signal(typeof initial === 'function' ? initial() : initial);
33
+ }
34
+ return ctx.hooks[index];
35
+ }
57
36
  export function useComputed(fn) {
58
- const ctx = getCtx();
59
- const { index, exists } = getHook(ctx);
60
-
61
- if (!exists) {
62
- ctx.hooks[index] = computed(fn);
63
- }
64
-
65
- return ctx.hooks[index];
66
- }
67
-
68
- // --- useEffect ---
69
- // Side effect that runs after render. Cleanup function returned by fn is called
70
- // before re-running and on unmount.
71
-
37
+ const ctx = getCtx();
38
+ const { index, exists } = getHook(ctx);
39
+ if (!exists) {
40
+ ctx.hooks[index] = computed(fn);
41
+ }
42
+ return ctx.hooks[index];
43
+ }
72
44
  export function useEffect(fn, deps) {
73
- const ctx = getCtx();
74
- const { index, exists } = getHook(ctx);
75
-
76
- if (!exists) {
77
- ctx.hooks[index] = { deps: undefined, cleanup: null };
78
- }
79
-
80
- const hook = ctx.hooks[index];
81
-
82
- if (depsChanged(hook.deps, deps)) {
83
- // Schedule after current render
84
- queueMicrotask(() => {
85
- if (ctx.disposed) return;
86
- if (hook.cleanup) hook.cleanup();
87
- hook.cleanup = fn() || null;
88
- });
89
- hook.deps = deps;
90
- }
91
- }
92
-
93
- // --- useMemo ---
94
- // Memoized value. Only recomputes when deps change.
95
-
45
+ const ctx = getCtx();
46
+ const { index, exists } = getHook(ctx);
47
+ if (!exists) {
48
+ ctx.hooks[index] = { deps: undefined, cleanup: null };
49
+ }
50
+ const hook = ctx.hooks[index];
51
+ if (depsChanged(hook.deps, deps)) {
52
+ queueMicrotask(() => {
53
+ if (ctx.disposed) return;
54
+ if (hook.cleanup) hook.cleanup();
55
+ hook.cleanup = fn() || null;
56
+ });
57
+ hook.deps = deps;
58
+ }
59
+ }
96
60
  export function useMemo(fn, deps) {
97
- const ctx = getCtx();
98
- const { index, exists } = getHook(ctx);
99
-
100
- if (!exists) {
101
- ctx.hooks[index] = { value: undefined, deps: undefined };
102
- }
103
-
104
- const hook = ctx.hooks[index];
105
-
106
- if (depsChanged(hook.deps, deps)) {
107
- hook.value = fn();
108
- hook.deps = deps;
109
- }
110
-
111
- return hook.value;
112
- }
113
-
114
- // --- useCallback ---
115
- // Memoized callback. Identity-stable when deps don't change.
116
-
61
+ const ctx = getCtx();
62
+ const { index, exists } = getHook(ctx);
63
+ if (__DEV__ && deps === undefined && !_useMemoNoDepsWarned) {
64
+ _useMemoNoDepsWarned = true;
65
+ console.warn(
66
+ '[what] useMemo() called without a deps array. ' +
67
+ 'This recomputes every render. Use useComputed() for signal-derived values, ' +
68
+ 'or pass deps to useMemo().'
69
+ );
70
+ }
71
+ if (!exists) {
72
+ ctx.hooks[index] = { value: undefined, deps: undefined };
73
+ }
74
+ const hook = ctx.hooks[index];
75
+ if (depsChanged(hook.deps, deps)) {
76
+ hook.value = fn();
77
+ hook.deps = deps;
78
+ }
79
+ return hook.value;
80
+ }
117
81
  export function useCallback(fn, deps) {
118
- return useMemo(() => fn, deps);
82
+ return useMemo(() => fn, deps);
119
83
  }
120
-
121
- // --- useRef ---
122
- // Mutable ref object. Does NOT trigger re-renders.
123
-
124
84
  export function useRef(initial) {
125
- const ctx = getCtx();
126
- const { index, exists } = getHook(ctx);
127
-
128
- if (!exists) {
129
- ctx.hooks[index] = { current: initial };
130
- }
131
-
132
- return ctx.hooks[index];
133
- }
134
-
135
- // --- useContext ---
136
- // Read from the nearest Provider in the component tree, or the default value.
137
- // Uses _parentCtx chain (persistent tree) instead of componentStack (runtime stack)
138
- // so context works correctly in re-renders, effects, and event handlers.
139
-
85
+ const ctx = getCtx();
86
+ const { index, exists } = getHook(ctx);
87
+ if (!exists) {
88
+ ctx.hooks[index] = { current: initial };
89
+ }
90
+ return ctx.hooks[index];
91
+ }
140
92
  export function useContext(context) {
141
- // Walk up the _parentCtx chain to find the nearest provider
142
- let ctx = getCurrentComponent();
143
- if (__DEV__ && !ctx) {
144
- console.warn(
145
- `[what] useContext(${context?.displayName || 'Context'}) called outside of component render. ` +
146
- 'useContext must be called during component rendering, not inside effects or event handlers. ' +
147
- 'Store the context value in a variable during render and use that variable in your callback.'
148
- );
149
- }
150
- while (ctx) {
151
- if (ctx._contextValues && ctx._contextValues.has(context)) {
152
- const val = ctx._contextValues.get(context);
153
- // If the stored value is a signal, read it to subscribe
154
- return (val && val._signal) ? val() : val;
155
- }
156
- ctx = ctx._parentCtx;
157
- }
158
- return context._defaultValue;
159
- }
160
-
161
- // --- createContext ---
162
- // Tree-scoped context: Provider sets value for its subtree only.
163
- // Multiple providers can coexist — each subtree sees its own value.
164
- // Context values are wrapped in signals so consumers re-render when values change.
165
-
93
+ let ctx = getCurrentComponent();
94
+ if (__DEV__ && !ctx) {
95
+ console.warn(
96
+ `[what] useContext(${context?.displayName || 'Context'}) called outside of component render. ` +
97
+ 'useContext must be called during component rendering, not inside effects or event handlers. ' +
98
+ 'Store the context value in a variable during render and use that variable in your callback.'
99
+ );
100
+ }
101
+ while (ctx) {
102
+ if (ctx._contextValues && ctx._contextValues.has(context)) {
103
+ const val = ctx._contextValues.get(context);
104
+ return (val && val._signal) ? val() : val;
105
+ }
106
+ ctx = ctx._parentCtx;
107
+ }
108
+ return context._defaultValue;
109
+ }
166
110
  export function createContext(defaultValue) {
167
- const context = {
168
- _defaultValue: defaultValue,
169
- Provider: ({ value, children }) => {
170
- const ctx = getCtx();
171
- if (!ctx._contextValues) ctx._contextValues = new Map();
172
- if (!ctx._contextSignals) ctx._contextSignals = new Map();
173
-
174
- // Create or update the context signal
175
- if (!ctx._contextSignals.has(context)) {
176
- const s = signal(value);
177
- ctx._contextSignals.set(context, s);
178
- ctx._contextValues.set(context, s);
179
- } else {
180
- ctx._contextSignals.get(context).set(value);
181
- }
182
- return children;
183
- },
184
- };
185
- return context;
186
- }
187
-
188
- // --- useReducer ---
189
- // State management with a reducer function (like React).
190
-
111
+ const context = {
112
+ _defaultValue: defaultValue,
113
+ Provider: ({ value, children }) => {
114
+ const ctx = getCtx();
115
+ if (!ctx._contextValues) ctx._contextValues = new Map();
116
+ if (!ctx._contextSignals) ctx._contextSignals = new Map();
117
+ if (!ctx._contextSignals.has(context)) {
118
+ const s = signal(value);
119
+ ctx._contextSignals.set(context, s);
120
+ ctx._contextValues.set(context, s);
121
+ } else {
122
+ ctx._contextSignals.get(context).set(value);
123
+ }
124
+ return children;
125
+ },
126
+ };
127
+ return context;
128
+ }
191
129
  export function useReducer(reducer, initialState, init) {
192
- const ctx = getCtx();
193
- const { index, exists } = getHook(ctx);
194
-
195
- if (!exists) {
196
- const initial = init ? init(initialState) : initialState;
197
- const s = signal(initial);
198
- const dispatch = (action) => {
199
- s.set(prev => reducer(prev, action));
200
- };
201
- ctx.hooks[index] = { signal: s, dispatch };
202
- }
203
-
204
- const hook = ctx.hooks[index];
205
- return [hook.signal(), hook.dispatch];
206
- }
207
-
208
- // --- onMount ---
209
- // Run callback once when component mounts. SolidJS-style lifecycle.
210
-
130
+ const ctx = getCtx();
131
+ const { index, exists } = getHook(ctx);
132
+ if (!exists) {
133
+ const initial = init ? init(initialState) : initialState;
134
+ const s = signal(initial);
135
+ const dispatch = (action) => {
136
+ s.set(prev => reducer(prev, action));
137
+ };
138
+ ctx.hooks[index] = { signal: s, dispatch };
139
+ }
140
+ const hook = ctx.hooks[index];
141
+ return [hook.signal(), hook.dispatch];
142
+ }
211
143
  export function onMount(fn) {
212
- const ctx = getCtx();
213
- if (!ctx.mounted) {
214
- ctx._mountCallbacks = ctx._mountCallbacks || [];
215
- ctx._mountCallbacks.push(fn);
216
- }
217
- }
218
-
219
- // --- onCleanup ---
220
- // Register cleanup function to run when component unmounts.
221
-
144
+ const ctx = getCtx();
145
+ if (!ctx.mounted) {
146
+ ctx._mountCallbacks = ctx._mountCallbacks || [];
147
+ ctx._mountCallbacks.push(fn);
148
+ }
149
+ }
222
150
  export function onCleanup(fn) {
223
- const ctx = getCtx();
224
- ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
225
- ctx._cleanupCallbacks.push(fn);
226
- }
227
-
228
- // --- createResource ---
229
- // Reactive data fetching primitive (SolidJS-style).
230
- // Returns [data, { loading, error, refetch, mutate }]
231
-
151
+ const ctx = getCtx();
152
+ ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
153
+ ctx._cleanupCallbacks.push(fn);
154
+ }
232
155
  export function createResource(fetcher, options = {}) {
233
- const data = signal(options.initialValue ?? null);
234
- const loading = signal(!options.initialValue);
235
- const error = signal(null);
236
-
237
- let controller = null;
238
-
239
- const refetch = async (source) => {
240
- // Abort previous request
241
- if (controller) controller.abort();
242
- controller = new AbortController();
243
- const { signal: abortSignal } = controller;
244
-
245
- loading.set(true);
246
- error.set(null);
247
-
248
- try {
249
- const result = await fetcher(source, { signal: abortSignal });
250
-
251
- // Only update if not aborted
252
- if (!abortSignal.aborted) {
253
- batch(() => {
254
- data.set(result);
255
- loading.set(false);
256
- });
257
- }
258
- } catch (e) {
259
- if (!abortSignal.aborted) {
260
- batch(() => {
261
- error.set(e);
262
- loading.set(false);
263
- });
264
- }
265
- }
266
- };
267
-
268
- const mutate = (value) => {
269
- data.set(typeof value === 'function' ? value(data()) : value);
270
- };
271
-
272
- // Register cleanup with component lifecycle: abort on unmount
273
- const ctx = getCurrentComponent?.();
274
- if (ctx) {
275
- ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
276
- ctx._cleanupCallbacks.push(() => {
277
- if (controller) controller.abort();
278
- });
279
- }
280
-
281
- // Initial fetch if no initial value
282
- if (!options.initialValue) {
283
- refetch(options.source);
284
- }
285
-
286
- return [data, { loading, error, refetch, mutate }];
287
- }
288
-
289
- // --- Dep comparison ---
290
-
291
- function depsChanged(oldDeps, newDeps) {
292
- if (oldDeps === undefined) return true;
293
- if (!oldDeps || !newDeps) return true;
294
- if (oldDeps.length !== newDeps.length) return true;
295
- for (let i = 0; i < oldDeps.length; i++) {
296
- if (!Object.is(oldDeps[i], newDeps[i])) return true;
297
- }
298
- return false;
156
+ const data = signal(options.initialValue ?? null);
157
+ const loading = signal(!options.initialValue);
158
+ const error = signal(null);
159
+ let controller = null;
160
+ const refetch = async (source) => {
161
+ if (controller) controller.abort();
162
+ controller = new AbortController();
163
+ const { signal: abortSignal } = controller;
164
+ loading.set(true);
165
+ error.set(null);
166
+ try {
167
+ const result = await fetcher(source, { signal: abortSignal });
168
+ if (!abortSignal.aborted) {
169
+ batch(() => {
170
+ data.set(result);
171
+ loading.set(false);
172
+ });
173
+ }
174
+ } catch (e) {
175
+ if (!abortSignal.aborted) {
176
+ batch(() => {
177
+ error.set(e);
178
+ loading.set(false);
179
+ });
180
+ }
181
+ }
182
+ };
183
+ const mutate = (value) => {
184
+ data.set(typeof value === 'function' ? value(data()) : value);
185
+ };
186
+ const ctx = getCurrentComponent?.();
187
+ if (ctx) {
188
+ ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
189
+ ctx._cleanupCallbacks.push(() => {
190
+ if (controller) controller.abort();
191
+ });
192
+ }
193
+ if (!options.initialValue) {
194
+ refetch(options.source);
195
+ }
196
+ return [data, { loading, error, refetch, mutate }];
299
197
  }
198
+ function depsChanged(oldDeps, newDeps) {
199
+ if (oldDeps === undefined) return true;
200
+ if (!oldDeps || !newDeps) return true;
201
+ if (oldDeps.length !== newDeps.length) return true;
202
+ for (let i = 0; i < oldDeps.length; i++) {
203
+ if (!Object.is(oldDeps[i], newDeps[i])) return true;
204
+ }
205
+ return false;
206
+ }