deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Unannounced npm forks prompt react-comic-viewer 1.1.0 controlled-state rework

A React maintainer found three unannounced npm forks of his comic viewer component; their diffs exposed a four-year-old API gap, now fixed in version 1.1.0.

Unannounced npm forks prompt react-comic-viewer 1.1.0 controlled-state rework

A maintainer found three silent forks of his own package

The developer behind react-comic-viewer, a small React component for reading comics and manga, recently discovered three npm packages published by other people that were near-copies of his work, according to a post on dev.to written under the handle piro0919. All three reused the original description and even pointed their repository field back at his source repo. None of the authors had ever opened an issue or pull request upstream, so the derivatives existed entirely off his radar until he happened across them while browsing the registry.

The fork was ahead, and it read like a bug report

The oldest fork appeared roughly three months after the original package was first published and was kept going for almost a year, the maintainer writes. At one point its version number ran well ahead of the original, 0.6.3 against 0.3.5. Reading the diff, he found the fork's commit history told a clearer story than most issue trackers: it dropped Sass from the build, added support for a className prop, introduced hotkey handling, and included a new example file demonstrating controlled usage of the component.

Two of those problems had since been fixed upstream, the className gap about a year after the fork addressed it. The third had not. After four years, the component still offered no controlled mode at all.

The four-year-old design gap

The fork's example drove the component through props like currentPage, isExpansion and page-change callbacks, an API the real package never had. Upstream, the equivalent props were initialCurrentPage and initialIsExpansion: the caller supplied a starting page once, and the component owned that state for the rest of its life.

That is fine for a demo and poor for real applications, the maintainer explains. With no way for a parent to set the page, a reader cannot jump to a location from a table of contents, the current page cannot be kept in sync with the URL, and there is no way to intercept a page turn for a chapter that must be purchased first. Every one of those features requires the parent component to be in charge, and it never was.

What version 1.1.0 changes

The fix, shipped in version 1.1.0, follows the standard controlled/uncontrolled pattern. A new useControllableState hook keeps internal state that is bypassed whenever the caller supplies a value. Two implementation details matter: refs keep the setter stable even when parents pass inline arrow functions, and the onChange call lives inside the setter rather than in an effect, because in controlled mode the local value never changes and an effect watching it would never fire.

Two internal behaviors complicated the migration. Entering fullscreen forced the expanded view, and switching to a two-page spread rounded the current page down to an even number. In controlled mode both now go through onChange, so if the parent ignores the request, nothing moves — the same semantics as a native controlled input.

The release adds currentPage and isExpansion as optional, independent props: supply one and you own it, leave both out and the old behavior is untouched. It also adds onTryMoveNextPage and onTryMovePrevPage, which fire before a page changes, and lets a pages entry be a function that receives the class name the viewer would have applied to its own image element, so callers can substitute their own markup for lazy loading. A live demo accompanies the release.

The ecosystem problem the story illustrates

Beyond a single component, the episode shows how poorly package registries surface derivative work. Because the fork authors never contacted the maintainer, years of signal — working fixes for genuine problems — sat in packages the original author did not know existed, and he found them only by accident. Anyone searching npm for the component could just as easily have installed a fork, since the copies carried the same description and repository URL with no indication that the codebases had diverged. Registry names are claimed independently, so near-identical packages can coexist indefinitely with nothing tying them together and no notification reaching the original publisher.

Why it matters

Unannounced forks cut both ways in open source. For maintainers, they are unusually honest bug reports: someone hit a wall, solved it properly and published the result, and the commit messages document exactly what was missing. For users, they are a supply-chain hazard — similar names, identical metadata, different code, and no channel telling anyone the lines have split. For ecosystems, the story is a reminder that the path of least resistance runs through forking, not filing issues, and the burden of noticing lands on whoever holds the original name. The maintainer's own takeaway is not blame toward the fork authors, whom he says he would probably have imitated, but regret at the delay: he wishes he had looked sooner.

  • #npm
  • #react
  • #open-source
  • #supply-chain
  • #javascript

Related posts