'Tomi Odusanya Designer | Building delightful web experiences @paystack

Recoil in action: Building a reusable code block component

6 min read 1711

React Recoil in Action: Building a Reusable Code Block Component

In this tutorial, you will learn how to build a code snippet block with multi-language support for your blog or developer documentation project. We’ll style the code block to look similar to the VS Code interface. We’ll also use React Recoil to improve UX by remembering the user’s language preference.

Setup

First, note that this tutorial assumes you have a fundamental understanding of React. Next, you’ll need to confirm that you have Node installed — any version ≥8 is fine. Lastly, if you’d like to follow along, here’s a link to the starter repo.

Your cloned project should have a folder structure similar to this:

|----- src
  |----- components
    |----- Codeblock
      |----- Codeblock.jsx
      |----- Codeblock.scss
    |----- CodeGroup
      |----- CodeGroup.jsx
      |----- CodeGroup.scss
  |----- views
    |----- Home.jsx
    |----- About.jsx
  |----- utils
  |----- store
      |----- index.js
  |----- index.js
  |----- App.js
  |----- App.scss

The starter repo is bootstrapped from create-react-app, with the following Node dependencies pre-installed:

  • node-sass: While styling, this helps us write SCSS instead of vanilla CSS
  • prism-react-renderer: A lightweight Prism React wrapper. This helps us build our custom code blocks with syntax highlights

Building the code snippet component with React Recoil

Our Finished Code Snippet Module
Our finished code snippet module.

Our code snippet component can be reused across the whole site. A perfect use case would be custom developer documentation or a technical blog, wherein you’ll need to frequently share code snippets.

To aid readability, we’ll split the logic into two components:

  • CodeSnippet: Displays the code and handles code syntax highlights
  • CodeGroup: Wraps the CodeSnippet component while adding multi-language support

Edit the CodeSnippet.jsx file with the code below:

import React from 'react';
import Highlight, { defaultProps } from 'prism-react-renderer';

// Import popular vscode nightOwl theme
import theme from 'prism-react-renderer/themes/nightOwl';
import './CodeSnippet.scss';

const supportedSyntax = {
  curl: 'bash',
  node: 'js',
  js: 'js',
  ruby: 'rb',
  php: 'php',
};

const Codeblock = ({ code, syntax, className }) => (
  <Highlight
    {...defaultProps}
    theme={theme}
    code={code.trim()}
    language={supportedSyntax[syntax]}>
    {({ style, tokens, getLineProps, getTokenProps }) => (
      <pre className={`c-pre ${className}`}>
        {/* Tokens are equivalent to each row/line of code text */}
        {tokens.map((line, index) => (
          <div
            className={'c-line'}
            key={index} {...getLineProps({ line, key: index })}>
            <span className="c-line-number">{index + 1}</span> {/* Show code line number */}
            <span className="c-line-content">
              {/* Show code snippet for that line */}
              {line.map((token, key) => (
                <span key={key} {...getTokenProps({ token, key })} />
              ))}
            </span>
          </div>
        ))}
      </pre>
    )}
  </Highlight>
);
export default Codeblock;

The code above extends Highlight and defaultProps from prism-react-renderer. Highlight is a React component that takes in a bunch of props, most importantly:

  • code (string): The text/code snippet to display
  • language (string): This helps prism-react-render set language-specific syntax highlights. See the Prism docs for list of supported languages
  • theme (object): This optional param lets you override the default code theme from Prism. In our case, we override it with the nightOwl theme in line 5 above

The defaultProps object handles all the other props required by Highlight; you’ll mostly like never need to change those. See the documentation for a full list of supported props.

Tip: It is advised to use the native JavaScript .trim() method on your code strings to remove unnecessary whitespace.

Edit the CodeGroup.jsx file with the code below:

We made a custom demo for .
No really. Click here to check it out.

import React, { useState, useEffect, useRef } from 'react';

import CodeSnippet from '../CodeSnippet/CodeSnippet';
import './CodeGroup.scss';

const languageOptions = {
  curl: 'cURL',
  node: 'NodeJS',
  php: 'PHP',
};

const CodeGroup = ({ code }) => {
  const select = useRef();
  // Set the default language //
  const [language, setLanguage] = useState('curl');
  const handleChange = event => setLanguage(event.target.value);
  
  // Update the select value when langauge state changes
  useEffect(() => {
    select.current.value = language;
  }, [language]);

  return (
    <code className="c-code-group">
      <span className="c-dropdown">
        <select
          ref={select}
          name="codeSelect"
          defaultValue={language}
          onChange={handleChange}>
          {Object.keys(code).map((lang, index) => (
            <option
              key={index}
              value={lang.toLowerCase()}>{languageOptions[lang]}</option>)
          )}
        </select>
      </span>
      <>
        {Object.keys(code).map((lang, index) => (
          <CodeSnippet
            key={index}
            code={code[lang]}
            className={language === lang ? 'c-snippet c-snippet--is-active' : 'c-snippet'}
            syntax={lang} />
        ))}
      </>
    </code>
  );
};

export default CodeGroup;

The CodeGroup component wraps the CodeSnippet component and adds multi-language support to our custom code block.

The CodeGroup component accepts a code prop of type object. This is the code we would like to display. The object key is the language name, and the value is a corresponding string to be displayed for that language.

For simplicity, we store these objects in a utils/{page_name}Requests.js file and import them when needed. Below is a sample code object. It can be found in the utils/HomeRequests.js directory:

const curl = `
curl -v -X POST https://api.sandbox.paypal.com/v2/invoicing/generate-next-invoice-number \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer Access-Token"
`;
const node = `
const https = require('https');
const options = {
  hostname: 'api.paystack.co',
  path: '/transaction/verify/:reference',
  method: 'GET',
  headers: {
    Authorization: 'Bearer SECRET_KEY'
  }
};
`;
const php = `
$stripe = new \Stripe\StripeClient('sk_test_BQokikJOvBiI2HlWgH4olfQ2');
$customer = $stripe->customers->create([
  'description' => 'example customer',
  'email' => 'email@example.com',
  'payment_method' => 'pm_card_visa',
]);
echo $customer;
`;
const code = {
  curl,
  node,
  php,
}
export default code;

Here’s a working demo of the app.

In the demo above, the code snippet works across multiple pages. However, the code language option resets when you navigate pages, this may not be great for usability. That’s where Recoil comes to play.

Saving language preference with Recoil

Recoil is a lightweight React state management library — like Redux, but simpler. Recoil has a simple API and is powered by React Hooks under the hood. Recoil lets us persist language across multiple pages or even remember a user’s preferred language when they return to your website.

Getting started with Recoil

First, run yarn add Recoil to add Recoil to your project. In your src/index.js file, import RecoilRoot from Recoil and wrap the App component with RecoilRoot:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { RecoilRoot } from 'Recoil';

import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <RecoilRoot> // Wrap the App component with RecoilRoot
        <App />
      </RecoilRoot>
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
);

At its core, React Recoil is based on an atom and selector model. In Recoil, atoms are units of states that components can subscribe to, while selectors are pure functions that are used to update the state, either synchronously or asynchronously. If you’re familiar with other state management libraries like Redux, atoms are akin to state objects. Read more here.

Edit the store/index.js file with the code below:

import { atom } from 'Recoil';

export const codeLanguageState = atom({
  key: 'snippetLanguageState', // Unique state identifier
  default: 'curl', // Default state value
});

The atom function accepts a JavaScript object:

  • key is a unique identifier
  • default, as the name implies, is the default state

We can now read and write to the codeLanguageState atom using Recoil’s useRecoilState Hook.

Edit CodeGroup.jsx with the following updates:

import React, { useEffect, useRef } from 'react';
+ import { useRecoilState } from 'Recoil';
import { codeLanguageState } from '../../store/index';

import CodeSnippet from '../CodeSnippet/CodeSnippet';
import './CodeGroup.scss';

const languageOptions = {
  curl: 'cURL',
  node: 'NodeJS',
  php: 'PHP',
};

const CodeGroup = ({ code }) => {
  const select = useRef();
  // Set default langauge using 
-  const [language, setLanguage] = useState('curl');
+  const [language, setLanguage] = useRecoilState(codeLanguageState);
  const handleChange = event => setLanguage(event.target.value);
  useEffect(() => {
    select.current.value = language;
  }, [language]);
  return (
    <code className="c-code-group">
      <span className="c-dropdown">
        <select
          ref={select}
          name="codeSelect"
          defaultValue={language}
          onChange={handleChange}>
          {Object.keys(code).map((lang, index) => (
            <option
              key={index}
              value={lang.toLowerCase()}>{languageOptions[lang]}</option>)
          )}
        </select>
      </span>
      <>
        {Object.keys(code).map((lang, index) => (
          <CodeSnippet
            key={index}
            code={code[lang]}
            className={language === lang ? 'c-snippet c-snippet--is-active' : 'c-snippet'}
            syntax={lang} />
        ))}
      </>
    </code>
  );
};
export default CodeGroup;

In the snippet above, we make two additions to the existing CodeGroup.jsx file:

  1. Extend Recoil’s useRecoilState (line 2)
  2. Replace the useState Hook with the useRecoilState Hook and pass our state — the codeLanguageState atom, in this case — as a param (line 18)

Recoil’s useRecoilState API is identical to React’s useState Hook, with the major difference being that our state can be shared between components.

Hurray! Our app now stores language preference across multiple pages.

If you’re still following along with the starter file, congratulations. The final step is to go to the Home.jsx and Payments.jsx files in the /views directory and undo the comments to import the CodeSnippet component to the Home and Payments pages.

Here’s a live demo.

Session persistence with Recoil

From the demo above, you’ll notice that even though the language persists when you navigate between pages, the language resets to curl when the page is refreshed. Using Recoil and recoil-persist (a third-party library), we can add session persistence to our app in three easy steps:

Step 1: Add recoil-persist

Run yarn add recoil-persist to add recoil-persist to our app.

Step 2: Add persistence_UNSTABLE to the atom

In the store/index.js file, update the codeLanguageState with a persistence_UNSTABLE key:

import { atom } from 'Recoil';
export const codeLanguageState = atom({
  key: 'snippetLanguageState',
  default: 'curl',
+ persistence_UNSTABLE: {
+   type: 'snippetLanguageState' // type should be the same as the atom key
+ },
});
Step 3: Set initializeState prop

Update your index.js file with the following:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { RecoilRoot } from 'Recoil';
+ import RecoilPersist from 'recoil-persist';
import './index.css';
import App from './App';
+ const { RecoilPersist, updateState } = RecoilPersist();


ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
-     <RecoilRoot>
+     <RecoilRoot initializeState={updateState}>
+       <RecoilPersist />
        <App />
      </RecoilRoot>
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
);

Conclusion

Now our app is complete. It’s smart enough to remember the user’s last selected language, even after a refresh. You can try it out here. Here’s a link to the Github repo, if you’d like to play around with the final code.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

'Tomi Odusanya Designer | Building delightful web experiences @paystack

Leave a Reply