First stable release. Semver rule 5: version 1.0.0 defines the public API —
and this is that API. The library has been in production since 2018 on a 0.x
that, per rule 4, claimed "anything MAY change at any time". It no longer
claims that.
The headline is a rewritten core. The recursive nested-VirtualizedList
renderer is replaced by a flattened row model (#469): node-view rendered a
VirtualizedList per node, recursively, so an N-level tree nested N lists of
the same orientation inside one another — the arrangement React Native warns
against. The tree is now flattened into a single array of visible rows rendered
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.
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
Nothing in the documented usage changes. data, renderNode,
getChildrenName, onNodePressed, extraData, keepOpenedState and
initialNumToRender behave as they did, and the default export, NestedRow and
INode are still what you import.
Two things may need attention, and both surface as type errors rather than
silently.
A node parameter annotated INode that reads opened or hidden expecting
a boolean wants the other type now:
// before
const renderNode = (node: INode) => <Text>{node.opened ? '▾' : '▸'}</Text>
// after
const renderNode = (node: IRenderedNode) => <Text>{node.opened ? '▾' : '▸'}</Text>Persisted _internalId values. They are paths now rather than content
hashes, so anything stored by a previous version will no longer match.
Breaking
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:
const data: INode[] = [{ title: 'Node 1' }]; // did not compile in 0.15.0opened 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 withobject-hash, on a pass that depended on
data,extraData,renderNode,onNodePressedandgetChildrenName.
SincerenderNodeandonNodePressedare inline arrows in every documented
usage, the whole tree was re-hashed on every render of the parent. Only a
change todataorextraDatarebuilds 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
FlatListrather than a bareVirtualizedList, which is what
makesListComponentinterchangeable.
Added
-
ListComponent, the list used to render the rows. Because the rows are
already flat, anything with aFlatList-shaped API works —LegendListor
FlashList, to get their recycling. -
keyExtractor, to decide a node's identity within its parent. -
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
showsVerticalScrollIndicatorwas unreachable. Rather than adding a prop per
option, the whole surface is now reachable. -
IRowandIListPropsare exported.ListComponentshipped 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
-
initialNumToRenderonly ever applied to the top level; the recursive
renderChildrencall omitted it. It now applies to the whole list. -
stylewas 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
dataonly 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. -
The
NestedRowprops table documented a defaultheightof 50, which has
never existed — the code appliesheight ? {height} : {}, so a row with no
heightsizes to its content. It also documentedlevelas required when it
defaults to0, markedchildrenrequired when it is optional, and omitted
paddingLeftIncremententirely. The defaults are now documented as they are
and covered by tests.heightdeliberately keeps having no default: adding one
would resize every row in every app that omits it.
Removed
-
The
object-hashdependency. The library now has no runtime dependencies. -
The undocumented
node-viewandnodes-context-providerinternals. These were
already unreachable from outside the package: theexportsmap has permitted
only the package root since 0.15.0.
Behaviour worth knowing about
-
_internalIdis now a path (parent/child) built from a node's ownid, or
failing that itskey, 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
level1, not 0. Inherited from the synthetic root
node the old recursive renderer wrappeddatain, and now kept on purpose
rather than by accident:NestedRowindents bylevel * paddingLeftIncrement,
so a 0 base would put top-level rows flush against the screen edge in every app
built on the documented pattern.TOP_LEVELis the single definition, and six
tests fail if it changes. Passlevel - 1for a flush edge. -
Expanded state now survives a change to
datawhenever a node's identity is
unchanged,keepOpenedStateor not.keepOpenedStatestill controls whether
the state outlives the node leaving the tree — including while it sits inside a
collapsed parent, which is when the recursive renderer used to discard it.
Full changelog: v0.15.0...v1.0.0