Recently I’ve had to build out some custom functionality in Shopify and my framework of choice is React. I’ve seen a few posts floating around on the intarwebs explaining that Shopify’s Liquid templating language and React just don’t mix. That’s hogwash.
Not only can Liquid + React play nicely together, with a little finessing you can use Liquid’s build-at-runtime feature to your advantage to pass dynamic data from Liquid → React on page load. Here’s how.
For the purposes of this tutorial, I’m assuming you are creating your app using Create-React-App and not ejecting.
Mounting Steps
Mounting a React app within a Shopify theme is the generally the same process as mounting in any HTML document with a few important differences so let’s go through step-by-step.
1. Build your React project.
Once you have developed locally you’ll get to a stage where you’ll want to mount your app into your Shopify theme to do some initial testing. To do that, you’ll need to build your project.
Hold up! First we need to customize our package.json
file to copy the build files to our theme. If you use Slate or Quickshot or another CI/CD pipeline for your Shopify development, this step ensures that the files are copied to your /assets
folder so they will be available to your theme at runtime. The exact steps for your particular build pipeline may differ, but here’s mine to get you started.
To make things much, much easier, install the copyfiles
npm package globally before the next step.
npm install copyfiles -g
I’m using Yarn so note my build
command below which copies the React built files to the local theme directory I specify — in this case /assets
:
"scripts": {
"start": "BROWSER='chrome' react-scripts start",
"build": "react-scripts build && copyfiles -f \"build/static/**/*.{js,css,map}\" /full_path_to_your_theme/theme/assets && echo 'build files copied'",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
If you’re using TypeScript, you can add .ts
and .tsx
file extensions above.
OK, with our build
command customized, we are ready to build.
Run:
yarn build
or
npm run build
If it is the first build, a /build
directory will be created at the root level of your React project and using our copyfiles
package and command during the build step, the React files should be in assets file in our theme.
2. Create the mount point in your theme.
By default, React apps build with Create-React-App (CRA) use a div with id="root"
as the mount point. Let’s create that in our Shopify theme.
I usually add something like this:
<div class="app-outer">
<div class="app-inner">
<div id="root"></div> <!-- mount point here -->
</div>
</div>
^^ The above can be in a /template
, /section
, or /snippet
as is appropriate for your theme and app.
One we have our mount point in our Shopify theme, we’re ready to move on to the next step.
3. Extract the built file + resources
Going back to our local React project, the /build
directory will be an index.html
file. Open this file in your editor and you will see something like this:
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>React App</title><link href="/static/css/main.0e32dcb3.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><div id="root-2"></div><script>!function(e){function r(r){for(var n,l,a=r[0],p=r[1],f=r[2],c=0,s=[];c<a.length;c++)l=a[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in p)Object.prototype.hasOwnProperty.call(p,n)&&(e[n]=p[n]);for(i&&i(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var p=t[a];0!==o[p]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var a=this["webpackJsonpma-your-app"]=this["webpackJsonpma-your-app"]||[],p=a.push.bind(a);a.push=r,a=a.slice();for(var f=0;f<a.length;f++)r(a[f]);var i=p;t()}([])</script><script src="/static/js/2.76709c3a.chunk.js"></script><script src="/static/js/main.cf461ff5.chunk.js"></script></body></html>
This entire chunk is a fully encapsulated React app with relative links to the CSS and JS files it needs. If we were running a single page app, we could upload the files to a web server and we’d be done. But we’re not doing that.
We’ve created our mount point in Step 2, above which is already on a web page in Shopify so we can remove the entire HTML section at the top of this file except for the link to the stylesheet. We can also remove the </body>
and </html>
tags at the end of the chunk. Remove any html tags and place the css link and script tags on their own lines.
After doing that, you will be left with something like this:
<link href="/static/css/main.0e32dcb3.chunk.css" rel="stylesheet">
<script>!function(e){function r(r){for(var n,l,a=r[0],p=r[1],f=r[2],c=0,s=[];c<a.length;c++)l=a[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in p)Object.prototype.hasOwnProperty.call(p,n)&&(e[n]=p[n]);for(i&&i(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var p=t[a];0!==o[p]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var a=this["webpackJsonpma-your-app"]=this["webpackJsonpma-your-app"]||[],p=a.push.bind(a);a.push=r,a=a.slice();for(var f=0;f<a.length;f++)r(a[f]);var i=p;t()}([])</script>
<script src="/static/js/2.76709c3a.chunk.js"></script>
<script src="/static/js/main.cf461ff5.chunk.js"></script>
We’re almost done!
4. Add the edited code to our theme.
We need to add our React chunk from Step 3 above below the mount point we created in Step 2. Thus, your file should look something like this:
<!-- code above -->
<link href="/static/css/main.0e32dcb3.chunk.css" rel="stylesheet">
<div class="app-outer">
<div class="app-inner">
<div id="root"></div> <!-- mount point here -->
</div>
</div>
<script>!function(e){function r(r){for(var n,l,a=r[0],p=r[1],f=r[2],c=0,s=[];c<a.length;c++)l=a[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in p)Object.prototype.hasOwnProperty.call(p,n)&&(e[n]=p[n]);for(i&&i(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var p=t[a];0!==o[p]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var a=this["webpackJsonpma-your-app"]=this["webpackJsonpma-your-app"]||[],p=a.push.bind(a);a.push=r,a=a.slice();for(var f=0;f<a.length;f++)r(a[f]);var i=p;t()}([])</script>
<script src="/static/js/2.76709c3a.chunk.js"></script>
<script src="/static/js/main.cf461ff5.chunk.js"></script>
^^ Notice the link to the css file is above the mounting point and the rest is below.
We just need to update our links to work with our theme and then we’re done!
5. Update the resource links.
The link to the css file needs to be edited to point to our /assets
file so let’s update it to this:
<link href="{{ 'main.0e32dcb3.chunk.css' | asset_url }}" rel="stylesheet">
Note that the numbers in your file will be different and don’t forget the inner single quotes around the filename.
Likewise, we need to update the links to the JavaScript files:
<script src="{{ '2.76709c3a.chunk.js' | asset_url }}"></script>
<script src="{{ 'main.cf461ff5.chunk.js' | asset_url }}"></script>
The entire React portion of the file should look something like this:
<link href="{{ 'main.0e32dcb3.chunk.css' | asset_url }}" rel="stylesheet">
<div class="app-outer">
<div class="app-inner">
<div id="root"></div> <!-- mount point here -->
</div>
</div>
<script>!function(e){function r(r){for(var n,l,a=r[0],p=r[1],f=r[2],c=0,s=[];c<a.length;c++)l=a[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in p)Object.prototype.hasOwnProperty.call(p,n)&&(e[n]=p[n]);for(i&&i(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var p=t[a];0!==o[p]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var a=this["webpackJsonpma-your-app"]=this["webpackJsonpma-your-app"]||[],p=a.push.bind(a);a.push=r,a=a.slice();for(var f=0;f<a.length;f++)r(a[f]);var i=p;t()}([])</script>
<script src="{{ '2.76709c3a.chunk.js' | asset_url }}"></script>
<script src="{{ 'main.cf461ff5.chunk.js' | asset_url }}"></script>
As long as the built React files are in your /assets
directory, your React app should be mounted and loaded in your Shopify theme after a reload!
Important Note
Each time your change your file locally, you will have to do a new build and change the file names in your theme as CRA will create new ones each build. CRA will only change the file if it has actually changed so often it is only a single name change per build.
Passing Data from Liquid → React
You might need to pass data from your Shopify Liquid theme to your React app. While I wouldn’t recommend doing this a lot, it is possible.
There are two main ways to do this:
- Set a JavaScript variable in Liquid
- Use jQuery in React.
Option1: Setting a JavaScript Variable in Liquid
This way is pretty straightforward. Set the variable in your Liquid template as on the second line here. Anything that is available on the template you can save to a variable for example the collection
object on Collection pages or the product
object on Product pages.
<script>
const productId = {{ product.id | json }}
</script>
<link href="{{ 'main.0e32dcb3.chunk.css' | asset_url }}" rel="stylesheet">
<div class="app-outer">
<div class="app-inner">
<div id="root"></div> <!-- mount point here -->
</div>
</div>
<!-- rest of React code -->
The caveat for this is you have to perform a check in your React app to make sure the variable has a value and is available but since Liquid is server side, it will be available once the page is built and your React app can use it.
Option 2: Use jQuery in React
Wait, I know what you are thinking: jQuery is imperative and React is declarative and never the twain shall meet. Welp, sometimes rules are meant to be broken. You can import jQuery into React and sometimes it is the easiest way to accomplish something like this.
In one case, I needed to grab the currently selected swatch from my Liquid template and use it in my React app. Sure I could use document.querySelector()
but why? A little smidge of jQuery is so much easier in this case.
Here is a quick-and-dirty example:
import { useState, useEffect } from 'react'
import $ from 'jquery'
const Component = () => {
const [mounted, setMounted] = useState(false)
const [productId, setProductId] = useState('')
useEffect(() => {
waitForElementToDisplay(
'.ma-product',
function () {
console.log('product mounted')
setMounted(true)
},
100,
5000
)
if (mounted) {
setProductId($('.ma-product').data('product-id')) // jQuery used here
} else {
console.log('product not mounted')
setProductId('6698680483902') // default
}
}, [])
return (
// rendering jsx stuffs here
)
}
export default Component
function waitForElementToDisplay(selector, callback, checkFrequencyInMs, timeoutInMs) {
var startTimeInMs = Date.now();
(function loopSearch() {
if (document.querySelector(selector) != null) {
callback();
return;
}
else {
setTimeout(function () {
if (timeoutInMs && Date.now() - startTimeInMs > timeoutInMs)
return;
loopSearch();
}, checkFrequencyInMs);
}
})();
}
In the above snippet, we’re using the waitForElementToDisplay
function to check to see whether our product element has mounted checking every 100 ms for 5000 ms. Once it has mounted, we use jQuery to grab the data attribute with the productId
that we need and set it to some state so it is available to our component.
Which approach you use to pass data between your Liquid Shopify theme and your mounted React app will depend on your particular app’s functionality but don’t be afraid to break the rules sometimes — it just might be the best and quickest way to get the job done.
In Conclusion
Hopefully I’ve provided some illumination on your path to working with mounted React apps in your Shopify Liquid theme. Not only can Shopify and React work well together, you can event pass data from Shopify to your React app at runtime.
If you have any questions about these tips, hit me Twitter @joshuaiz