<?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: Nicholas Petrasek</title>
    <description>The latest articles on DEV Community by Nicholas Petrasek (@cylop).</description>
    <link>https://dev.to/cylop</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%2F1313441%2F31bb3e91-1e8f-45b4-9c62-27fa58cc5812.jpeg</url>
      <title>DEV Community: Nicholas Petrasek</title>
      <link>https://dev.to/cylop</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cylop"/>
    <language>en</language>
    <item>
      <title>The request timed out. Did the server save the change?</title>
      <dc:creator>Nicholas Petrasek</dc:creator>
      <pubDate>Thu, 22 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/cylop/the-request-timed-out-did-the-server-save-the-change-2nni</link>
      <guid>https://dev.to/cylop/the-request-timed-out-did-the-server-save-the-change-2nni</guid>
      <description>&lt;p&gt;The user clicks Save. A spinner runs for a while, then the request times out.&lt;/p&gt;

&lt;p&gt;It is tempting to show “Save failed” and a Retry button. For a write, that may leave out the hardest part: the browser does not necessarily know whether the operation was applied.&lt;/p&gt;

&lt;p&gt;This is a design walkthrough for a hypothetical admin action. The same question appears in imports, saved items, and other operations that change server state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The response is evidence about an operation
&lt;/h2&gt;

&lt;p&gt;Consider three possible sequences:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server outcome&lt;/th&gt;
&lt;th&gt;What the browser receives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The request never reaches the handler&lt;/td&gt;
&lt;td&gt;No successful response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The handler rejects the operation&lt;/td&gt;
&lt;td&gt;A rejection, if the response arrives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The server commits, then the response is lost&lt;/td&gt;
&lt;td&gt;No successful response&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first and third can look the same to the client. A transport failure is therefore not enough evidence to claim that nothing changed.&lt;/p&gt;

&lt;p&gt;I would represent at least these outcomes in the UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confirmed success:&lt;/strong&gt; the operation was applied.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirmed rejection:&lt;/strong&gt; the operation was not accepted under the server's contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown outcome:&lt;/strong&gt; the client cannot yet establish what happened.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third state needs an explanation and a recovery path. It should not pretend to be either of the first two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the attempted operation identifiable
&lt;/h2&gt;

&lt;p&gt;For an operation that must avoid duplicate application, one possible contract uses a client-generated operation identifier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;operation: op-123
action: import the reviewed file
payload: the approved plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server associates the identifier with the intended payload and result. A retry using the same identifier can refer to the same logical operation rather than create a new one.&lt;/p&gt;

&lt;p&gt;That is a proposed application contract, not behavior supplied automatically by a custom request header. It needs implementation: concurrent retries, payload mismatches, authorization, retention, and failure recovery all matter.&lt;/p&gt;

&lt;p&gt;For example, accepting the same identifier with different import contents would make it difficult to know what was retried. The server should define what it accepts and how it reports a mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recover the outcome
&lt;/h2&gt;

&lt;p&gt;A status endpoint can let the browser ask about &lt;code&gt;op-123&lt;/code&gt;. A fresh read can also work when the application can reliably identify the attempted change. Merely seeing a similar record may be insufficient when concurrent users can create similar data.&lt;/p&gt;

&lt;p&gt;During reconciliation, a message such as “We couldn't confirm the import. Checking its status…” describes the available evidence.&lt;/p&gt;

&lt;p&gt;If the server is still working, show that state. If it committed, show the result. If the outcome remains unknown, explain which action is safe instead of inventing a result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries need a defined effect
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.2" rel="noopener noreferrer"&gt;HTTP semantics&lt;/a&gt; defines idempotent methods in terms of their intended server effect when identical requests are repeated. That helps when deciding what a client can safely retry, but an operation's application-level behavior still needs attention.&lt;/p&gt;

&lt;p&gt;A retried “create another import” command and a retry of one identified import do not express the same intent.&lt;/p&gt;

&lt;p&gt;I would choose the retry policy at the same time as the endpoint contract, rather than bolt it onto a generic error handler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cancel has its own meaning
&lt;/h2&gt;

&lt;p&gt;Closing a dialog or aborting a fetch stops part of the client interaction. It does not establish that a server-side write was reversed.&lt;/p&gt;

&lt;p&gt;A long-running operation may support a cancellation request. That request also has an outcome: the job might stop before applying anything, stop after partial work, or finish before cancellation is accepted.&lt;/p&gt;

&lt;p&gt;The UI needs to describe the actual result. “Cancellation requested” is a more limited statement than “Nothing was imported.”&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure test worth running
&lt;/h2&gt;

&lt;p&gt;Let the server apply the operation, then drop the response. Reload the page and retry the same operation from another tab.&lt;/p&gt;

&lt;p&gt;Check whether the application can recover one authoritative result, whether the user understands the state, and whether the retry creates duplicate work.&lt;/p&gt;

&lt;p&gt;This is where a reliable workflow separates itself from a demo that only handles a successful response. What does your application show when the write may have succeeded but the browser cannot prove it yet?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>backend</category>
    </item>
    <item>
      <title>Your import preview is a proposed change, not just a parsed CSV</title>
      <dc:creator>Nicholas Petrasek</dc:creator>
      <pubDate>Mon, 19 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/cylop/your-import-preview-is-a-proposed-change-not-just-a-parsed-csv-5166</link>
      <guid>https://dev.to/cylop/your-import-preview-is-a-proposed-change-not-just-a-parsed-csv-5166</guid>
      <description>&lt;p&gt;An import screen can parse a file successfully, show a convincing table, and still leave the user unsure what clicking Import will do.&lt;/p&gt;

&lt;p&gt;Consider a preview that says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;80 records will be created
20 records will be updated
3 rows need attention
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is useful only if the user can understand the matching rule, inspect the changes, and trust that the commit will follow the same contract.&lt;/p&gt;

&lt;p&gt;This is a design approach I would use for an admin import flow. The example is hypothetical; it is not a claim about a particular product's implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parse into a plan
&lt;/h2&gt;

&lt;p&gt;Parsing tells you what the file contains. Planning tells you what those values mean for the application.&lt;/p&gt;

&lt;p&gt;A plan might contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceRow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recordId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expectedVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"before"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Ltd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"after"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Limited"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shape is illustrative, not an API specification. Its purpose is to make the decision visible: which row matched which record, what would change, and which record version the plan used.&lt;/p&gt;

&lt;p&gt;I would also show rejected and ambiguous rows. A file row that matches multiple records should not disappear into a generic error count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide what can become stale
&lt;/h2&gt;

&lt;p&gt;Between preview and commit, another user may edit a record or create a new one that matches an imported identifier.&lt;/p&gt;

&lt;p&gt;There are several reasonable policies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reject stale plans and ask for a fresh preview.&lt;/li&gt;
&lt;li&gt;Revalidate at commit and show conflicts for review.&lt;/li&gt;
&lt;li&gt;Apply unaffected rows while reporting conflicting rows separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The choice depends on the product. What I would avoid is silently changing the matching rule or overwriting newer values while continuing to imply that the original preview was applied.&lt;/p&gt;

&lt;p&gt;The server needs to enforce the chosen rule. If a version check happens before the write without an atomic guard, another change can still occur between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define what a retry means
&lt;/h2&gt;

&lt;p&gt;Suppose the server imports the rows, but the response never reaches the browser. The user clicks again.&lt;/p&gt;

&lt;p&gt;Is that a retry of the same approved operation, or a new import?&lt;/p&gt;

&lt;p&gt;An operation identifier can help the server recognize retries, but it needs a real contract: where the identifier is stored, how long it remains valid, and whether a different payload with the same identifier is rejected.&lt;/p&gt;

&lt;p&gt;The button label alone cannot solve duplicate writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make partial results actionable
&lt;/h2&gt;

&lt;p&gt;If partial application is allowed, I want the result to explain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which rows were applied.&lt;/li&gt;
&lt;li&gt;Which were skipped or rejected, and why.&lt;/li&gt;
&lt;li&gt;Whether retrying the remaining rows can repeat successful writes.&lt;/li&gt;
&lt;li&gt;Where to inspect the resulting records.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep row numbers tied to the original file. Users should not have to reconstruct which line an error refers to after sorting the preview.&lt;/p&gt;

&lt;p&gt;An undo feature can be valuable, but reversing an import may conflict with edits made afterward. “Undo” needs its own policy for intervening changes; it is not automatically equivalent to restoring a database snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The review I would run
&lt;/h2&gt;

&lt;p&gt;Before calling the flow ready, I would test a duplicate identifier, an ambiguous match, a record changed after preview, a lost success response, and a retry of a partially applied import.&lt;/p&gt;

&lt;p&gt;Those cases connect backend correctness with the UI's promise. A clean preview table is only one part of that promise.&lt;/p&gt;

&lt;p&gt;If you build admin tools, which import behavior do users ask for first: better matching, a clearer diff, useful errors, or safer retries?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>database</category>
    </item>
    <item>
      <title>Optimistic UI: the rollback can be older than the user's intent</title>
      <dc:creator>Nicholas Petrasek</dc:creator>
      <pubDate>Thu, 15 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/cylop/optimistic-ui-the-rollback-can-be-older-than-the-users-intent-ndo</link>
      <guid>https://dev.to/cylop/optimistic-ui-the-rollback-can-be-older-than-the-users-intent-ndo</guid>
      <description>&lt;p&gt;A favourite button is a useful place to start thinking about optimistic UI. Change the icon immediately, send the update, and restore the old value if the request fails.&lt;/p&gt;

&lt;p&gt;That works for a single operation. Repeated input makes the contract more interesting.&lt;/p&gt;

&lt;p&gt;Imagine this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The item starts unfavourited.&lt;/li&gt;
&lt;li&gt;The user favourites it. Request A starts.&lt;/li&gt;
&lt;li&gt;The user removes the favourite. Request B starts.&lt;/li&gt;
&lt;li&gt;The user favourites it again. Request C starts.&lt;/li&gt;
&lt;li&gt;B and C succeed.&lt;/li&gt;
&lt;li&gt;A fails, and its error handler restores the original unfavourited state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The old snapshot now disagrees with the latest choice. A stale rollback has overwritten a newer successful action. The broader problem is applying an operation's historical state to the current interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model three separate things
&lt;/h2&gt;

&lt;p&gt;I find it useful to distinguish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confirmed state:&lt;/strong&gt; the latest authoritative state the client knows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desired state:&lt;/strong&gt; what the user currently wants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pending operations:&lt;/strong&gt; the requests attempting to reconcile them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An operation's failure should not automatically become a command to restore the whole interface. First ask whether the operation still represents the user's current intent.&lt;/p&gt;

&lt;p&gt;Here is a tiny, runnable example of the client-side relevance check. It deliberately models one control; it is not a complete network implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;latestOperation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;desired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextValue&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="nx"&gt;operation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;latestOperation&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;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;desired&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;desired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nextValue&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="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;()&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;operation&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;latestOperation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;desired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;previous&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="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="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;desired&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// true: an old failure did not replace newer intent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The check prevents an obsolete failure from changing &lt;code&gt;desired&lt;/code&gt;. It does &lt;strong&gt;not&lt;/strong&gt; prove the server has the right value. It also does not solve what to do if several operations fail: restoring an earlier desired value may restore a value that was never confirmed.&lt;/p&gt;

&lt;p&gt;That limitation is why I would not paste this into a production favourite button and call the problem solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose a concurrency policy
&lt;/h2&gt;

&lt;p&gt;For one item, serializing changes can make the contract simpler. An app can disable input until the request finishes, or keep collecting the latest desired state while allowing only one request at a time.&lt;/p&gt;

&lt;p&gt;If it keeps collecting intent, the coordinator can compare the new confirmed value with the latest desired value after each response and decide whether another request is needed. A failed request still needs a defined retry or error policy.&lt;/p&gt;

&lt;p&gt;If requests are allowed to overlap, the API needs a rule too. Sending “set favourite to true” communicates intent more clearly than “toggle,” but absolute values alone do not prevent older writes from arriving last. Versions, conditional writes, or server-side sequencing may be needed.&lt;/p&gt;

&lt;p&gt;This is not specific to React. A hook, mutation library, or store can manage the mechanics, but the product still needs to choose what repeated input means.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the inconvenient order
&lt;/h2&gt;

&lt;p&gt;I would include at least these scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An earlier request fails after a later request succeeds.&lt;/li&gt;
&lt;li&gt;Responses arrive in the opposite order from the clicks.&lt;/li&gt;
&lt;li&gt;Every pending request fails.&lt;/li&gt;
&lt;li&gt;The response is lost after the server commits the write.&lt;/li&gt;
&lt;li&gt;Another tab changes the same item.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check the displayed state and the persisted state separately. A correct icon can mask an incorrect server value, and the next reload will expose it.&lt;/p&gt;

&lt;p&gt;The right policy depends on the action. Saving a marketplace item may justify quick repeated interaction. Approving a business record may justify blocking input and requiring a fresh review after a conflict.&lt;/p&gt;

&lt;p&gt;My preference is to start with the simplest explicit policy that fits the action, then add concurrency only when it improves the experience enough to justify the extra states.&lt;/p&gt;

&lt;p&gt;How do you handle repeated optimistic actions on the same item: block, serialize, coalesce, or overlap?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Positioning autocomplete at the caret takes two kinds of updates</title>
      <dc:creator>Nicholas Petrasek</dc:creator>
      <pubDate>Mon, 12 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/cylop/positioning-autocomplete-at-the-caret-takes-two-kinds-of-updates-47je</link>
      <guid>https://dev.to/cylop/positioning-autocomplete-at-the-caret-takes-two-kinds-of-updates-47je</guid>
      <description>&lt;p&gt;A textarea's bounding box is easy to measure. An autocomplete menu usually needs a different anchor: the insertion point inside it.&lt;/p&gt;

&lt;p&gt;Those positions diverge as soon as text wraps or the textarea scrolls. A menu anchored to the bottom of the element can be nowhere near the word the user is editing.&lt;/p&gt;

&lt;p&gt;I maintain &lt;a href="https://github.com/NIPE-Solutions/caret-geometry" rel="noopener noreferrer"&gt;Caret Geometry&lt;/a&gt;, an alpha-stage browser primitive for measuring that anchor. Here is how its boundary fits into a popup implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the coordinate you actually need
&lt;/h2&gt;

&lt;p&gt;The smallest call looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getCaretRect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@nipe-solutions/caret-geometry&lt;/span&gt;&lt;span class="dl"&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;rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getCaretRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textarea&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;rect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&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;The rectangle uses viewport-relative CSS pixels in the target's browsing context, matching the coordinate space of &lt;code&gt;getBoundingClientRect()&lt;/code&gt;. It is not a document offset or a device-pixel measurement.&lt;/p&gt;

&lt;p&gt;For supported targets, a missing current layout or relevant selection can produce &lt;code&gt;null&lt;/code&gt;. Unsupported input types throw an error. Password inputs are deliberately unsupported and are rejected before their value is mirrored.&lt;/p&gt;

&lt;p&gt;A caret rectangle answers where the anchor is. Choosing which side of it a popup should occupy is another job.&lt;/p&gt;

&lt;h2&gt;
  
  
  There are two reasons to reposition
&lt;/h2&gt;

&lt;p&gt;A user can move the caret while the textarea stays completely still. Arrow keys, a selection change, composition and internal scrolling can all matter.&lt;/p&gt;

&lt;p&gt;The reverse happens too: the caret stays at the same text position while a parent scrolls, the page resizes or the surrounding layout shifts.&lt;/p&gt;

&lt;p&gt;The integration needs to respond to both:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What changed?&lt;/th&gt;
&lt;th&gt;Who observes it?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Caret, selection, composition or internal textarea scroll&lt;/td&gt;
&lt;td&gt;&lt;code&gt;observeCaretGeometry&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surrounding scroll, resize or layout changes&lt;/td&gt;
&lt;td&gt;The positioning engine's update mechanism&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both trigger the same positioning function. Attaching a listener only to typing misses part of the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect a virtual caret to Floating UI
&lt;/h2&gt;

&lt;p&gt;This example follows the &lt;a href="https://github.com/NIPE-Solutions/caret-geometry/blob/main/examples/floating-ui.ts" rel="noopener noreferrer"&gt;compiling integration example in the repository&lt;/a&gt;. The popup should already be mounted and styled with &lt;code&gt;position: fixed&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;createCaretVirtualElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;observeCaretGeometry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@nipe-solutions/caret-geometry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;autoUpdate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;computePosition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;flip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@floating-ui/dom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;connectCaretPopup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLTextAreaElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;popup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&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="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="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;caret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCaretVirtualElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textarea&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;caret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;updatePosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &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="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;caret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCaretRect&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;popup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;popup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;computePosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;popup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fixed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;placement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bottom-start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;flip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;})],&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;popup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&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="nx"&gt;caretObserver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;observeCaretGeometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;updatePosition&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="nx"&gt;stopLayout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;autoUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;popup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updatePosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;updatePosition&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;caretObserver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;stopLayout&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the returned cleanup function and call it when that connection ends, before removing the elements. The &lt;a href="https://caret-geometry.nipesolutions.com/integrations/floating-ui.html" rel="noopener noreferrer"&gt;integration guide&lt;/a&gt; explains the boundary in more detail.&lt;/p&gt;

&lt;p&gt;Caret Geometry supplies the virtual reference and caret updates. Floating UI handles the popup's placement, including the offset, flip and shift choices above.&lt;/p&gt;

&lt;p&gt;Your application still owns when suggestions open, which results to show, keyboard selection, focus and accessibility semantics. A measured caret does not implement a combobox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check more than typing at the end of a line
&lt;/h2&gt;

&lt;p&gt;Before relying on a popup, I would try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Long wrapped text and internal textarea scrolling.&lt;/li&gt;
&lt;li&gt;Moving the caret with the keyboard without typing.&lt;/li&gt;
&lt;li&gt;A backward selection. The default endpoint is its focus; in a backward input selection, that means &lt;code&gt;selectionStart&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;IME composition in the browsers you support.&lt;/li&gt;
&lt;li&gt;Scrolling an ancestor and resizing the page with the popup open.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you pass an explicit text-control position, it uses a UTF-16 string index. That matters when your application computes positions around emoji or other text represented by multiple code units.&lt;/p&gt;

&lt;p&gt;This library is still alpha. The &lt;a href="https://github.com/NIPE-Solutions/caret-geometry/blob/main/docs/compatibility.md" rel="noopener noreferrer"&gt;compatibility documentation&lt;/a&gt; and &lt;a href="https://github.com/NIPE-Solutions/caret-geometry/blob/main/docs/release-readiness.md" rel="noopener noreferrer"&gt;release-readiness notes&lt;/a&gt; describe its current scope and remaining gates.&lt;/p&gt;

&lt;p&gt;I'm looking for real integrations, especially autocomplete, mention menus and editor helpers. If the anchor drifts in your layout, a &lt;a href="https://github.com/NIPE-Solutions/caret-geometry/issues" rel="noopener noreferrer"&gt;small reproduction with the browser and target type&lt;/a&gt; is useful feedback.&lt;/p&gt;

&lt;p&gt;What is the most awkward caret-positioning case in your editor?&lt;/p&gt;




&lt;p&gt;Caret Geometry is part of my &lt;a href="https://opensource.nipesolutions.com" rel="noopener noreferrer"&gt;NIPE open-source frontend libraries&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Exposing live SDK state without handing callers a mutable object</title>
      <dc:creator>Nicholas Petrasek</dc:creator>
      <pubDate>Thu, 08 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/cylop/exposing-live-sdk-state-without-handing-callers-a-mutable-object-k7p</link>
      <guid>https://dev.to/cylop/exposing-live-sdk-state-without-handing-callers-a-mutable-object-k7p</guid>
      <description>&lt;p&gt;An SDK can own a mutable state object and still need to expose it to consumers. The difficult part is deciding what a consumer actually receives.&lt;/p&gt;

&lt;p&gt;A copy can become stale. Freezing the SDK's original state prevents the owner from changing it. Returning the original object with a readonly TypeScript type relies on callers respecting a compile-time contract.&lt;/p&gt;

&lt;p&gt;For one particular use case, I want a live view: callers can read supported nested values, the SDK can keep updating its source, and writes through that view are rejected at runtime.&lt;/p&gt;

&lt;p&gt;The examples here follow the 2.0 alpha API documented in the repository.&lt;/p&gt;

&lt;p&gt;That is the boundary I built &lt;a href="https://github.com/NIPE-Solutions/readonly-view" rel="noopener noreferrer"&gt;ReadonlyView&lt;/a&gt; for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The outer object is only the outer object
&lt;/h2&gt;

&lt;p&gt;Here is a small example of what Object.freeze protects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alice&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFrozen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;      &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFrozen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// false&lt;/span&gt;

&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bob&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Bob&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is expected behavior: freezing is shallow. Freezing every reachable supported object would require a deeper operation, and it would still prevent the owner from updating that same graph.&lt;/p&gt;

&lt;p&gt;That can be exactly what you want for an immutable snapshot. It is awkward when the SDK needs to retain an internally mutable source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the public view from the owner's reference
&lt;/h2&gt;

&lt;p&gt;ReadonlyView wraps the source and returns a deeply readonly view for supported values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;readonlyView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;DirectMutationError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@nipe-solutions/readonly-view&lt;/span&gt;&lt;span class="dl"&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;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;editor&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="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readonlyView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bob&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Bob&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Carol&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="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;DirectMutationError&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Bob&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example uses JavaScript so you can see the runtime rejection. In TypeScript, the returned type also makes that assignment a compile-time error.&lt;/p&gt;

&lt;p&gt;The original source stays mutable for the owner. A write through the public view throws. Owner-side changes remain visible through that same view.&lt;/p&gt;

&lt;p&gt;You could use this boundary for SDK state, a registry or a plugin context, provided the data fits the supported-value contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  A graph doesn't need to be copied up front
&lt;/h2&gt;

&lt;p&gt;The library creates nested proxies on access and reuses them. Creating the initial view doesn't eagerly walk the entire object graph.&lt;/p&gt;

&lt;p&gt;Shared references and cycles preserve identity within one view. For example, two source paths pointing to the same object still point to the same wrapped object through that view.&lt;/p&gt;

&lt;p&gt;Lazy wrapping avoids an initial deep traversal; proxy reads still have overhead. I don't want to turn that design choice into a blanket performance claim. The &lt;a href="https://github.com/NIPE-Solutions/readonly-view/tree/main/benchmarks" rel="noopener noreferrer"&gt;benchmark methodology&lt;/a&gt; explains how performance is assessed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The owner still has responsibilities
&lt;/h2&gt;

&lt;p&gt;A readonly view doesn't revoke other mutable references. If the owner hands out the original object elsewhere, that alias can still change it.&lt;/p&gt;

&lt;p&gt;The supported-type boundary matters too. Plain objects, arrays, Maps, Sets and Dates have documented support. Some built-ins, including mutable native buffers and weak collections, are rejected. Functions and custom classes have receiver semantics worth reading before adoption.&lt;/p&gt;

&lt;p&gt;Use the &lt;a href="https://github.com/NIPE-Solutions/readonly-view/blob/main/docs/supported-types.md" rel="noopener noreferrer"&gt;support matrix&lt;/a&gt; and &lt;a href="https://github.com/NIPE-Solutions/readonly-view/blob/main/docs/guarantees.md" rel="noopener noreferrer"&gt;guarantees&lt;/a&gt; to check the values your API exposes. This is a runtime readonly boundary, not a security sandbox for untrusted code.&lt;/p&gt;

&lt;p&gt;It also doesn't manage application state or subscribe React to updates. A live backing value and a UI rerender are separate concerns.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small adoption experiment
&lt;/h2&gt;

&lt;p&gt;The package is &lt;a href="https://github.com/NIPE-Solutions/readonly-view" rel="noopener noreferrer"&gt;@nipe-solutions/readonly-view&lt;/a&gt;. Start with one public state object whose mutable owner you can identify.&lt;/p&gt;

&lt;p&gt;Then check three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Owner-side updates appear through the view.&lt;/li&gt;
&lt;li&gt;Writes through supported nested paths are rejected.&lt;/li&gt;
&lt;li&gt;Consumers don't receive an unintended mutable alias somewhere else.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is a useful trial without replacing your state model.&lt;/p&gt;

&lt;p&gt;I maintain this library and would appreciate reports from real SDKs, registries or plugin interfaces. A small example of an unsupported value, unexpected receiver behavior or confusing readonly boundary would be especially helpful in &lt;a href="https://github.com/NIPE-Solutions/readonly-view/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When you expose internal state, do you need a snapshot, a live readonly view, or explicit getter methods?&lt;/p&gt;




&lt;p&gt;ReadonlyView is part of my &lt;a href="https://opensource.nipesolutions.com" rel="noopener noreferrer"&gt;NIPE open-source frontend libraries&lt;/a&gt;. I'm looking for real usage and concrete feedback to improve them.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What JSON.stringify hides in a React debug panel</title>
      <dc:creator>Nicholas Petrasek</dc:creator>
      <pubDate>Tue, 06 Oct 2026 06:37:02 +0000</pubDate>
      <link>https://dev.to/cylop/what-jsonstringify-hides-in-a-react-debug-panel-19bb</link>
      <guid>https://dev.to/cylop/what-jsonstringify-hides-in-a-react-debug-panel-19bb</guid>
      <description>&lt;p&gt;A quick React debug panel often starts here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;pre&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;For plain JSON data, that's a reasonable place to start. Then a Map appears as &lt;code&gt;{}&lt;/code&gt;, and the panel has quietly stopped showing the thing you were trying to debug.&lt;/p&gt;

&lt;p&gt;Here's a small example you can run in a JavaScript console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ada&lt;/span&gt;&lt;span class="dl"&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;]]),&lt;/span&gt;
  &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;editor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-10-06T08:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"updatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-06T08:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"primary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ada"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"selected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ada"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Map entries and Set values are gone. The Date became a string. The &lt;code&gt;missing&lt;/code&gt; property disappeared. And you can't tell that &lt;code&gt;primary&lt;/code&gt; and &lt;code&gt;selected&lt;/code&gt; point to the same object.&lt;/p&gt;

&lt;p&gt;Those are expected &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify" rel="noopener noreferrer"&gt;JSON.stringify semantics&lt;/a&gt;. They just happen to remove information that can matter in a debugging interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared objects and cycles are different problems
&lt;/h2&gt;

&lt;p&gt;In that example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no cycle. Two properties share one object. If a viewer renders two unrelated copies, a useful relationship gets lost.&lt;/p&gt;

&lt;p&gt;Now add this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// throws TypeError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time there is a cycle. A viewer needs to stop following it while still showing where the reference goes.&lt;/p&gt;

&lt;p&gt;A replacer can make a value serializable, but you then have to decide how to represent collections, identity and reference targets. Those decisions become part of your debug panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspection can execute code, too
&lt;/h2&gt;

&lt;p&gt;This example has another surprise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;reads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;reads&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&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="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reads&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading the property ran its getter. That getter could have been expensive, stateful or capable of throwing.&lt;/p&gt;

&lt;p&gt;For a panel that opens automatically, I want to inspect the property descriptor without calling the getter. The panel can show that an accessor exists and leave its execution alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The React component I built for this
&lt;/h2&gt;

&lt;p&gt;These are the cases &lt;a href="https://github.com/NIPE-Solutions/react-data-inspector" rel="noopener noreferrer"&gt;React Data Inspector&lt;/a&gt; is designed to handle. I maintain it as an open-source React component for inspecting JavaScript values, including their types and reference relationships.&lt;/p&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @nipe-solutions/react-data-inspector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then try this in a JSX file in your React app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DataInspector&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@nipe-solutions/react-data-inspector&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@nipe-solutions/react-data-inspector/styles.css&lt;/span&gt;&lt;span class="dl"&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ada&lt;/span&gt;&lt;span class="dl"&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;debugData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;]]),&lt;/span&gt;
  &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;editor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-10-06T08:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;debugData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;debugData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DebugPanel&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DataInspector&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;debugData&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inspector distinguishes shared references from circular ones, and supports navigation to reference targets. It also has search through collapsed data, controlled expansion and selection, and tree keyboard navigation. Large values use hierarchical ranges and bounded rendering.&lt;/p&gt;

&lt;p&gt;It is read-only: it doesn't evaluate getters, await promises or execute functions. WeakMap and WeakSet contents remain opaque. The &lt;a href="https://github.com/NIPE-Solutions/react-data-inspector#readme" rel="noopener noreferrer"&gt;README&lt;/a&gt; covers the supported types and customization options.&lt;/p&gt;

&lt;p&gt;If you just need a formatted API response made of JSON-compatible values, the small &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; example may already do the job. The inspector is for cases where the richer JavaScript value is what you need to see.&lt;/p&gt;

&lt;p&gt;You can try the &lt;a href="https://react-data-inspector.nipesolutions.com/playground" rel="noopener noreferrer"&gt;playground&lt;/a&gt; before installing it.&lt;/p&gt;

&lt;p&gt;I'm looking for feedback from people building internal tools, SDK diagnostics or development panels. I'd especially like to hear about awkward object graphs, slow interactions with large values, and places where keyboard navigation gets in the way. A minimal reproduction in &lt;a href="https://github.com/NIPE-Solutions/react-data-inspector/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt; would help me turn that into a fix.&lt;/p&gt;

&lt;p&gt;What is the first object you'd put through it?&lt;/p&gt;




&lt;p&gt;I also maintain &lt;a href="https://github.com/NIPE-Solutions/readonly-view" rel="noopener noreferrer"&gt;ReadonlyView&lt;/a&gt;, which tackles a related state boundary: exposing a lazy, live readonly view while keeping owner-side updates visible. You can find it and my other frontend libraries at &lt;a href="https://opensource.nipesolutions.com" rel="noopener noreferrer"&gt;NIPE Open Source&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>My Angular Flex-Layout migration compiled. The spacing was wrong.</title>
      <dc:creator>Nicholas Petrasek</dc:creator>
      <pubDate>Tue, 06 Oct 2026 06:35:36 +0000</pubDate>
      <link>https://dev.to/cylop/my-angular-flex-layout-migration-compiled-the-spacing-was-wrong-3dj8</link>
      <guid>https://dev.to/cylop/my-angular-flex-layout-migration-compiled-the-spacing-was-wrong-3dj8</guid>
      <description>&lt;p&gt;The Angular build passed. The footer still had the wrong spacing.&lt;/p&gt;

&lt;p&gt;I found this while trying my &lt;a href="https://github.com/NIPE-Solutions/flex-layout-migrator" rel="noopener noreferrer"&gt;Angular Flex-Layout codemod&lt;/a&gt; against the archived Kubernetes Dashboard frontend. I maintain the codemod, and I wanted a codebase with enough history to find cases my examples weren't covering.&lt;/p&gt;

&lt;p&gt;The problem came down to one directive: &lt;code&gt;fxLayoutGap&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The conversion that looked reasonable
&lt;/h2&gt;

&lt;p&gt;Here is a simplified version of the problematic pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;footer&lt;/span&gt; &lt;span class="na"&gt;fxLayoutGap=&lt;/span&gt;&lt;span class="s"&gt;"16px"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/docs"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Docs&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/support"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Support&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A tempting migration is to remove the directive and add a class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;footer&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"footer-gap"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/docs"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Docs&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/support"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Support&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.footer-gap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&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;It parses. It compiles. The CSS is valid.&lt;/p&gt;

&lt;p&gt;But if that footer is an ordinary block container, &lt;code&gt;gap&lt;/code&gt; doesn't produce the spacing the old directive did.&lt;/p&gt;

&lt;p&gt;Flex-Layout's ordinary gap behavior uses margins on children. CSS &lt;code&gt;gap&lt;/code&gt; belongs to a layout context: flex, grid or multi-column. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/gap" rel="noopener noreferrer"&gt;MDN's reference&lt;/a&gt; describes those contexts. Putting &lt;code&gt;gap&lt;/code&gt; on a regular block container doesn't turn it into one.&lt;/p&gt;

&lt;p&gt;Adding &lt;code&gt;display: flex&lt;/code&gt; just to make the gap work would introduce another change. Now the children participate in flex layout, which may affect their sizing and placement. That needs an intentional layout decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  A directive name doesn't establish the layout
&lt;/h2&gt;

&lt;p&gt;The easy case has both directives on the same element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;fxLayout=&lt;/span&gt;&lt;span class="s"&gt;"row"&lt;/span&gt; &lt;span class="na"&gt;fxLayoutGap=&lt;/span&gt;&lt;span class="s"&gt;"16px"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;Cancel&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;Save&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives the migration tool evidence about the container. The standalone gap gives it much less.&lt;/p&gt;

&lt;p&gt;Responsive directives make this harder. If the container is flex at one viewport width, can the tool establish the required layout wherever the gap is active? A conversion that works on a wide screen can still be wrong on a narrow one.&lt;/p&gt;

&lt;p&gt;The check has to consider the effective layout across those ranges.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in 2.0.1
&lt;/h2&gt;

&lt;p&gt;I changed the codemod to preserve an uncertain &lt;code&gt;fxLayoutGap&lt;/code&gt; and report it for review when it cannot establish the needed flex layout on the same element across the active ranges.&lt;/p&gt;

&lt;p&gt;That leaves some work for the developer. I prefer an explicit unresolved directive to a clean-looking diff with missing spacing.&lt;/p&gt;

&lt;p&gt;After the fix, the Dashboard trial scanned &lt;strong&gt;164 templates&lt;/strong&gt; and proposed changes in &lt;strong&gt;103&lt;/strong&gt;. For both the native CSS and Tailwind targets, the report contained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;327 converted directives&lt;/li&gt;
&lt;li&gt;13 cases needing review&lt;/li&gt;
&lt;li&gt;3 invalid cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two standalone gaps stayed in the source for review. Those numbers describe the migration proposal; they don't mean the whole application has been migrated. This was a local trial on the archived project.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/NIPE-Solutions/flex-layout-migrator/pull/77" rel="noopener noreferrer"&gt;fix&lt;/a&gt; is in &lt;a href="https://github.com/NIPE-Solutions/flex-layout-migrator/releases/tag/v2.0.1" rel="noopener noreferrer"&gt;release 2.0.1&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it on your own templates
&lt;/h2&gt;

&lt;p&gt;The CLI supports native CSS and Tailwind CSS v4. It requires Node 22.12 or newer, which can be different from the runtime your older Angular application uses.&lt;/p&gt;

&lt;p&gt;Start in a branch and install the version used here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; &lt;span class="nt"&gt;--save-exact&lt;/span&gt; @nipe-solutions/flex-layout-codemod@2.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a native CSS proposal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flex-layout-codemod ./src/app &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; css &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stylesheet&lt;/span&gt; ./src/flex-layout.css &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--plan&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This plans the conversion without writing templates or a stylesheet. Review the proposed changes and unresolved diagnostics before using &lt;code&gt;--write&lt;/code&gt;. The &lt;a href="https://angular-flex-layout-codemod.nipesolutions.com/docs/workflow" rel="noopener noreferrer"&gt;workflow guide&lt;/a&gt; covers stylesheet inclusion and the write step.&lt;/p&gt;

&lt;p&gt;A few boundaries matter: the CLI works on external HTML templates, preserves unsupported cases, and doesn't remove Flex-Layout imports or dependencies. Unresolved cases produce exit code 2 by default. You still need to account for them before removing the original library.&lt;/p&gt;

&lt;p&gt;For a migration like this, I'd check the build, then check the actual layout at the viewports used by the application. Spacing, wrapping and alignment deserve their own review.&lt;/p&gt;

&lt;p&gt;If you're maintaining an Angular app that still uses Flex-Layout, I'd appreciate a trial on one representative directory. A small template that converts incorrectly is especially useful. &lt;a href="https://github.com/NIPE-Solutions/flex-layout-migrator/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; with the input, target and expected layout, or try the &lt;a href="https://angular-flex-layout-codemod.nipesolutions.com/#playground" rel="noopener noreferrer"&gt;single-template playground&lt;/a&gt; first.&lt;/p&gt;

&lt;p&gt;Have you run into a migration where the compiler was happy but the UI changed?&lt;/p&gt;




&lt;p&gt;I maintain this alongside other &lt;a href="https://opensource.nipesolutions.com" rel="noopener noreferrer"&gt;NIPE open-source frontend libraries&lt;/a&gt;. I'm looking for real usage and reproducible feedback to help improve them.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>css</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Introducing the Enhanced @nipe-solutions/react-spring-bottom-sheet: Now Supporting React 18 and XState v5</title>
      <dc:creator>Nicholas Petrasek</dc:creator>
      <pubDate>Mon, 22 Apr 2024 06:51:51 +0000</pubDate>
      <link>https://dev.to/cylop/introducing-the-enhanced-nipe-solutionsreact-spring-bottom-sheet-now-supporting-react-18-and-xstate-v5-4825</link>
      <guid>https://dev.to/cylop/introducing-the-enhanced-nipe-solutionsreact-spring-bottom-sheet-now-supporting-react-18-and-xstate-v5-4825</guid>
      <description>&lt;h2&gt;
  
  
  ✨ Updated Version
&lt;/h2&gt;

&lt;p&gt;We're excited to announce the release of the updated @nipe-solutions/react-spring-bottom-sheet, a progressive enhancement of the original library authored by Cody Olsen. This project, which I have carefully forked from Jasmine GH's maintained version, now includes full support for React 18 and integrates the latest features from XState v5.&lt;/p&gt;

&lt;p&gt;This release is not just a minor update; it's a significant leap forward, ensuring that the library stays in step with the evolving JavaScript ecosystem. We have worked diligently to incorporate the newest capabilities of React and XState, enhancing performance and developer experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  📝 Attribution
&lt;/h2&gt;

&lt;p&gt;Credit where credit is due: The seeds of this project were planted by Cody Olsen. I built upon the foundation laid by Jasmine GH, who has maintained the library with dedication. While my updates focus on aligning the project with the latest in React and state management practices, the historical contributions of Cody and Jasmine have been invaluable. For those interested in the evolution of this project or in previous versions, please visit the original repositories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Original Author's Repository: &lt;a href="https://github.com/stipsan/react-spring-bottom-sheet"&gt;Cody Olsen's GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Forked Version I Based My Work On: &lt;a href="https://github.com/JasGH/react-spring-bottom-sheet"&gt;Jasmine GH's GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔗 About the Update
&lt;/h2&gt;

&lt;p&gt;In this updated version, we've made sure that the bottom sheet component is not only up-to-date with &lt;strong&gt;React 18's&lt;/strong&gt; latest features like concurrent rendering and improved hooks support, but also with the robust, state-management capabilities provided by &lt;strong&gt;XState v5&lt;/strong&gt;. This makes the component more reliable and smoother in its operations, enhancing user interfaces with finesse.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Library Compatibility
&lt;/h2&gt;

&lt;p&gt;This update is designed to be an in-place replacement for previous versions, maintaining all the original syntax to ensure a seamless transition for existing projects. You can integrate this new version without any changes to your current implementation, benefiting immediately from the improvements in React 18 and XState v5.&lt;/p&gt;

&lt;h2&gt;
  
  
  🖥️ Example Code
&lt;/h2&gt;

&lt;p&gt;For developers looking to integrate or upgrade to this new version, example code and detailed documentation are available on our &lt;a href="https://github.com/NIPE-Solutions/react-spring-bottom-sheet"&gt;GitHub repository&lt;/a&gt; and the &lt;a href="https://www.npmjs.com/package/@nipe-solutions/react-spring-bottom-sheet"&gt;npm package page&lt;/a&gt;. We encourage you to explore these resources to understand the enhancements and how they can be applied to your projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌐 Live Demo
&lt;/h2&gt;

&lt;p&gt;To see the &lt;code&gt;@nipe-solutions/react-spring-bottom-sheet&lt;/code&gt; in action, visit our demo site at &lt;a href="https://react-spring-bottom-sheet.nipesolutions.com"&gt;react-spring-bottom-sheet.nipesolutions.com&lt;/a&gt;. This will give you a firsthand look at the smooth functionality and updated features of the bottom sheet in a live environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠️ How You Can Contribute
&lt;/h2&gt;

&lt;p&gt;The beauty of open source is collaboration. If you encounter any issues while using the updated &lt;code&gt;@nipe-solutions/react-spring-bottom-sheet&lt;/code&gt;, or if you have suggestions for further enhancements, your contributions are welcome. Feel free to &lt;a href="https://github.com/NIPE-Solutions/react-spring-bottom-sheet/issues"&gt;create an issue&lt;/a&gt; on GitHub or submit a pull request to help improve the library.&lt;/p&gt;

&lt;h2&gt;
  
  
  📣 Spread the Word
&lt;/h2&gt;

&lt;p&gt;If you're excited about this release as we are, help us spread the word! Share this post on social media, within your professional networks, or with anyone who might benefit from this enhanced library. Together, we can continue to push the boundaries of what web technologies can do.&lt;/p&gt;




&lt;p&gt;Stay tuned to our &lt;a href="https://github.com/NIPE-Solutions/react-spring-bottom-sheet"&gt;GitHub repository&lt;/a&gt; and &lt;a href="https://www.npmjs.com/package/@nipe-solutions/react-spring-bottom-sheet"&gt;npm package page&lt;/a&gt; for the latest updates and documentation. Whether you're upgrading an existing project or starting a new one, &lt;code&gt;@nipe-solutions/react-spring-bottom-sheet&lt;/code&gt; is ready to help you create a seamless and engaging user experience.&lt;/p&gt;

&lt;p&gt;Thank you so much for taking the time to read about our latest release. We hope you're as excited about these enhancements as we are. Your feedback and contributions are invaluable and make a significant difference in open-source projects. Your engagement not only helps us improve but also fosters a vibrant community around these initiatives. 🌟 Please don't hesitate to reach out, share your thoughts, or get involved. Together, let's collaborate to make the web a better place and enhance software development! 🚀&lt;/p&gt;

</description>
      <category>react</category>
      <category>bottomsheet</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
