<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: piro</title>
    <description>The latest articles on DEV Community by piro (@piro0919).</description>
    <link>https://dev.to/piro0919</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4090269%2Fb63ae8d1-4f6f-42ec-9b0a-63c897eea705.png</url>
      <title>DEV Community: piro</title>
      <link>https://dev.to/piro0919</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/piro0919"/>
    <language>en</language>
    <item>
      <title>Someone forked my React component instead of opening an issue</title>
      <dc:creator>piro</dc:creator>
      <pubDate>Sun, 23 Aug 2026 00:18:36 +0000</pubDate>
      <link>https://dev.to/piro0919/someone-forked-my-react-component-instead-of-opening-an-issue-3d37</link>
      <guid>https://dev.to/piro0919/someone-forked-my-react-component-instead-of-opening-an-issue-3d37</guid>
      <description>&lt;p&gt;I maintain a small comic and manga viewer component for React called &lt;a href="https://github.com/piro0919/react-comic-viewer" rel="noopener noreferrer"&gt;react-comic-viewer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The other day I was poking around npm and noticed something odd — there were three other packages with basically my package's name, published by other people.&lt;/p&gt;

&lt;p&gt;All three were forks of mine. Same description, same repository URL pointing back at my repo.&lt;/p&gt;

&lt;p&gt;None of the three authors had ever opened an issue or a pull request on my side.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the fork changed
&lt;/h2&gt;

&lt;p&gt;The oldest fork was made about three months after I first published, and it kept going for almost a year. Its version number ran ahead of mine at the time — I was on 0.3.5 while the fork was on 0.6.3.&lt;/p&gt;

&lt;p&gt;So I read the diff. Honestly, it was more useful than any issue would have been.&lt;/p&gt;

&lt;p&gt;The commit messages alone told the whole story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;remove sass&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fix: support className props&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;use Hotkeys&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;and a new example file called &lt;code&gt;controlled.tsx&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sass one I'd already fixed. The className one I'd fixed too, about a year later.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;controlled.tsx&lt;/code&gt; one I had never fixed. Not in four years.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part I never fixed
&lt;/h2&gt;

&lt;p&gt;Here's what their example looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ComicViewer&lt;/span&gt;
  &lt;span class="na"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;isExpansion&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onTryMoveNextPage&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextPage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onChangedCurrentPage&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setCurrentPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's what my component actually accepted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ComicViewer&lt;/span&gt;
  &lt;span class="na"&gt;initialCurrentPage&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;initialIsExpansion&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onChangeCurrentPage&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;initial&lt;/code&gt; prefix is the whole problem.&lt;/p&gt;

&lt;p&gt;My component would take a starting page from you, and then never let you touch it again. It owned that state for the rest of its life.&lt;/p&gt;

&lt;p&gt;That's fine for a demo. It's pretty bad for anything real.&lt;/p&gt;

&lt;p&gt;You can't jump to a page from a table of contents. Syncing the current page with the URL doesn't work either. And if a chapter needs to be purchased first, there's no way to step in and stop the move.&lt;/p&gt;

&lt;p&gt;Every one of those needs the parent to be in charge, and the parent never was.&lt;/p&gt;




&lt;h2&gt;
  
  
  Making it controllable
&lt;/h2&gt;

&lt;p&gt;This is the standard controlled/uncontrolled pattern, and there's nothing clever about it — but it took me an embarrassingly long time to get around to it, so here's the shape I ended up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useControllableState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;controlledValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;uncontrolledValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUncontrolledValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isControlled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;controlledValue&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isControlled&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;controlledValue&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;uncontrolledValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;valueRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;valueRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onChangeRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;onChangeRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="k"&gt;as &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;valueRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolved&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;valueRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isControlled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setUncontrolledValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;onChangeRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isControlled&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth pointing out.&lt;/p&gt;

&lt;p&gt;The refs are there so the setter stays stable even when you pass an inline arrow function as &lt;code&gt;onChange&lt;/code&gt;. Without them, every render hands you a new setter, and everything downstream re-renders with it.&lt;/p&gt;

&lt;p&gt;And the &lt;code&gt;onChange&lt;/code&gt; call lives inside the setter, not in a &lt;code&gt;useEffect&lt;/code&gt; watching the value. That matters — in controlled mode the value doesn't change locally at all, so an effect watching it would simply never fire, and the parent would hear nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two spots that got awkward
&lt;/h2&gt;

&lt;p&gt;Two places inside the component wrote to that state on their own.&lt;/p&gt;

&lt;p&gt;Going fullscreen forces the expanded view, and restores the old value on exit. And switching to a two-page spread rounds the current page down to an even number.&lt;/p&gt;

&lt;p&gt;In controlled mode, neither of those can just happen anymore. The parent owns the value.&lt;/p&gt;

&lt;p&gt;I settled on the boring answer: the component asks, through &lt;code&gt;onChange&lt;/code&gt;, and if the parent ignores it, nothing moves. That's how a controlled input behaves too — type into one without wiring up &lt;code&gt;onChange&lt;/code&gt; and the text just doesn't appear.&lt;/p&gt;

&lt;p&gt;Slightly surprising the first time, but it's the honest behavior, and it's what everyone else already does.&lt;/p&gt;




&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;1.1.0&lt;/code&gt; adds &lt;code&gt;currentPage&lt;/code&gt; and &lt;code&gt;isExpansion&lt;/code&gt; as optional props. Pass either one and you own it; leave it out and the old behavior is untouched. Each is independent, so you can control the page and let the component keep the expansion state.&lt;/p&gt;

&lt;p&gt;It also adds &lt;code&gt;onTryMoveNextPage&lt;/code&gt; and &lt;code&gt;onTryMovePrevPage&lt;/code&gt;, which fire before the page changes, and lets an entry of &lt;code&gt;pages&lt;/code&gt; be a function receiving the class name the viewer would have applied to its own &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; — so you can bring your own element for lazy loading.&lt;/p&gt;

&lt;p&gt;There's a demo at &lt;a href="https://react-comic-viewer.kkweb.io" rel="noopener noreferrer"&gt;react-comic-viewer.kkweb.io&lt;/a&gt; if you want to poke at it.&lt;/p&gt;




&lt;p&gt;The thing I keep coming back to isn't the code, though.&lt;/p&gt;

&lt;p&gt;Somebody hit a wall in my library, solved it properly, published the fix under their own name, and I found out four years later by accident.&lt;/p&gt;

&lt;p&gt;That's not on them. Forking is cheaper than filing an issue and waiting, and I'd probably do the same.&lt;/p&gt;

&lt;p&gt;I just wish I'd looked sooner. Four years is a long time to leave something sitting there.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>opensource</category>
      <category>javascript</category>
      <category>react</category>
    </item>
  </channel>
</rss>
