examples: add 6 new over-engineering survivors + platform-native guide (#109)
New examples (examples/): - modal-dialog: <dialog> vs Radix/react-modal - url-params: URLSearchParams vs query-string - number-formatting: Intl.NumberFormat vs numeral - infinite-scroll: IntersectionObserver vs react-infinite-scroll-component - deep-clone: structuredClone vs lodash.cloneDeep / JSON hack - group-by: Object.groupBy vs lodash.groupBy New doc (docs/platform-native.md): Comprehensive reference of platform-native solutions across HTML elements, CSS, Browser APIs, Node.js stdlib, Python stdlib, and database features. Covers 60+ cases where the platform already has what developers reach for a package to do.
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# Deep Clone
|
||||
|
||||
**Task:** "Deep clone this object."
|
||||
|
||||
## Without Ponytail
|
||||
|
||||
```bash
|
||||
npm install lodash
|
||||
```
|
||||
|
||||
```js
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
const copy = cloneDeep(original);
|
||||
```
|
||||
|
||||
Or the classic hack:
|
||||
|
||||
```js
|
||||
// fragile: loses Date, undefined, Map, Set, circular refs, functions
|
||||
const copy = JSON.parse(JSON.stringify(original));
|
||||
```
|
||||
|
||||
## With Ponytail
|
||||
|
||||
```js
|
||||
// ponytail: structuredClone does this
|
||||
const copy = structuredClone(original);
|
||||
```
|
||||
|
||||
**1 dependency (or a fragile hack) → 1 built-in.** `structuredClone` handles `Date`, `Map`, `Set`, `ArrayBuffer`, `RegExp`, circular references, and more — everything `JSON.parse/stringify` silently drops. Available in every browser since 2022 and Node.js since v17. Pull lodash in when you need the rest of it, not for one function.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Group By
|
||||
|
||||
**Task:** "Group this array of objects by a key."
|
||||
|
||||
## Without Ponytail
|
||||
|
||||
```bash
|
||||
npm install lodash
|
||||
```
|
||||
|
||||
```js
|
||||
import { groupBy } from "lodash";
|
||||
|
||||
const byStatus = groupBy(orders, "status");
|
||||
// → { pending: [...], shipped: [...], delivered: [...] }
|
||||
```
|
||||
|
||||
Or the hand-rolled version:
|
||||
|
||||
```js
|
||||
const byStatus = orders.reduce((acc, order) => {
|
||||
(acc[order.status] ??= []).push(order);
|
||||
return acc;
|
||||
}, {});
|
||||
```
|
||||
|
||||
## With Ponytail
|
||||
|
||||
```js
|
||||
// ponytail: Object.groupBy does this
|
||||
const byStatus = Object.groupBy(orders, order => order.status);
|
||||
// → { pending: [...], shipped: [...], delivered: [...] }
|
||||
```
|
||||
|
||||
**1 dependency (or a reduce) → 1 built-in.** `Object.groupBy` shipped in Chrome 117, Firefox 119, Safari 17.4, Node.js 21. If you need a `Map` instead of a plain object: `Map.groupBy(orders, o => o.status)`. Check your target runtime; if you need IE11 or old Node, the `reduce` one-liner is still the right call — not lodash.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Infinite Scroll
|
||||
|
||||
**Task:** "Load more items when the user scrolls to the bottom."
|
||||
|
||||
## Without Ponytail
|
||||
|
||||
```bash
|
||||
npm install react-infinite-scroll-component
|
||||
```
|
||||
|
||||
```jsx
|
||||
import InfiniteScroll from "react-infinite-scroll-component";
|
||||
|
||||
export function Feed({ items, fetchMore, hasMore }) {
|
||||
return (
|
||||
<InfiniteScroll
|
||||
dataLength={items.length}
|
||||
next={fetchMore}
|
||||
hasMore={hasMore}
|
||||
loader={<Spinner />}
|
||||
endMessage={<p>No more items</p>}
|
||||
scrollThreshold={0.9}
|
||||
>
|
||||
{items.map(item => <Card key={item.id} item={item} />)}
|
||||
</InfiniteScroll>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
A dependency to watch scroll position and fire a callback.
|
||||
|
||||
## With Ponytail
|
||||
|
||||
```jsx
|
||||
// ponytail: IntersectionObserver does this, no scroll listener needed
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export function Feed({ items, fetchMore, hasMore }) {
|
||||
const sentinel = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
if (entry.isIntersecting && hasMore) fetchMore();
|
||||
});
|
||||
if (sentinel.current) observer.observe(sentinel.current);
|
||||
return () => observer.disconnect();
|
||||
}, [hasMore, fetchMore]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map(item => <Card key={item.id} item={item} />)}
|
||||
<div ref={sentinel} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**1 dependency → 0 dependencies.** `IntersectionObserver` fires only when the sentinel enters the viewport — no scroll event, no throttling, no jank. Ships in every browser. The library wraps exactly this API.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Modal Dialog
|
||||
|
||||
**Task:** "Add a modal dialog for the delete confirmation."
|
||||
|
||||
## Without Ponytail
|
||||
|
||||
```bash
|
||||
npm install @radix-ui/react-dialog
|
||||
# or: npm install react-modal
|
||||
```
|
||||
|
||||
```jsx
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { useState } from "react";
|
||||
|
||||
export function DeleteModal({ onConfirm, onCancel }) {
|
||||
return (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger asChild>
|
||||
<button className="btn-danger">Delete</button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-overlay" />
|
||||
<Dialog.Content className="dialog-content">
|
||||
<Dialog.Title>Confirm deletion</Dialog.Title>
|
||||
<Dialog.Description>This action cannot be undone.</Dialog.Description>
|
||||
<div className="dialog-actions">
|
||||
<Dialog.Close asChild>
|
||||
<button onClick={onCancel}>Cancel</button>
|
||||
</Dialog.Close>
|
||||
<button className="btn-danger" onClick={onConfirm}>Delete</button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
A dependency, a portal, an overlay, a root, a trigger, a content wrapper — to show a box with two buttons.
|
||||
|
||||
## With Ponytail
|
||||
|
||||
```html
|
||||
<!-- ponytail: browser has one, with focus trapping and backdrop built in -->
|
||||
<dialog id="confirm-delete">
|
||||
<p>This action cannot be undone.</p>
|
||||
<button id="cancel">Cancel</button>
|
||||
<button id="confirm">Delete</button>
|
||||
</dialog>
|
||||
```
|
||||
|
||||
```js
|
||||
const dialog = document.getElementById("confirm-delete");
|
||||
document.getElementById("cancel").onclick = () => dialog.close();
|
||||
document.getElementById("confirm").onclick = () => { onConfirm(); dialog.close(); };
|
||||
|
||||
// Open it:
|
||||
dialog.showModal();
|
||||
```
|
||||
|
||||
**1 dependency + 30 lines → 0 dependencies + 8 lines.** The native `<dialog>` traps focus automatically, closes on Escape, renders a backdrop via `::backdrop`, and is accessible by default. All browsers since 2022. The library was solving a problem the platform solved.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Number Formatting
|
||||
|
||||
**Task:** "Format numbers as currency and with thousand separators."
|
||||
|
||||
## Without Ponytail
|
||||
|
||||
```bash
|
||||
npm install numeral
|
||||
# or: npm install accounting
|
||||
```
|
||||
|
||||
```js
|
||||
import numeral from "numeral";
|
||||
|
||||
numeral(1234567.89).format("$1,234.00"); // "$1,234,567.89"
|
||||
numeral(0.745).format("0.0%"); // "74.5%"
|
||||
numeral(1500).format("0.0a"); // "1.5k"
|
||||
```
|
||||
|
||||
## With Ponytail
|
||||
|
||||
```js
|
||||
// ponytail: Intl.NumberFormat does this, locale-aware
|
||||
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" })
|
||||
.format(1234567.89);
|
||||
// → "$1,234,567.89"
|
||||
|
||||
new Intl.NumberFormat("en-US", { style: "percent" })
|
||||
.format(0.745);
|
||||
// → "74.5%"
|
||||
|
||||
new Intl.NumberFormat("en-US", { notation: "compact" })
|
||||
.format(1500);
|
||||
// → "1.5K"
|
||||
```
|
||||
|
||||
**1 dependency → 0 dependencies.** `Intl.NumberFormat` is built into every JS runtime, handles every locale correctly, and gets currency symbols, decimal separators, and grouping right for any market without a lookup table. A library that hardcodes formats will always be wrong for someone.
|
||||
@@ -0,0 +1,41 @@
|
||||
# URL Parameters
|
||||
|
||||
**Task:** "Parse and build URL query strings."
|
||||
|
||||
## Without Ponytail
|
||||
|
||||
```bash
|
||||
npm install query-string
|
||||
# 4.5 kB gzipped, 3.5M downloads/week
|
||||
```
|
||||
|
||||
```js
|
||||
import qs from "query-string";
|
||||
|
||||
// Parse
|
||||
const params = qs.parse(location.search);
|
||||
// → { page: "2", sort: "name", tags: ["js", "css"] }
|
||||
|
||||
// Build
|
||||
const url = qs.stringify({ page: 2, sort: "name", tags: ["js", "css"] });
|
||||
// → "page=2&sort=name&tags=js&tags=css"
|
||||
```
|
||||
|
||||
## With Ponytail
|
||||
|
||||
```js
|
||||
// ponytail: URLSearchParams does this
|
||||
const params = new URLSearchParams(location.search);
|
||||
|
||||
// Read
|
||||
params.get("page"); // "2"
|
||||
params.getAll("tags"); // ["js", "css"]
|
||||
|
||||
// Build
|
||||
const out = new URLSearchParams({ page: 2, sort: "name" });
|
||||
out.append("tags", "js");
|
||||
out.append("tags", "css");
|
||||
out.toString(); // "page=2&sort=name&tags=js&tags=css"
|
||||
```
|
||||
|
||||
**1 dependency → 0 dependencies.** `URLSearchParams` is in every browser and in Node.js since v10. It handles encoding, repeated keys, and iteration. The package was a polyfill for an API that has shipped everywhere for years.
|
||||
Reference in New Issue
Block a user