<?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>Forem: Nick van Dyke</title>
    <description>The latest articles on Forem by Nick van Dyke (@nick_van_dyke).</description>
    <link>https://forem.com/nick_van_dyke</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2638377%2Feb9964d1-f9a1-44cd-9eec-40abca4e4c9e.jpg</url>
      <title>Forem: Nick van Dyke</title>
      <link>https://forem.com/nick_van_dyke</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/nick_van_dyke"/>
    <language>en</language>
    <item>
      <title>Engage Users Instantly: Embed an Interactive Demo in your React SPA</title>
      <dc:creator>Nick van Dyke</dc:creator>
      <pubDate>Thu, 09 Jan 2025 19:25:31 +0000</pubDate>
      <link>https://forem.com/nick_van_dyke/engage-users-instantly-embed-an-interactive-demo-in-your-react-spa-2cf1</link>
      <guid>https://forem.com/nick_van_dyke/engage-users-instantly-embed-an-interactive-demo-in-your-react-spa-2cf1</guid>
      <description>&lt;p&gt;&lt;strong&gt;If a picture is worth a thousand words, then an interactive demo must be worth... a million?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Do you enjoy scrolling through buzzwords to understand an app's purpose? Probably not. And I didn't care to write all that blather for my latest passion project, &lt;a href="https://wanna.social" rel="noopener noreferrer"&gt;Wanna&lt;/a&gt;. So I pursued a more interesting solution: nest my app inside its own landing page for users to explore!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1h1vyuarpvrou1kl05c4.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1h1vyuarpvrou1kl05c4.gif" width="493" height="1075"&gt;&lt;/a&gt;&lt;/p&gt;
This gif has 263 frames, so I guess it's worth 263,000 words



&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;Thanks to React's composability, we can &lt;em&gt;almost&lt;/em&gt; simply render our root &lt;code&gt;App&lt;/code&gt; component and call it a day:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;InteractiveDemo&lt;/span&gt; &lt;span class="o"&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="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;lt;&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;/&amp;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;However, you'll run into a few problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The demo app's navigation will navigate the real app&lt;/li&gt;
&lt;li&gt;The demo app will retrieve real data, which may fail or not showcase it well&lt;/li&gt;
&lt;li&gt;It may not be obvious to users what they're looking at&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's solve those. Wanna uses React Router v6 and Apollo GraphQL, but the concepts apply regardless of technology.&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigation
&lt;/h3&gt;

&lt;p&gt;To separate the demo app's navigation from the real app, we wrap it inside another navigation provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+import { MemoryRouter, UNSAFE_LocationContext } from 'react-router'
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const InteractiveDemo = () =&amp;gt; {
&lt;/span&gt;    return (
&lt;span class="gi"&gt;+       // Hack to nest MemoryRouter inside BrowserRouter.
+       // https://github.com/remix-run/react-router/issues/7375
+       &amp;lt;UNSAFE_LocationContext.Provider value={null}&amp;gt;
+           &amp;lt;MemoryRouter initialEntries={['/app']}&amp;gt;
&lt;/span&gt;                &amp;lt;App /&amp;gt;
&lt;span class="gi"&gt;+           &amp;lt;/MemoryRouter&amp;gt;
+       &amp;lt;/UNSAFE_LocationContext.Provider&amp;gt;
&lt;/span&gt;    )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note we use a &lt;code&gt;MemoryRouter&lt;/code&gt; so the browser remains on the same page while the demo navigates internally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data
&lt;/h3&gt;

&lt;p&gt;To provide the demo app with fake data, we maintain a fake "backend" inside the client app with &lt;code&gt;useState&lt;/code&gt; and serve it via a mock client or server (depending on implementation). It's minimally invasive to the rest of the app code, and even lets us use the demo for manual testing - very handy when iterating quickly.&lt;/p&gt;

&lt;p&gt;I used &lt;a href="https://github.com/mike-gibson/mock-apollo-client" rel="noopener noreferrer"&gt;mock-apollo-client&lt;/a&gt;; for REST or tRPC, you might use something like &lt;a href="https://github.com/nock/nock" rel="noopener noreferrer"&gt;nock&lt;/a&gt;. They're meant for automated testing but are exactly what we need here.&lt;/p&gt;

&lt;p&gt;First, we create a mock client whose request handlers query and mutate demo data in a way that mimicks the real backend:&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;InMemoryCache&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="s1"&gt;@apollo/client&lt;/span&gt;&lt;span class="dl"&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;createMockClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createMockSubscription&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="s1"&gt;mock-apollo-client&lt;/span&gt;&lt;span class="dl"&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;useMemo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&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="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="c1"&gt;// GraphQL documents that our client sends to the real server&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;GET_FRIENDS&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../gql/getFriends.gql&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ADD_FRIEND&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../gql/addFriend.gql&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Simplified example&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useDemoClient&lt;/span&gt; &lt;span class="o"&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="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;friends&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFriends&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="na"&gt;__typename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User&lt;/span&gt;&lt;span class="dl"&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;1&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="s1"&gt;Nick&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="c1"&gt;// Cache should persist across clients&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&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="c1"&gt;// Should be the same cache configuration you provide to your real Apollo client&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InMemoryCache&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="c1"&gt;// We need to recreate the mock client whenever the data changes&lt;/span&gt;
    &lt;span class="c1"&gt;// because it doesn't support resetting request handlers.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mockClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMockClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;GET_FRIENDS&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;friends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;friends&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}))&lt;/span&gt;
        &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ADD_FRIEND&lt;/span&gt;&lt;span class="p"&gt;,&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;setFriends&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&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="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;addFriend&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="p"&gt;})&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;friends&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;mockClient&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then just like we did with navigation, we wrap our demo in a new provider with our mock client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+import { ApolloProvider } from '@apollo/client'
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const InteractiveDemo = () =&amp;gt; {
&lt;/span&gt;&lt;span class="gi"&gt;+   const demoClient = useDemoClient()
&lt;/span&gt;    return (
&lt;span class="gi"&gt;+       &amp;lt;ApolloProvider client={demoClient}&amp;gt; 
&lt;/span&gt;            &amp;lt;UNSAFE_LocationContext.Provider value={null}&amp;gt;
                &amp;lt;MemoryRouter initialEntries={['/app']}&amp;gt;
                    &amp;lt;App /&amp;gt;
                &amp;lt;/MemoryRouter&amp;gt;
            &amp;lt;/UNSAFE_LocationContext.Provider&amp;gt;
&lt;span class="gi"&gt;+       &amp;lt;/ApolloProvider&amp;gt;
&lt;/span&gt;    )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you used a mock server instead, you'd inject its URL into the demo app's real client.&lt;/p&gt;

&lt;h3&gt;
  
  
  Visuals
&lt;/h3&gt;

&lt;p&gt;It works! Now how do we make it obvious to the user that they're viewing an interactive demo?&lt;/p&gt;

&lt;p&gt;Wanna is mobile-first, so I chose to render the demo inside a phone frame. I used &lt;a href="https://github.com/picturepan2/devices.css" rel="noopener noreferrer"&gt;devices.css&lt;/a&gt; because it offers the devices I thought looked best (i.e. minimal bezel to maximize demo space). But for simplicity, here we'll use a library that supports React out-of-the-box: &lt;a href="https://github.com/zheeeng/react-device-frameset" rel="noopener noreferrer"&gt;react-device-frameset&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let's also use &lt;code&gt;zoom&lt;/code&gt; to shrink the demo UI and nicely nest it inside the rest of the page. In Wanna I had to invert and account for this zoom when using &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; with &lt;a href="https://www.npmjs.com/package/visjs-html-nodes" rel="noopener noreferrer"&gt;visjs-html-nodes&lt;/a&gt; to draw the invitee graph, but for the most part it "just works".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+import { DeviceFrameset } from 'react-device-frameset'
+import 'react-device-frameset/styles/marvel-devices.min.css'
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;export const InteractiveDemo = () =&amp;gt; {
&lt;/span&gt;    const demoClient = useDemoClient()
    return (
&lt;span class="gi"&gt;+       &amp;lt;DeviceFrameset 
+           device='iPhone X'
+           style={{ zoom: '70%' }}&amp;gt;
&lt;/span&gt;            &amp;lt;ApolloProvider client={mockClient}&amp;gt; 
                &amp;lt;UNSAFE_LocationContext.Provider value={null}&amp;gt;
                    &amp;lt;MemoryRouter initialEntries={['/app']}&amp;gt;
                        &amp;lt;App /&amp;gt;
                    &amp;lt;/MemoryRouter&amp;gt;
                &amp;lt;/UNSAFE_LocationContext.Provider&amp;gt;
            &amp;lt;/ApolloProvider&amp;gt;
&lt;span class="gi"&gt;+       &amp;lt;/DeviceFrameset&amp;gt;
&lt;/span&gt;    )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lastly, it helps to have an animation so it's clearly not a static image. For example, Wanna continually ghost-types suggestions in the activity input field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration
&lt;/h2&gt;

&lt;p&gt;Now that we have our &lt;code&gt;InteractiveDemo&lt;/code&gt; component, we render it inside our landing page and we're done!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;export const Landing = () =&amp;gt; {
&lt;/span&gt;    return (
        &amp;lt;div&amp;gt;
            {/* ... */}
&lt;span class="gi"&gt;+           &amp;lt;InteractiveDemo /&amp;gt;
&lt;/span&gt;            {/* ... */}
        &amp;lt;/div&amp;gt;
    )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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