DEV Community

Cover image for Import a File as a Raw String in Gatsby or React
Chris Frewin
Chris Frewin

Posted on

Import a File as a Raw String in Gatsby or React

Right to the Example

The fully working CodeSandbox is here. Later on in this post, you'll see step-by-step how it's possible to build such a component for your own blog posts or site!

Why Did I Need to Import Files as a Raw String?

Some readers have may have already explored my blog's snippets page, where I've collected key snippets from across all my blog posts in the cleanest form possible. This was heavily inspired by the snippets page on Josh Comeau's blog.

I've added my own flair to my snippets page: I have snippets from all around the stack, including snippets from languages like C#, Python, and Node.js

I think the TypeScript / JavaScript toggler I built is particularly cool! Later in this post, we'll go through a tutorial on how you can implement your own language toggler.

If you check out the repo for my blog, you'll see that I have a folder with each snippet actually as a normal code file in it's own language (.py for python, .cs for C#, .ts for TypeScript, and so on). As we'll see by the end of this post, I actually import each of these files as a raw string and then syntax highlight them using the prism-react-renderer library.

Walkthrough of My First (Failed) Attempt

As a walkthrough, let's assume we want to just render a single snippet, which will be a React hook, just a normal ol' TypeScript file (.ts extension), useDidMount.ts. The actual code of the hook itself is the following:

import { useState, useEffect } from 'react'

export const useDidMount = (): boolean => {
  const [didMount, setDidMount] = useState<boolean>(false)

  useEffect(() => {
    setDidMount(true)
  }, [])

  return didMount
}
Enter fullscreen mode Exit fullscreen mode

To try to render this file on my blog here, I tried first naΓ―vely to import the file directly and just calling toString() on it. πŸ˜‚ Observe this example component which does that:

import * as React from "react"
import { useDidMount } from '../snippets/useDidMount';

const ExampleComponent = () => {
    return (
        <>
            <p>useDidMount()</p>
            <pre>{useDidMount.toString()}</pre>
        </>
    )
}

export default ExampleComponent
Enter fullscreen mode Exit fullscreen mode

The result of this component ends up looking like this if you were actually to see this in browser:

Incorrect snippet rendering.

Yeah... the original source code of the snippet has been transformed and become all Webpack-y. Not useful at all for the developer visiting the site who wants the clean snippet code!

So it's clear that we can't use the standard import method to import our code snippets for rendering.

We need to import them as raw strings.

The Solution

The solution was more challenging to find than I thought. There is a package, gatsby-remark-embed-snippet, which helps you embed code snippets from a file in Gatsby, but that is a remark plugin intended only for use in markdown (or MDX) files, and anyway as of writing this, that package's dependencies were not yet upgraded to be compatible with Gatsby V3, which my blog is using. My snippets page is not an .md or .mdx file; it's a typescript react component, in a .tsx file, and I didn't want to refactor the entire page to .md or .mdx.

So, with some searching, I first found this Stackoverflow post about importing a CSS file in a raw fashion, as the original poster was creating a WYSIWYG editor, and wanted to render the CSS exactly as it was written in the editor. In that post, I found the answer:

You need to use require with Webpack's raw-loader to import files as a raw string!

Following the useDidMount example we've been using, that would be:

const useDidMount = require("!!raw-loader!./useDidMount");
Enter fullscreen mode Exit fullscreen mode

We can then render this variable using useDidMount.default.toString(), for example in a <pre> tag:

<p>useDidMount()</p>
<pre>{useDidMount.default.toString()}</pre>
Enter fullscreen mode Exit fullscreen mode

This will render the snippet's source code in the browser exactly as it appears in its source file, looking like this:

Correct snippet rendering.

Perfect! That's exactly what we want! Now it's just a matter of syntax highlighting the actual code string, and we're off!

I created a Stack Overflow question for this, which I answered myself, but it was downvoted for some reason. 😞 I'm not sure why - I think my solution is the only one that works if you want to import files as raw strings into a Gatsby project into a non-markdown or MDX file!

The Fun Part: How I make those fancy TypeScript / JavaScript Togglers on the Snippets Page!

So, with the knowledge we've learned so far in this post, I'll now provide a tutorial of how I actually build those snippet togglers on my snippets page.

The name I chose for my snippet toggler component was... SnippetToggler! Essentially, we will need a label for what we want to call our snippet, the two file labels (one for javascript and one for TypeScript) and then the actual two code sources - which will come from the headache that was the whole raw-loader issue.

So, let's start with a skeleton of our component, setting up the interface contract for the props we will need:

export interface ISnippetTogglerProps {
    snippetLabel: string
    fileLabels: Array<string>
    typeScriptCode: string
    javaScriptCode: string
}

export function SnippetToggler(props: ISnippetTogglerProps) {
  return <></>
}
Enter fullscreen mode Exit fullscreen mode

So far so good. The first thing we'll need to do is to pull off all the props:

export interface ISnippetTogglerProps {
    snippetLabel: string
    fileLabels: Array<string>
    typeScriptCode: string
    javaScriptCode: string
}

export function SnippetToggler(props: ISnippetTogglerProps) {
   const {
     snippetLabel,
     fileLabels,
     typeScriptCode,
     javaScriptCode
   } = props

  return <></>
}
Enter fullscreen mode Exit fullscreen mode

Then let's think about the state we will need. We'll need a boolean state variable to let us know which of the two snippets to actually render. I called it showJavaScript. We'll then make use of an activeModeText and className which will help us change styles and the label when we toggle the snippet:

export interface ISnippetTogglerProps {
    snippetLabel: string
    fileLabels: Array<string>
    typeScriptCode: string
    javaScriptCode: string
}

export function SnippetToggler(props: ISnippetTogglerProps) {
  const {
    snippetLabel,
    fileLabels,
    typeScriptCode,
    javaScriptCode
  } = props

   const [showJavaScript, setShowJavaScript] = useState<boolean>(false)
   const activeModeText = showJavaScript ? "JavaScript" : "TypeScript"
   const className = activeModeText.toLowerCase()

  return <></>
}
Enter fullscreen mode Exit fullscreen mode

Let's then start to think about render markup. I save the TypeScript and JavaScript version of the code block as two seperate const variables respectively:

export interface ISnippetTogglerProps {
    snippetLabel: string
    fileLabels: Array<string>
    typeScriptCode: string
    javaScriptCode: string
}

export function SnippetToggler(props: ISnippetTogglerProps) {
  const {
    snippetLabel,
    fileLabels,
    typeScriptCode,
    javaScriptCode
  } = props

  const [showJavaScript, setShowJavaScript] = useState<boolean>(false)
  const activeModeText = showJavaScript ? "JavaScript" : "TypeScript"
  const className = activeModeText.toLowerCase()

   const typeScriptBlock = (
     <>
         <code className={className}>{fileLabels[0]}</code>
         <Pre
             codeString={typeScriptCode}
             language="typescript"
         />
     </>
   )

   const javaScriptBlock = (
     <>
         <code className={className}>{fileLabels[1]}</code>
         <Pre
             codeString={javaScriptCode}
             language="javascript"
         />
     </>
   )

  return <></>
}
Enter fullscreen mode Exit fullscreen mode

where the <Pre/> component is yet another react component (it's rather involved due to the fact that we need javascript based, not markdown based syntax highlighting - here I've left out various parts of my own <Pre/> component, like a copy button and showing confetti when it is clicked. I've also fixed the syntax highlighting theme as github, but there are many other themes to choose from. See my <Pre/> component on the repository to explore the full one.

import React from "react";
import Highlight, { defaultProps, Language } from "prism-react-renderer";
import github from "prism-react-renderer/themes/github";

export interface IPreProps {
  codeString: string;
  language: Language;
}

export const Pre = (props: IPreProps) => {
  const { codeString, language } = props;

  return (
    <Highlight
      {...defaultProps}
      code={codeString}
      language={language}
      theme={github}
    >
      {({ className, style, tokens, getLineProps, getTokenProps }) => (
        <pre
          className={className}
          style={{
            ...style,
            padding: "2rem",
            position: "relative",
            overflowX: "scroll"
          }}
        >
          {tokens.map((line, i) => (
            <div {...getLineProps({ line, key: i })} style={style}>
              {line.map((token, key) => (
                <span {...getTokenProps({ token, key })} />
              ))}
            </div>
          ))}
        </pre>
      )}
    </Highlight>
  );
};
Enter fullscreen mode Exit fullscreen mode

Returning to our <SnippetToggler/> component, we can get to the return statement, adding in the actual switch component, and use the state variable showJavaScript to determine which of those const variables to render.

We've arrived at the final version of our SnippetToggler component:

import * as React from "react";
import { useState } from "react";
import { Pre } from "./Pre";

export interface ISnippetTogglerProps {
  snippetLabel: string;
  fileLabels: Array<string>;
  typeScriptCode: string;
  javaScriptCode: string;
}

export function SnippetToggler(props: ISnippetTogglerProps) {
  const { snippetLabel, fileLabels, typeScriptCode, javaScriptCode } = props;

  const [showJavaScript, setShowJavaScript] = useState<boolean>(false);
  const activeModeText = showJavaScript ? "JavaScript" : "TypeScript";
  const className = activeModeText.toLowerCase();

  const typeScriptBlock = (
    <>
      <code className={className}>{fileLabels[0]}</code>
      <Pre codeString={typeScriptCode} language="typescript" />
    </>
  );

  const javaScriptBlock = (
    <>
      <code className={className}>{fileLabels[1]}</code>
      <Pre codeString={javaScriptCode} language="javascript" />
    </>
  );

   return (
     <>
       <h3 className={className}>{snippetLabel}</h3>
       <div>
         <label className={`switch ${className}`}>
           <input
             type="checkbox"
             onChange={() => setShowJavaScript(!showJavaScript)}
             checked={showJavaScript}
           />
           <span className="slider round" />
           <span className="switch-text snippet">
             {activeModeText} Mode Active
           </span>
         </label>
       </div>
       {showJavaScript ? javaScriptBlock : typeScriptBlock}
     </>
   );
}
Enter fullscreen mode Exit fullscreen mode

Nice, we're done! πŸŽ‰

Here's the SCSS I have for the colors and switch used in our <SnippetToggler/>, if you're interested:

$typeScriptBlue: #2f74c0;
$javaScriptYellow: #efd81c;
$width: 50px;
$height: 27px;
$lightColor: #ffffff;
$darkColor: #191919;

.switch {
    position: relative;
    text-align: center;
    display: inline-block;
    height: $height;

    & .switch-text {
      margin-top: 1rem;
      display: block;
    }

    & input {
        opacity: 0;
        width: 0;
        height: 0;
    }

    & .slider {
        width: $width;
        position: absolute;
        cursor: pointer;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: $lightColor;
        -webkit-transition: 0.4s;
        transition: 0.4s;
        margin: 0 auto;
    }

    & .slider:before {
        position: absolute;
        content: "";
        height: 19px;
        width: 20px;
        left: 4px;
        bottom: 4px;
        background-color: $darkColor;
        -webkit-transition: 0.4s;
        transition: 0.4s;
    }

    & input:checked + .slider {
        background-color: $darkColor;
    }
    & input:checked + .slider:before {
        background-color: $lightColor;
    }

    & input:focus + .slider {
        box-shadow: 0 0 1px $darkColor;
    }

    & input:checked + .slider:before {
        -webkit-transform: translateX(22px);
        -ms-transform: translateX(22px);
        transform: translateX(22px);
    }

    /* Rounded sliders */
    & .slider.round {
        border-radius: $height;
    }

    & .slider.round:before {
        border-radius: 50%;
    }
}

.switch.typescript {
    & .switch-text {
        color: $typeScriptBlue;
    }
    & .slider {
        background-color: $typeScriptBlue;
    }
}

.switch.javascript {
    & .switch-text {
        color: $javaScriptYellow;
    }
    & input:checked + .slider {
        background-color: $javaScriptYellow;
    }
}
Enter fullscreen mode Exit fullscreen mode

Working Example

I've put up a CodeSandbox with the full code referenced in this blog post if you'd like to take a look and fool around with it yourself. The SCSS is the minimum working example, so of course it won't look exactly like the togglers you see on my blog, but it's pretty close!

I still have future work planned for this component. Some ideas:

  • context prop, where I can reference what blog post or posts that I've used or shown the snippet
  • description prop that gives a brief overview of what the snippet does
  • usage prop that shows the function being called in some realistic looking context
  • id prop, so I can link to specific snippets, as I expect the snippets page to get quite long

Thanks!

As always, thanks for reading and I hope this post was useful to you! You now know how to import files as raw strings and do whatever you want with them in Gatsby and / or React!

Cheers! 🍻

-Chris

Top comments (0)