For my last freelance job, I decided to experiment with some no-code tools (Airtable, Make aka Integromat, Webflow and Memberstack). Thanks to that, I was able to cut the costs of development by 75%.

It's something!

However, no-code wasn't enough for some of the business requirements. For instance, I had to collect data from users and render them on charts. It would be really hard to implement it in pure Webflow! As a result, I wrote some frontend parts in React.

Below I wrote a simple tutorial where I show how to insert React components into the Webflow page and plug in MemberStack to provide a two-way data flow.

Welcome to another TODO list app tutorial 🥳

Our app will contain two React components - Table of TODO items and Modal to submit new item. Thanks to that we will learn how to render React components on normal Webflow view and on some events (like clicking button).

Demo page: https://react-webflow-memberstack.jasiek.net

Source code: https://github.com/jasiek-net/react-webflow-memberstack

React side

First, we have to create a fresh React app with create-react-app (CRA). We will use TypeScript and PrimeReact as a UI library. Execute in your terminal commands below:

mkdir todo && cd todo
yarn create react-app . --template typescript
yarn add primereact primeicons
yarn start

And let's delete all default files (we won't need them):

rm \
  public/favicon.ico \
  public/logo192.png \
  public/logo512.png \
  public/manifest.json \
  public/robots.txt \
  src/App.css \
  src/App.test.tsx \
  src/App.tsx \
  src/index.css \
  src/logo.svg \
  react-app-env.d.ts \
  src/reportWebVitals.ts \
  src/setupTests.ts \

Components

Now we will create two components that use MemberStack as an API:

// src/Table.tsx
import React, { useEffect, useState } from 'react';
import { Button } from 'primereact/button';
import { Column } from 'primereact/column';
import { DataTable } from 'primereact/datatable';

export function Table() {
  const [data, setData] = useState([]);

  useEffect(() => {
    window.$memberstackDom.getMemberJSON()
      .then(({ data }: any) => setData(data || []));
  }, []);

  const onRemove = (index: number) => {
    window.$memberstackDom
      .updateMemberJSON({ json: data.splice(index, 1) })
      .then(({ data }: any) => setData(data));
  }

  return (
    <DataTable value={data}>
      <Column header="TODO" field="todo" />
      <Column body={(_, res) => <Button
          icon="pi pi-trash"
          className="p-button-outlined"
          onClick={() => onRemove(res.rowIndex)}
        />}
      />
    </DataTable>
  );
}
// src/Modal.tsx
import React, { useState } from 'react';
import { Button } from 'primereact/button';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import { unmountModal } from '.';

export function Modal() {
  const [value, setValue] = useState('');

  const onSubmit = () => {
    window.$memberstackDom.getMemberJSON()
      .then((res: any) => {
        const todos = [ ...(res.data || []), { todo: value } ];
        window.$memberstackDom.updateMemberJSON({ json: todos })
          .then(unmountModal);
      })
  }

  return (
    <Dialog
      visible={true}
      onHide={unmountModal}
      header={'Add TODO'}>
      <InputText value={value} onChange={(e) => setValue(e.target.value)} />
      <br />
      <br />
      <Button label="Submit" style={{ width: '100%' }} onClick={onSubmit} />
    </Dialog>
  );
}

Now we will implement two global methods that will be used on Webflow side:

  • REACT_RENDER(id: string) - to render component based on passed id
  • REACT_UNMOUNT(id: string) - to unmount component based on root id

For that, we will replace src/index.tsx with the code below. Let's pay attention to the event REACTLoaded that we dispatch at the end of this script. We will listen to this event on a Webflow side to start rendering React components.

// src/index.tsx
import "primereact/resources/themes/saga-blue/theme.css";
import "primereact/resources/primereact.min.css";
import "primeicons/primeicons.css";
import ReactDOM, { Root } from 'react-dom/client';
import { Modal } from "./Modal";
import { Table } from "./Table";

export const ID_MODAL = 'root-modal';
export const ID_TABLE = 'root-table';

const roots = {} as { [id: string]: Root | null };

const components = {
  [ID_MODAL]: <Modal />,
  [ID_TABLE]: <Table />,
} as { [id: string]: JSX.Element };

window.REACT_RENDER = (id: string) => {
  const el = document.getElementById(id)!;
  roots[id] = ReactDOM.createRoot(el);
  roots[id]?.render(components[id]);
};

window.REACT_UNMOUNT = (id: string) => {
  roots[id]?.unmount();
  roots[id] = null;
}

export const unmountModal = () => window.REACT_UNMOUNT(ID_MODAL);

document.dispatchEvent(new Event('REACTLoaded'));

To avoid TypeScript errors we have to declare our new global methods and mark the presence of the MemberStack library:

// src/global.d.ts
declare global {
  interface Window {
    $memberstackDom: any
    REACT_RENDER: Function
    REACT_UNMOUNT: Function
  }
}
export {};

As a final step, we will replace public/index.html with logic that will simulate the behavior we will implement on the Webflow side, i.e.

  • MemberStack script in head tag
  • method to render Table
  • method that will open Modal on click
<!-- public/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <script data-memberstack-app="app_clfoaqm2j00tz0ukuaocdeyfq" src="https://static.memberstack.com/scripts/v1/memberstack.js" type="text/javascript"></script>
  </head>
  <body>
    <button id="button-modal">Open modal</button>
    <div id="root-modal"></div>
    <div id="root-table"></div>
    <script>
      document
        .addEventListener("REACTLoaded", () => window.REACT_RENDER('root-table'));
      document
        .getElementById('button-modal')
        .addEventListener('click', () => window.REACT_RENDER('root-modal'));
      $memberstackDom.getCurrentMember()
        .then(res => res.data ? console.log(res.data) : $memberstackDom.openModal("SIGNUP"));
    </script>
  </body>
</html>

OK, that's all on the React side! Now we have to publish our app to Netlify (or any other static server). It's important to add the environment variable PUBLIC_URL with the domain of our React app. Thanks to that, assets generated with CRA will contain references to the proper domain (in our case its https://react-webflow-memberstack.jasiek.net/)

# .env
PUBLIC_URL=https://react-webflow-memberstack.jasiek.net/

Webflow side

Let's jump to the Webflow side. Firstly we need to paste the MemberStack script to the Webflow Custom Code section. To do this we have to go to Webflow > Project Settings > Custom Code. Just below we will add a script that fetches our React app. This is a tricky part, because CRA adds a unique hash to filenames after every build, so we never know the path to our React bundle. Luckily CRA generates asset-manifest.json that contains a map of all assets, so we will use this in our script

<!-- Memberstack webflow package -->
<script data-memberstack-app="app_cl7oz7qvt01aa0wi873rdc1w3" src="https://static.memberstack.com/scripts/v1/memberstack.js" type="text/javascript"></script>
<!-- Custom React App -->
<script>
fetch('https://react-webflow-memberstack.jasiek.net/asset-manifest.json')
  .then(res => res.json())
  .then(res => {
    const script = document.createElement('script');
    script.setAttribute('type', "text/javascript");
    script.setAttribute('src', res.files['main.js']);
    document.head.appendChild(script);

    const link = document.createElement('link');
    link.setAttribute('type', 'text/css');
    link.setAttribute('rel', 'stylesheet');
    link.setAttribute('href', res.files['main.css']);
    document.head.appendChild(link);
  })
</script>

After publishing changes in Webflow we will have access to REACT_RENDER and REACT_UNMOUNT global methods. To display React component on our Webflow site, we have to add "HTML Embed" element at the desired location, add a root element with the proper id and trigger REACT_RENDER. So this is the same logic we wrote in a public/index.html on React side:

<div id="root-table"></div>
<script>
document.addEventListener('REACTLoaded', () => window.REACT_RENDER('root-table'));
</script>

To render the Modal we have to add the Webflow button with the proper id ("button-modal" in our example) and paste the snippet below the button. Thanks to that, after clicking the button, React will render our modal using the "root-modal" element.

<div id="root-modal"></div>
<script>
document
  .getElementById('button-modal')
  .addEventListener('click', () => window.REACT_RENDER('root-modal'));
</script>

And that's all! In my project I used much more complex React components like Charts or Tables of files etc. Sometimes I had to render it on the special event (like clicking or loading content) and sometimes I had to destroy them after navigating to another tab. Thanks to these two methods REACT_RENDER and REACT_UNMOUNT we achieved full control over React lifecycle and have a clean separation between Webflow and React logic.


P.S. If you are interested in similar projects or you want to build your next MVP with some no-code tools, feel free to write to me! I'm open to any form of collaboration, you can read more about me in the about section and check out some of my projects.