@rectify-dev/core 2.1.0 → 2.3.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 +445 -0
- package/dist/index.cjs +803 -403
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +125 -3
- package/dist/index.d.ts +125 -3
- package/dist/index.js +800 -404
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.d.cts +6 -0
- package/dist/jsx-runtime.d.ts +6 -0
- package/package.json +4 -4
package/README.md
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
# @rectify-dev/core
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@rectify-dev/core) [](../../LICENSE)
|
|
4
|
+
|
|
5
|
+
A lightweight React-like UI library built from scratch — fiber reconciler, concurrent rendering, class components, a complete hooks API, lazy/Suspense, and context. All with zero React dependencies and **~10 KB gzipped**.
|
|
6
|
+
|
|
7
|
+
| Feature | Status |
|
|
8
|
+
|---------|--------|
|
|
9
|
+
| Function components + auto-bailout | ✅ |
|
|
10
|
+
| Class components (full lifecycle) | ✅ |
|
|
11
|
+
| `useState`, `useReducer` | ✅ |
|
|
12
|
+
| `useEffect`, `useLayoutEffect` | ✅ |
|
|
13
|
+
| `useRef` (object + callback refs) | ✅ |
|
|
14
|
+
| `useMemo`, `useCallback` | ✅ |
|
|
15
|
+
| `useContext` + `createContext` | ✅ |
|
|
16
|
+
| `useId`, `useDeferredValue` | ✅ |
|
|
17
|
+
| `memo()` with custom comparator | ✅ |
|
|
18
|
+
| `lazy()` + `<Suspense>` | ✅ |
|
|
19
|
+
| SVG elements | ✅ |
|
|
20
|
+
| Client-side router | [`@rectify-dev/router`](../../libs/rectify-router) |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
Scaffold a new project with one command — no config needed:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# pnpm
|
|
30
|
+
pnpm create @rectify-dev/rectify-app my-app
|
|
31
|
+
|
|
32
|
+
# npm
|
|
33
|
+
npm create @rectify-dev/rectify-app@latest my-app
|
|
34
|
+
|
|
35
|
+
# yarn
|
|
36
|
+
yarn create @rectify-dev/rectify-app my-app
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd my-app
|
|
43
|
+
pnpm install
|
|
44
|
+
pnpm dev
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This scaffolds a Vite + TypeScript project pre-configured with the Rectify JSX runtime, `@rectify-dev/vite-plugin`, and a ready-to-edit `src/App.tsx`.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm add @rectify-dev/core
|
|
55
|
+
# or
|
|
56
|
+
npm install @rectify-dev/core
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Configure your bundler to use the Rectify JSX runtime:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
// tsconfig.json
|
|
63
|
+
{
|
|
64
|
+
"compilerOptions": {
|
|
65
|
+
"jsx": "react-jsx",
|
|
66
|
+
"jsxImportSource": "@rectify-dev/core"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Table of Contents
|
|
74
|
+
|
|
75
|
+
- [Quick Start](#quick-start)
|
|
76
|
+
- [Rendering](#rendering)
|
|
77
|
+
- [Hooks](#hooks)
|
|
78
|
+
- [useState](#usestate)
|
|
79
|
+
- [useReducer](#usereducer)
|
|
80
|
+
- [useEffect](#useeffect)
|
|
81
|
+
- [useLayoutEffect](#uselayouteffect)
|
|
82
|
+
- [useRef](#useref)
|
|
83
|
+
- [useMemo](#usememo)
|
|
84
|
+
- [useCallback](#usecallback)
|
|
85
|
+
- [useId](#useid)
|
|
86
|
+
- [useContext / createContext](#usecontext--createcontext)
|
|
87
|
+
- [Class Components](#class-components)
|
|
88
|
+
- [memo](#memo)
|
|
89
|
+
- [lazy + Suspense](#lazy--suspense)
|
|
90
|
+
- [Fragment](#fragment)
|
|
91
|
+
- [TypeScript](#typescript)
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Rendering
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { createRoot } from "@rectify-dev/core";
|
|
99
|
+
|
|
100
|
+
const root = createRoot(document.getElementById("app")!);
|
|
101
|
+
root.render(<App />);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Hooks
|
|
107
|
+
|
|
108
|
+
### useState
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { useState } from "@rectify-dev/core";
|
|
112
|
+
|
|
113
|
+
const Counter = () => {
|
|
114
|
+
const [count, setCount] = useState(0);
|
|
115
|
+
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
|
116
|
+
};
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Supports lazy initializer:
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
const [state, setState] = useState(() => expensiveComputation());
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### useReducer
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import { useReducer } from "@rectify-dev/core";
|
|
131
|
+
|
|
132
|
+
type Action = { type: "inc" } | { type: "dec" };
|
|
133
|
+
|
|
134
|
+
const reducer = (state: number, action: Action) => {
|
|
135
|
+
if (action.type === "inc") return state + 1;
|
|
136
|
+
if (action.type === "dec") return state - 1;
|
|
137
|
+
return state;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const Counter = () => {
|
|
141
|
+
const [count, dispatch] = useReducer(reducer, 0);
|
|
142
|
+
return (
|
|
143
|
+
<>
|
|
144
|
+
<button onClick={() => dispatch({ type: "dec" })}>-</button>
|
|
145
|
+
{count}
|
|
146
|
+
<button onClick={() => dispatch({ type: "inc" })}>+</button>
|
|
147
|
+
</>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### useEffect
|
|
155
|
+
|
|
156
|
+
Runs after the browser has painted. Clean up by returning a function.
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
import { useEffect } from "@rectify-dev/core";
|
|
160
|
+
|
|
161
|
+
const Timer = () => {
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
const id = setInterval(() => console.log("tick"), 1000);
|
|
164
|
+
return () => clearInterval(id);
|
|
165
|
+
}, []); // empty deps → run once on mount
|
|
166
|
+
};
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### useLayoutEffect
|
|
172
|
+
|
|
173
|
+
Same signature as `useEffect` but fires synchronously after DOM mutations, before the browser paints. Use for measuring layout or imperatively updating the DOM.
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
import { useLayoutEffect, useRef } from "@rectify-dev/core";
|
|
177
|
+
|
|
178
|
+
const Tooltip = () => {
|
|
179
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
180
|
+
useLayoutEffect(() => {
|
|
181
|
+
console.log(ref.current?.getBoundingClientRect());
|
|
182
|
+
});
|
|
183
|
+
return <div ref={ref}>hover me</div>;
|
|
184
|
+
};
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### useRef
|
|
190
|
+
|
|
191
|
+
Returns a stable mutable container whose `.current` value persists across renders.
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
import { useRef } from "@rectify-dev/core";
|
|
195
|
+
|
|
196
|
+
const Input = () => {
|
|
197
|
+
const ref = useRef<HTMLInputElement>(null);
|
|
198
|
+
return <input ref={ref} onFocus={() => ref.current?.select()} />;
|
|
199
|
+
};
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Callback refs are also supported:
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
<div ref={(node) => { /* attach */ return () => { /* cleanup */ }; }} />
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
### useMemo
|
|
211
|
+
|
|
212
|
+
Memoises an expensive computation; recomputes only when `deps` change.
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
import { useMemo } from "@rectify-dev/core";
|
|
216
|
+
|
|
217
|
+
const List = ({ items }: { items: number[] }) => {
|
|
218
|
+
const sorted = useMemo(() => [...items].sort((a, b) => a - b), [items]);
|
|
219
|
+
return <ul>{sorted.map(n => <li key={n}>{n}</li>)}</ul>;
|
|
220
|
+
};
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### useCallback
|
|
226
|
+
|
|
227
|
+
Memoises a function reference; useful for stable event handler props passed to `memo`-wrapped children.
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
import { useCallback } from "@rectify-dev/core";
|
|
231
|
+
|
|
232
|
+
const Parent = () => {
|
|
233
|
+
const handleClick = useCallback(() => console.log("click"), []);
|
|
234
|
+
return <Child onClick={handleClick} />;
|
|
235
|
+
};
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
### useId
|
|
241
|
+
|
|
242
|
+
Returns a stable, globally unique string ID that never changes for the lifetime of the component. Ideal for linking form labels to inputs.
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
import { useId } from "@rectify-dev/core";
|
|
246
|
+
|
|
247
|
+
const Field = ({ label }: { label: string }) => {
|
|
248
|
+
const id = useId();
|
|
249
|
+
return (
|
|
250
|
+
<>
|
|
251
|
+
<label htmlFor={id}>{label}</label>
|
|
252
|
+
<input id={id} />
|
|
253
|
+
</>
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Multiple calls in the same component each return a distinct ID:
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
const Form = () => {
|
|
262
|
+
const nameId = useId(); // ":r0:"
|
|
263
|
+
const emailId = useId(); // ":r1:"
|
|
264
|
+
// ...
|
|
265
|
+
};
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
### useContext / createContext
|
|
271
|
+
|
|
272
|
+
Share values through the tree without prop drilling.
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
import { createContext, useContext } from "@rectify-dev/core";
|
|
276
|
+
|
|
277
|
+
const ThemeContext = createContext<"light" | "dark">("light");
|
|
278
|
+
|
|
279
|
+
const App = () => (
|
|
280
|
+
<ThemeContext.Provider value="dark">
|
|
281
|
+
<Page />
|
|
282
|
+
</ThemeContext.Provider>
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const Page = () => {
|
|
286
|
+
const theme = useContext(ThemeContext);
|
|
287
|
+
return <div className={theme}>...</div>;
|
|
288
|
+
};
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Class Components
|
|
294
|
+
|
|
295
|
+
Extend `Component<Props, State>` to write class-based components.
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
import { Component } from "@rectify-dev/core";
|
|
299
|
+
|
|
300
|
+
interface Props { title: string }
|
|
301
|
+
interface State { open: boolean }
|
|
302
|
+
|
|
303
|
+
class Accordion extends Component<Props, State> {
|
|
304
|
+
state = { open: false };
|
|
305
|
+
|
|
306
|
+
componentDidMount() {
|
|
307
|
+
console.log("mounted");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
componentDidUpdate(prevProps: Props, prevState: State) {
|
|
311
|
+
// prevProps and prevState are correct snapshots from before the render
|
|
312
|
+
if (prevState.open !== this.state.open) {
|
|
313
|
+
console.log("toggled", this.state.open);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
componentWillUnmount() {
|
|
318
|
+
console.log("unmounted");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
shouldComponentUpdate(nextProps: Props, nextState: State) {
|
|
322
|
+
// return false to skip re-render
|
|
323
|
+
return nextState.open !== this.state.open || nextProps.title !== this.props.title;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
render() {
|
|
327
|
+
return (
|
|
328
|
+
<div>
|
|
329
|
+
<button onClick={() => this.setState({ open: !this.state.open })}>
|
|
330
|
+
{this.props.title}
|
|
331
|
+
</button>
|
|
332
|
+
{this.state.open && <div>content</div>}
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`setState` accepts a partial state object or an updater function:
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
this.setState({ count: 42 });
|
|
343
|
+
this.setState(prev => ({ count: prev.count + 1 }));
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## memo
|
|
349
|
+
|
|
350
|
+
Prevents re-renders when props are shallowly equal.
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
import { memo } from "@rectify-dev/core";
|
|
354
|
+
|
|
355
|
+
const Item = memo(({ name }: { name: string }) => <li>{name}</li>);
|
|
356
|
+
|
|
357
|
+
// Custom comparator
|
|
358
|
+
const Item = memo(
|
|
359
|
+
({ value }: { value: number }) => <span>{value}</span>,
|
|
360
|
+
(prev, next) => prev.value === next.value,
|
|
361
|
+
);
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## lazy + Suspense
|
|
367
|
+
|
|
368
|
+
Code-split a component and show a fallback while it loads.
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
import { lazy, Suspense } from "@rectify-dev/core";
|
|
372
|
+
|
|
373
|
+
const HeavyChart = lazy(() => import("./HeavyChart"));
|
|
374
|
+
|
|
375
|
+
const Dashboard = () => (
|
|
376
|
+
<Suspense fallback={<div>Loading chart...</div>}>
|
|
377
|
+
<HeavyChart data={data} />
|
|
378
|
+
</Suspense>
|
|
379
|
+
);
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Multiple lazy components under one `Suspense` boundary are all handled — the fallback shows until every child resolves.
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Fragment
|
|
387
|
+
|
|
388
|
+
Group children without adding a DOM node.
|
|
389
|
+
|
|
390
|
+
```tsx
|
|
391
|
+
import { Fragment } from "@rectify-dev/core";
|
|
392
|
+
|
|
393
|
+
const Rows = () => (
|
|
394
|
+
<Fragment>
|
|
395
|
+
<tr><td>A</td></tr>
|
|
396
|
+
<tr><td>B</td></tr>
|
|
397
|
+
</Fragment>
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// Shorthand (requires Babel / TSX support)
|
|
401
|
+
const Rows = () => (
|
|
402
|
+
<>
|
|
403
|
+
<tr><td>A</td></tr>
|
|
404
|
+
<tr><td>B</td></tr>
|
|
405
|
+
</>
|
|
406
|
+
);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## TypeScript
|
|
412
|
+
|
|
413
|
+
All JSX element attributes, event handlers, CSS properties, and ARIA attributes are fully typed. Import convenience types directly from the package:
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
import type {
|
|
417
|
+
FC,
|
|
418
|
+
RectifyNode,
|
|
419
|
+
CSSProperties,
|
|
420
|
+
HTMLAttributes,
|
|
421
|
+
InputHTMLAttributes,
|
|
422
|
+
SuspenseProps,
|
|
423
|
+
RefObject,
|
|
424
|
+
RectifyContext,
|
|
425
|
+
Reducer,
|
|
426
|
+
Dispatch,
|
|
427
|
+
SyntheticMouseEvent,
|
|
428
|
+
SyntheticKeyboardEvent,
|
|
429
|
+
// ... and all other synthetic event types
|
|
430
|
+
} from "@rectify-dev/core";
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### `key` prop
|
|
434
|
+
|
|
435
|
+
The `key` prop is accepted on every element (host and component) without appearing in the component's own props type:
|
|
436
|
+
|
|
437
|
+
```tsx
|
|
438
|
+
items.map(item => <Row key={item.id} data={item} />);
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## License
|
|
444
|
+
|
|
445
|
+
MIT
|