719-286-0751 [email protected]

How to add React to a Shopify Theme

How to add React to a Shopify Theme

In this tutorial we will cover the basic setup required to include React into a Shopify theme. This will include:

  • Initializing NPM in our theme
  • Installing necessary dependencies
  • Creating React components within our Shopify theme
  • Configuring webpack and babel to set up our dev environment and include our react into our theme
  • Editing liquid templates to enable React to render
  • Utilizing the Shopify AJAX API to get data in our React component

 

Prerequisites

This tutorial assumes a basic working knowledge of javascript, react, node/npm, command line interface, and Shopify theme development.

Required software:

If you want to follow along step by step, I created a brand new store with the Debut theme for this tutorial.

I have also created a repo on github with the finished product!

Steps for Adding React to a Shopify Theme

0. ThemeKit

Let’s get ThemeKit watching our repo so that as we change things, they will be reflected in our site. Go to your theme directory and run theme watch and keep that running!

1. Setup up theme as NPM package

This will allow us to install NPM packages into the theme directory. Enter your theme root directory and use the following command in a terminal:

$ npm init

for interactive setup or I will use

$ npm init -y

to have everything set as default (fine for a tutorial, but probably not for your production theme).

2. Install Necessary Dependencies

We will use NPM to install all the needed dependencies for react development. --save ensures that these packages will be added to the package.json file and be tracked as a dependency.

$ npm install --save react react-dom

We also need some helpers for our developer environment, to be able to package things in a browser friendly way:

$ npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader core-js webpack webpack-cli webpack-merge

Babel will transpile our react code to something that browsers understand

core-js provides polyfills for older browsers (you’ll see how we use this in a bit)

webpack bundles things nicely for us (creates a single file)

3. Getting set up

Before we really try to write any serious code, let’s get react into our site and make sure it is working. We’ll start by just replacing the checkout icon in our header with one from react to verify everything is working.

We need to:
1. create the checkout icon in react
2. create a location in the header to attach react to
3. create a JS file that tells react where and what to render
4. set up babel and webpack to create a file Shopify can understand and serve to visitors
5. modify our liquid files to source this JS file

Step 1

I’m going to create a scripts directory in my Shopify theme to organize our react code. Let’s create our cart icon in the Components subdirectory as scripts/Components/CartToolTip.js (no, it’s not a tooltip yet, but soon it will be so this will just save us some re-naming). We just need to find the svg HTML for out cart icon in our theme… looks like this is at snippets/icon-cart.liquid . Copy the contents of that file to our CartToolTip component, it should look like the following when you are finished:


import React from 'react'

export const CartToolTip = () => ()
Step 2

We need to replace our existing cart icon with an empty element for react to render within. Currently the cart logo is included in the header in the file sections/header.liquid line 139:


          <a href="{{ routes.cart_url }}" class="site-header__icon site-header__cart">
            {% include 'icon-cart' %}
            <span class="icon__fallback-text">{{ 'layout.cart.title' | t }}</span>
            <div id="CartCount" class="site-header__cart-count{% if cart.item_count == 0 %} hide{% endif %}" data-cart-count-bubble>
              <span data-cart-count>{{ cart.item_count }}</span>
              <span class="icon__fallback-text medium-up--hide">{{ 'layout.cart.items_count' | t: count: cart.item_count }}</span>
            </div>
          </a>

We are going to replace the reference to the cart icon with a file we will create in a moment. Let’s also remove the anchor tag, otherwise our tooltip will be one giant link (we will make our icon a link again later).


          <span class="site-header__icon site-header__cart">
            {% include 'react-cart-tool-tip' %}
            <span class="icon__fallback-text">{{ 'layout.cart.title' | t }}</span>
            <div id="CartCount" class="site-header__cart-count{% if cart.item_count == 0 %} hide{% endif %}" data-cart-count-bubble>
              <span data-cart-count>{{ cart.item_count }}</span>
              <span class="icon__fallback-text medium-up--hide">{{ 'layout.cart.items_count' | t: count: cart.item_count }}</span>
            </div>
          </span>

Now we create the file we referenced at `snippets/react-cart-tool-tip.liquid`


{% comment %}

Root element for react tool tip

{% endcomment %}

<div id="react-cart-tool-tip"></div>
Step 3

Then, we need to create a file that tells react to put our icon in this element we created. Create the following file at scripts/cart-tool-tip.js


import ReactDOM from "react-dom"
import React from "react"
import { CartToolTip } from "./Components/CartToolTip"

const rootEl = document.getElementById("react-cart-tool-tip")

rootEl && ReactDOM.render(, rootEl)

Alright! Wait… nothing is happening. This is because we still haven’t created anything that Shopify/ThemeKit will sync to your theme!

Step 4

We need to tell Babel and Webpack how to handle our javascript code. If you’ve used create-react-app, this is handled for you. Because we are integrating this with Shopify, we need to tell these helpers exactly where these files need to end up to work properly.

Babel

Babel is what will translate our react code (JSX) to normal javascript. create a .babelrc file in the root of your theme:


{
   "presets":[
      [
         "@babel/preset-env",
         {
            "useBuiltIns":"usage",
            "corejs":3
         }
      ],
      "@babel/preset-react"
   ]
}

 

This tells babel to use core-js to polyfill things back to ES2015 (and others like fetch for IE), and to translate JSX to normal javascript. Webpack will handle running babel on our code.

Webpack

We are going to set up 2 configurations for webpack – one for development (that includes devtools and source mapping) and one for production (that is as light as possible). We need to create 3 files —

webpack.common.js — this is shared config


const path = require("path")

module.exports = {
  entry: {
    'cart-tool-tip': "./scripts/cart-tool-tip.js"
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: ["babel-loader"]
      }
    ]
  },
  output: {
    filename: "[name].bundle.js",
    path: path.resolve(__dirname, "assets")
  }
}

 

notice the entry field — this tells webpack where to source our file. the property name (cart-tool-tip) is what will be put in place of [name] for our generated file (under output), in the directory specified by output.path. So this config will generate assets/cart-tool-tip.bundle.js for our theme!

webpack.dev.js


const merge = require("webpack-merge")
const common = require("./webpack.common.js")

module.exports = merge(common, {
  mode: "development",
  devtool: "inline-source-map",
  watch: true
})

 

webpack.prod.js


const merge = require("webpack-merge")
const common = require("./webpack.common.js")

module.exports = merge(common, {
  mode: "production"
})

 

Now let’s set up a couple helper scripts in our package.json so we can utilize these! Add a scripts field (or add these entries to the scripts field).


  "scripts": {
    "dev": "webpack --config webpack.dev.js --progress --color",
    "build": "webpack --config webpack.prod.js --progress --color"
  },

 

Recap

Woo that was a lot! if you’ve been following along, your package.json should look something like this at this point:


{
  "name": "react-shopify-example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack --config webpack.dev.js --progress --color",
    "build": "webpack --config webpack.prod.js --progress --color"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^16.12.0",
    "react-dom": "^16.12.0"
  },
  "devDependencies": {
    "@babel/core": "^7.7.5",
    "@babel/preset-env": "^7.7.6",
    "@babel/preset-react": "^7.7.4",
    "babel-loader": "^8.0.6",
    "core-js": "^3.4.8",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10",
    "webpack-merge": "^4.2.2"
  }
}

 

Did it work?

At this point, you should be able to run npm run dev and webpack will build the asset file! (and watch for changes)

Just one more step to get it onto the page…

Step 5

We have to include this JS file into our site! I am going to include mine in sections/header.liquid . I put it at line 17, with some other scripts before the actual visible HTML. We are going to load this as a module and defer it, so it won’t load until after the visible elements anyway. Be careful not to unintentionally put your script tag inside a conditional block!


<script defer type="module" src="{{ "cart-tool-tip.bundle.js" | asset_url }}"></script>

 

Step 6

Load up your theme! You now have the same cart icon… exciting! This at least shows that it’s all working. Inspect the HTML and you should see that you now have the SVG as a child of the div#react-cart-tool-tip element.

4. Do something useful

Alright, let’s do something more interesting and set up our tool tip!

First, add reactjs-popup to our theme:

$ npm install --save reactjs-popup

reactjs-popup is the library we will be using to help with the tooltip.

Now let’s create a CartSummary component, this will be the contents of our tooltip. It will accept the number of items and cart total and display them along with a cart button. We’ll include some fallback text if the cart is empty. I used some inline styles here just to keep things as simple as possible for now.

scripts/Components/CartSummary.js


import React from "react"

export const CartSummary = ({ items, total }) => (
  <div
    style={{
      textAlign: "center"
    }}
  >
    {items > 0 ? (
      <>
        <p>Items: {items}</p>
        <p>Total: ${total / 100}</p>
        <a href={'/cart'} className="btn">
          Go to Cart
        </a>
      </>
    ) : (
      <p>There's nothing in your cart!</p>
    )}
  </div>
)

We will now update our CartToolTip to use reactjs-popup, our CartSummary, and our cart icon.

scripts/Components/CartToolTip.js


import React, { useEffect, useState } from "react"
import Popup from "reactjs-popup"
import { CartSummary } from "./CartSummary"

const cartIcon = (
  <svg
    aria-hidden="true"
    focusable="false"
    role="presentation"
    className="icon icon-cart"
    viewBox="0 0 37 40"
  >
    <path d="M36.5 34.8L33.3 8h-5.9C26.7 3.9 23 .8 18.5.8S10.3 3.9 9.6 8H3.7L.5 34.8c-.2 1.5.4 2.4.9 3 .5.5 1.4 1.2 3.1 1.2h28c1.3 0 2.4-.4 3.1-1.3.7-.7 1-1.8.9-2.9zm-18-30c2.2 0 4.1 1.4 4.7 3.2h-9.5c.7-1.9 2.6-3.2 4.8-3.2zM4.5 35l2.8-23h2.2v3c0 1.1.9 2 2 2s2-.9 2-2v-3h10v3c0 1.1.9 2 2 2s2-.9 2-2v-3h2.2l2.8 23h-28z" />
  </svg>
)

const getCart = () => {
  return fetch("/cart.js", {
    headers: {
      "Content-Type": "application/json",
      pragma: "no-cache",
      "cache-control": "no-cache"
    },
    credentials: "same-origin"
  }).then(data => data.json())
}

export const CartToolTip = () => {
  const [cart, setCart] = useState(null)
  useEffect(() => {
    getCart().then(cart => setCart(cart))
  }, [])
  return (
    <Popup
      trigger={<a href={"/cart"}>{cartIcon}</a>}
      on={["hover", "click"]}
      position="bottom right"
      contentStyle={{
        width: "min-content",
        maxWidth: "400px",
        minWidth: "200px",
        padding: "25%"
      }}
    >
      {cart ? (
        <CartSummary items={cart.item_count} total={cart.total_price} />
      ) : (
        <p>Fetching cart...</p>
      )}
    </Popup>
  )
}

What did all this do?

We are now fetching our cart using the Shopify AJAX API using our getCart function. This information is held in the state of our component, and passed to our CartSummary component. If there is no cart, we will show some loading text. The trigger property tells reactjs-popup what to display as a trigger element (our cart icon). We have wrapped this in an anchor tag to restore the ability to click on the icon to get to the cart. Our cart icon is no longer a component, it’s just stored in a variable at the top of this file for simplicity.

If you still have npm run dev and theme watch running, your theme should be all up to date! Go check it out!

If not run npm run dev and theme deploy to get the latest code onto your theme.

If you plan to deploy this, make sure to run npm run build to get the production build of our code into your theme!

Our code so far is on the branch post-1 on the repo I created.

What’s next?

If you look closely, you may notice that there are a couple bugs with this.. If we add or remove something from the cart, it gets out of sync! Or what if our cart url is non-standard (something other than /cart)?

In future posts we’ll expand this tool tip to address these issues and include the following:

– Information available from liquid templating
– A preview of cart items and quantities
– Keeping our tool tip in sync with cart events
– The ability to add or remove items from your cart
– The ability to change the quantity of items in your cart

Submit a Comment

Your email address will not be published. Required fields are marked *

Install our webapp on your iPhone! Tap and then Add to homescreen.
Share This