npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

eleventy-plugin-react

v1.1.0

Published

A plugin that allows you to use React as a templating language for Eleventy

Downloads

19

Readme

eleventy-plugin-react

npm node

A plugin that allows you to use React as a templating language for Eleventy. This is currently experimental, and relies on unstable Eleventy APIs.

Installation

npm install eleventy-plugin-react babel-loader @babel/core @babel/preset-env @babel/preset-react react react-dom core-js@3 regenerator-runtime

or

yarn add eleventy-plugin-react babel-loader @babel/core @babel/preset-env @babel/preset-react react react-dom core-js@3 regenerator-runtime

Usage

First, add the plugin to your config. The plugin will automatically compile any files given to it with a .js or .jsx extension using Babel and server-side render the page.

// .eleventy.js

const eleventyReact = require("eleventy-plugin-react");

module.exports = function (eleventyConfig) {
  eleventyConfig.addPlugin(eleventyReact);

  return {
    dir: {
      input: "src/pages",
    },
  };
};
// src/pages/index.js OR src/pages/index.jsx

import React from "react";
import ParentLayout from "../layouts/ParentLayout";
import ChildComponent from "../components/ChildComponent";

// `props` is the data provided by Eleventy.
export default function IndexPage(props) {
  return (
    <ParentLayout>
      <h1>Welcome!</h1>
      <ChildComponent url={props.page.url} />
    </ParentLayout>
  );
}

All the content will be rendered into the body. Using the postProcess hook with React Helmet can be used to alter the head (see here).

Data for each page is passed as props to the entrypoint page component. You can learn more about using data in Eleventy here.

You can now run Eleventy to build your site!

# Requires ELEVENTY_EXPERIMENTAL flag to run

ELEVENTY_EXPERIMENTAL=true npx @11ty/eleventy

Note: Since this plugin currently relies on experimental Eleventy APIs, running the build requires using the ELEVENTY_EXPERIMENTAL=true CLI flag.

Options

targets (optional)

{
  targets?: string;
}

targets is what used to specify browser targets for the component hydration bundle for the client. Under the hood, it's passed to @babel/preset-env. Defaults to "last 2 versions, safari >= 12". Please note that you do not need to specify this if you are creating your own custom Babel configuration using the babelConfig option, as long as you set your own targets.

exts (optional)

{
  exts?: string[];
}

exts allows you to define what extensions you would like Eleventy to include when running this plugin. Defaults to ["js", "jsx"].

babelConfig (optional)

{
  babelConfig: ({ config: BabelConfig; isClientBundle: boolean }) =>
    BabelConfig;
}

This option is only required if you would like to customize your Babel configuration. babelConfig is a function that returns a Babel configuration object to be used both for compiling during server-side rendering the the static markup as well as when bundling the hydrated components for the browser. This takes the place of using a standard Babel configuration file, and the available options can be found here.

The function is called with an object that has the following signature:

{
  // The default Babel config.
  config: BabelConfig;
  // When `true`, the configuration is being used to build the client bundle.
  // When `false`, it is being used to compile the code for server-side rendering.
  isClientBundle: boolean;
}

There are a few gotchas when configuring Babel for server-side rendering as well as for the client:

  1. Compile to CommonJS when executing in a project that is not using ES Modules.
  2. targets should be set so that the code can be executed in the version of Node.js you're using. If this doesn't match the syntax supported in the target browsers, you can use the isClientBundle property in the context object to configure it for both environments.
const presetEnv = require("@babel/preset-env");
const presetReact = require("@babel/preset-react");

function babelConfig({ config, isClientBundle }) {
  return {
    presets: [
      [
        presetEnv,
        {
          // Must be "commonjs" when not using ES Modules in Node.js.
          modules: isClientBundle ? false : "commonjs",
          targets: isClientBundle
            ? "> 0.25%, not dead"
            : {
                // Ensure that the server-side rendered components can be executed
                // in the current version of Node.js.
                node: process.versions.node,
              },
        },
      ],
      presetReact,
    ],
  };
}

assetsPath (optional)

{
  assetsPath?: string;
}

assetsPath is the path for the outputted bundle of hydrated client-side assets, relative to Eleventy's configured output directory. Defaults to "/assets/". By default, this means that the client-side bundles would be outputted to _site/assets/.

postProcess (optional)

{
  postProcess?: ({ html: string, data: EleventyData }) => string | async ({ html: string, data: EleventyData }) => string;
}

postProcess is a function (both synchronous and asynchronous functions are supported) that is called after server-side rendering has completed. This hook serves as a way to transform the rendered output before it is written to disk (extracting critical styles and inserting them into the head, for instance). The string (or Promise resolving to a string) that is returned will be written to disk.

The function is called with an object that has the following signature:

{
  // The rendered HTML for the page.
  html: string;
  // The data provided to the page by Eleventy.
  data: EleventyData;
}

If postProcess is not defined, the following default HTML will be generated:

function defaultPostProcess({ html, data }) {
  return `
    <!DOCTYPE html>
    <html>
      <head>
        <title>${data.page.title || data.site.title}</title>
        <meta name="description" content=${
          data.page.description || data.site.description
        } />
        <meta
          name="viewport"
          content="width=device-width, initial-scale=1, shrink-to-fit=no"
        />
      </head>
      <body>
        <div id="content">${html}</div>
      </body>
    </html>
  `;
}

To integrate react-helmet, you can use the following postProcess function in your .eleventy.js configuration:

const { Helmet } = require("react-helmet");

// Remove Helmet-specific data attributes from static HTML.
function removeHelmetDataAttribute(str) {
  return str.replace(/data-react-helmet="true"/g, "").replace(/ {2,}/g, " ");
}

function postProcess({ html, data }) {
  const helmet = Helmet.renderStatic();

  return `
    <!doctype html>
    <html ${removeHelmetDataAttribute(helmet.htmlAttributes.toString())}>
      <head>
        ${removeHelmetDataAttribute(helmet.title.toString())}
        ${removeHelmetDataAttribute(helmet.meta.toString())}
        ${removeHelmetDataAttribute(helmet.link.toString())}
      </head>
      <body ${removeHelmetDataAttribute(helmet.bodyAttributes.toString())}>
        <div id="content">
          ${html}
        </div>
      </body>
    </html>
  `;
}

Example usage

const myBabelPlugin = require("my-babel-plugin");

eleventyConfig.addPlugin(eleventyReact, {
  babelConfig({ config, isClientBundle }) {
    if (isClientBundle) {
      config.plugins.push(myBabelPlugin);
    }

    return config;
  },
  assetsPath: "/assets/js",
  async postProcess(html) {
    try {
      // Try to extract and inline critical styles into head.
      const transformedHtml = await extractAndInsertCritialStyles(html);
      return transformedHtml;
    } catch (e) {
      // Fall back to original html if unsuccessful.
      console.error("Extraction of critical styles failed.");
      return html;
    }
  },
});

Interactive components

The plugin includes a withHydration higher order component utility that marks a component for hydration, bundles the component, and inserts a script into the body of the rendered HTML that hydrates the component in the client.

Some important notes about withHydration:

  • The component to be hydrated must either be the default export of a file or wrapped in the withHydration higher order component and exported. Examples below!
  • The plugin keeps track of the hydrated components (which is why marking components for hydration is limited to default exports) and only bundles those components. I'd love to find a way around this limitation, if anyone has any ideas.
  • The hydration JavaScript bundle is only created when there are components marked for hydration and it is automatically generated and appended to the body of the server-side rendered markup for each page.
import React from "react";
import { Helmet } from "react-helmet";

export default function PageLayout(props) {
  return (
    <>
      <Helmet>
        <meta charSet="utf-8" />
        <title>My Title</title>
        <link rel="canonical" href="http://mysite.com/example" />
      </Helmet>
      <div class="container">{props.children}</div>
    </>
  );
}
import React, { useState } from "react";
import { withHydration } from "eleventy-plugin-react/utils";

function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me, please!</button>
    </>
  );
}

// The component can be wrapped in the withHydration higher order component and exported.
// Note that it must be the default export.
export default withHydration(Counter);
import React, { useState } from "react";

function Counter2({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me, please!</button>
    </>
  );
}

// The component can be exported and then wrapped in the withHydration higher order component in
// the parent component. Note that it must be the default export.
export default Counter2;
import React from "react";
import { withHydration } from "eleventy-plugin-react/utils";
import PageLayout from "../layouts/Page";
import Counter from "../components/Counter";
import Counter2 from "../components/Counter2";

// `props` is the data provided by Eleventy.
export default function IndexPage(props) {
  // Only hydrate the Counter2 component on the home page.
  // Counter2 is the default export in the module it's defined in,
  // so we can hydrate it conditionally in the component that imports it.
  const MaybeHydratedCounter2 =
    props.page.url === "/" ? withHydration(Counter2) : Counter2;

  return (
    <PageLayout>
      {/* Counter is wrapped and then exported */}
      <Counter initialCount={2} />
      {/* Counter2 is exported and then wrapped */}
      <MaybeHydratedCounter2 />
    </PageLayout>
  );
}