diff --git a/CHANGELOG.md b/CHANGELOG.md index 131c800..e07f1a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,17 @@ by one list, and depth is a number carried on a row rather than a level of nesting in the component tree. Alongside it, everything that would otherwise have forced a major bump later was -settled first: the node types (#492), the `level` base (#493) and the list's prop -surface (#451). That is what the version number is for. +settled first: the node types (#492), the `level` base (#493) and the list's +prop surface (#451). That is what the version number is for. + +### At a glance + +| | 0.15.0 | 1.0.0 | +| ------------------------------------- | -------------------- | ------------------------- | +| Lists mounted for an N-level tree | N | **1** | +| Rows mounted for a 20,000-level tree | every node | **bounded by the window** | +| Work per render of the parent | whole tree re-hashed | **none** | +| Runtime dependencies | `object-hash` | **none** | ### Upgrading from 0.15.0 @@ -27,50 +36,68 @@ Nothing in the documented usage changes. `data`, `renderNode`, `INode` are still what you import. Two things may need attention, and both surface as type errors rather than -silently: +silently. + +**A node parameter annotated `INode`** that reads `opened` or `hidden` expecting +a `boolean` wants the other type now: + +```tsx +// before +const renderNode = (node: INode) => {node.opened ? '▾' : '▸'} -- if you annotate a node parameter as `INode` and read `opened` or `hidden` - expecting a `boolean`, switch that annotation to `IRenderedNode` -- if you persisted `_internalId` values anywhere, they are paths now rather than - content hashes +// after +const renderNode = (node: IRenderedNode) => {node.opened ? '▾' : '▸'} +``` + +**Persisted `_internalId` values.** They are paths now rather than content +hashes, so anything stored by a previous version will no longer match. ### Breaking -- **`INode` now describes the node you pass in, not the node the list hands - back.** It required `_internalId` — which the library assigns and a caller - cannot know — so the only exported node type could not type the input: - `const data: INode[] = [{title: 'x'}]` did not compile. `opened` and `hidden` - are optional now, and a second exported type, **`IRenderedNode`**, describes - what `renderNode` and `onNodePressed` receive, where `_internalId` is a - `string` and `opened` a `boolean`, both guaranteed. - - Property *access* keeps compiling either way, because the index signature - resolves any property to `any`. The narrowing is `opened` and `hidden` on an - input node, which are now `boolean | undefined`. Typing a `renderNode` - parameter as `IRenderedNode` gets the guarantees back, and gets them honestly — - previously the required property and the index signature contradicted each - other. - - `getChildrenName` and `keyExtractor` are now typed with `INode`, which is what - they were already being called with. They claimed an `_internalId` that was - not there. -- **`data` is typed `readonly INode[]`** rather than `any`, which is the point of - having an input type. +#### `INode` describes the node you pass in, not the node the list hands back + +It required `_internalId` — which the library assigns and a caller cannot know — +so the only exported node type could not describe the input: + +```ts +const data: INode[] = [{ title: 'Node 1' }]; // did not compile in 0.15.0 +``` + +`opened` and `hidden` are optional now, and a second exported type, +**`IRenderedNode`**, describes what `renderNode` and `onNodePressed` receive, +where `_internalId` is a `string` and `opened` a `boolean`, both guaranteed. + +Property *access* keeps compiling either way, because the index signature +resolves any property to `any`. The narrowing is `opened` and `hidden` on an +input node, which are now `boolean | undefined`. Typing a `renderNode` parameter +as `IRenderedNode` gets the guarantees back, and gets them honestly — previously +the required property and the index signature contradicted each other. + +`getChildrenName` and `keyExtractor` are now typed with `INode`, which is what +they were already being called with. They claimed an `_internalId` that was not +there. + +#### `data` is typed `readonly INode[]` + +Rather than `any`, which is the point of having an input type at all. ### Changed - **One list instead of one per node.** A 20,000-level-deep tree now mounts as few rows as a flat one; it previously mounted every node. Expanding and collapsing recomputes the row array rather than mounting and unmounting lists. + - **Nothing walks the tree on an ordinary render.** Ids were produced by hashing each node's entire subtree with `object-hash`, on a pass that depended on `data`, `extraData`, `renderNode`, `onNodePressed` and `getChildrenName`. Since `renderNode` and `onNodePressed` are inline arrows in every documented usage, the whole tree was re-hashed on every render of the parent. Only a change to `data` or `extraData` rebuilds the rows now. + - **Rows that did not move are no longer re-rendered.** A rebuild hands back the same row object where nothing about the node changed, so expanding a node re-renders the rows that appeared rather than every row on screen. + - The list is a `FlatList` rather than a bare `VirtualizedList`, which is what makes `ListComponent` interchangeable. @@ -79,33 +106,43 @@ silently: - **`ListComponent`**, the list used to render the rows. Because the rows are already flat, anything with a `FlatList`-shaped API works — `LegendList` or `FlashList`, to get their recycling. + - **`keyExtractor`**, to decide a node's identity within its parent. -- **`listProps`**, forwarded to the underlying list, which closes #451 — there - was previously no way to reach the list at all, so something as ordinary as + +- **`listProps`**, forwarded to the underlying list. This closes #451: there was + previously no way to reach the list at all, so something as ordinary as `showsVerticalScrollIndicator` was unreachable. Rather than adding a prop per option, the whole surface is now reachable. - The merge order is defined and tested: `listProps` first, then `extraData`, - `initialNumToRender` and `style` when they are given as their own props, then - `data`, `renderItem` and `keyExtractor`, which the component controls and - nothing can override. Those three are excluded from the `IListProps` type, so - passing one is a compile error rather than a silent no-op. - - An absent `extraData`, `initialNumToRender` or `style` does **not** erase a - value set through `listProps`, which the obvious implementation gets wrong. - **`IRow` and `IListProps` are exported.** `ListComponent` shipped without a way to type the rows a custom list receives, which left it unusable from TypeScript. +#### How `listProps` merges + +Defined and tested, in this order: + +| Order | What | Notes | +| ----- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| 1 | `listProps` | everything you pass | +| 2 | `extraData`, `initialNumToRender`, `style` | win over `listProps`, but **only when actually passed** | +| 3 | `data`, `renderItem`, `keyExtractor` | set by the component. Excluded from `IListProps`, so passing one is a compile error rather than a silent no-op | + +An absent `extraData`, `initialNumToRender` or `style` does **not** erase a value +set through `listProps`, which the obvious implementation gets wrong. + ### Fixed - `initialNumToRender` only ever applied to the top level; the recursive `renderChildren` call omitted it. It now applies to the whole list. + - `style` was declared on the props but never used. It is applied to the list. + - Two nodes holding the same content shared an `_internalId`, because the id was a hash of content alone. They therefore shared one expansion state and one React key: expanding either expanded both. Ids are now paths, unique by construction. + - A node kept its expanded state across a change to `data` only when its content happened to be unchanged, since that content was its React key. State is now keyed by node identity, so a node stays expanded while its own content changes. @@ -121,6 +158,7 @@ silently: ### Removed - **The `object-hash` dependency.** The library now has no runtime dependencies. + - The undocumented `node-view` and `nodes-context-provider` internals. These were already unreachable from outside the package: the `exports` map has permitted only the package root since 0.15.0. @@ -130,12 +168,14 @@ silently: - `_internalId` is now a path (`parent/child`) built from a node's own `id`, or failing that its `key`, or failing that its position, rather than a hash of the node's content. Do not persist these values across versions. + - **Top-level nodes are at `level` 1, not 0.** Inherited from the synthetic root node the old recursive renderer wrapped `data` in, and now kept on purpose rather than by accident: `NestedRow` indents by `level * paddingLeftIncrement`, so a 0 base would put top-level rows flush against the screen edge in every app built on the documented pattern. `TOP_LEVEL` is the single definition, and six tests fail if it changes. Pass `level - 1` for a flush edge. + - Expanded state now survives a change to `data` whenever a node's identity is unchanged, `keepOpenedState` or not. `keepOpenedState` still controls whether the state outlives the node leaving the tree — including while it sits inside a