React NonSPA Strategies - egnomerator/misc GitHub Wiki

How to Use React in a Traditional Web Application (Non-SPA) ... but first, a lot of context

React is most commonly discussed in the context of SPAs

  • quality blogs, tutorials, and video courses are all over the place for learning how to create React SPAs
  • these sources commonly use modern JavaScript tooling
  • it's almost a certainty to see JavaScript modules, modern JavaScript language features, NPM packages, JSX, TypeScript, module bundlers, transpilers, package managers, build tool plugins, etc.
  • if you aren't already familiar with all of these modern JavaScript tools, it can be challenging to understand what React is and what the rest of it is
  • it can appear as though using all the modern JavaScript tools is required in order to use React--but this is not the case

React can be used in non-SPAs as well--but how?

  • it's somewhat challenging to find information about how to use React in the context of a non-SPA application
  • it's also challenging to find examples of using all these modern JavaScript tools in the context of a non-SPA application

"classic scripts" vs. "module scripts"

Module scripts is a key evolution of JavaScript development that fundamentally changes the design approach for web application front-ends.

classic scripts vs. module scripts

  • global scope is probably the biggest differentiating factor
  • "classic scripts" meaning scripts that are not modules (source: https://html.spec.whatwg.org/multipage/webappapis.html#classic-script)
    • everything in these script files is global by default
    • you can use various approaches such as the IIFE pattern to keep the global scope from getting cluttered
  • "module scripts" meaning scripts that are modules (example, using es6 import/export statements)
    • everything in these script files is private by default--not accessible outside the script file
    • you must use the "export" statement (if using es6 module syntax) for something to be accessible by another module
    • a module that wants to access something exported by another module must use the "import" statement (if using es6 module syntax)

here's a source to get an introduction to JavaScript modules: https://javascript.info/modules-intro

managing complexity

A major factor in modern JavaScript development is managing the complexity of an increasingly large front-end code base

  • challenges with managing many classic script files
    • keeping global scope clean--a polluted global scope can lead to lots of development maintenance challenges
    • managing interdependencies between scripts
      • numerous classic script files can lead to challenges determining the correct order to load different scripts and determining what scripts depend on other scripts
  • how module scripts and modern JavaScript tools can help
    • since everything in module scripts is private, you don't have to work so hard to keep global scope clean
    • managing interdependencies
      • there are many helpful modern JavaScript tools for handling module bundling and other build steps
      • these tools massively alleviate the challenge of managing these interdependencies
      • note: the need to learn these tools presents its own challenges and complexity as well though

Modern JavaScript Tools

The modern JavaScript ecosystem can help manage highly complex applications, but learning enough to understand this ecosystem and how to use it is a daunting challenge in itself.

new challenges

  • learning about all the new concepts, tools, and design approaches takes a lot of time and effort
    • modern JavaScript language features, JavaScript modules, module bundlers, compilers/transpilers, npm packages, package managers, etc.
  • a part of the challenge is learning the boundaries of each of these tools
    • they all fit together in what can initially appear to be a glob of one big tool set, difficult to distinguish what each tool accomplishes individually
    • the tools commonly affect each other's behavior--here's some jumbled facts about these tools to illustrate
      • webpack bundles modules, but it also can use babel for transpilation
      • babel can transpile JSX, it can also transpile TypeScript, but it doesn't perform type checking--you need tsc for that
      • babel uses various plugins that affect its behavior, you can enable Jest (a JavaScript unit testing library) to use babel as well so you get the same build behavior for unit tests
      • NPM (node package manager) provides a way to install all of these tools, and enables running scripts that execute webpack and other tools
      • each of these tools has a host of configuration options and these tools are each only one option among other tools that accomplish the same things in different ways
  • how to pick the right bundler, the right package manger, etc.? how to choose the right set of tools that work together well? how to configure all of these tools properly for a consistent build process?
    • do you need ALL of these tools? if not, how do you know when you don't need one?

Do you need modules and modern JavaScript tools?

  • no you don't need them technically--they are not a prerequisite
  • but the larger the front-end code base, the stronger the case for using these tools becomes
  • these modern tools have become prolific in the industry due to how useful they have proven to be in helping teams to manage large, complex front-end code bases

Cool! I want to use BOTH! (classic scripts and module scripts)

Why would you?

  • LEARNING: maybe you just want to learn
    • it can be a lot of fun and valuable to learn about all these modern JavaScript concepts/tools, how each one works individually, and how they can interact with each other
    • it can be valuable to know not only how a full set of modern JavaScript tools can work together, but also to know how to mix and match these tools effectively for different purposes
  • EXISTING APP: maybe you have an existing application using classic scripts and you want to convert it to using modules and modern JavaScript tools
    • this could take a lot of time but might be worth it to keep delivering value regularly while continually improving your application's maintainability
    • this might be something done incrementally over a fairly long period of time
  • DESIGN GOAL: maybe you have a specific use case where this could be useful
    • maybe you have a host application that you want some fairly small amount of classic script functionality available across the application
    • and perhaps you also would like to load different Micro Frontends for different pages or load multiple Micro Frontends to handle different parts of the same page
      • these Micro Frontends might be designed and built with modern JavaScript tools
      • then the host application could take the responsibility of loading the right Micro Frontend when needed
  • WHY NOT?
    • creative thinking is powerful and can lead to great things
    • of course generally speaking it's probably best to leave creativity that is especially experimental in nature for side projects rather than professional applications
    • thought exercise: has the modern JavaScript development ecosystem matured enough for professional, critical applications (for example, in the healthcare or banking industries), or is it still in the "creative" phase?
      • what might be some arguments that the modern JavaScript development ecosystem has matured enough? what might be some arguments that it has not?
      • does merely asking that question seem silly since it is already in use prolifically across many industries?
      • another thought: why is it so common to hear about NPM-related security breaches? how can modern JavaScript apps be protected?

how to enable classic scripts to utilize module scripts?

the problem: module scripts are private

  • module scripts are not in the global scope
  • so classic scripts cannot access module scripts
  • you must import a module to use it
  • and the act of importing a module makes that file which imported it a module itself by virtue of using the import statement
    • example:
      • module script "moduleA.js" exists; classic script "classicA.js" wants to use "moduleA.js"; classic script "classicB.js" uses globals defined in "classicA.js"
      • if "classicA.js" imports "moduleA.js" then "classicA.js" becomes a module script--meaning everything in "classicA.js" is suddenly no longer in the global scope
      • now "classicB.js" breaks since it is trying to use globals from "classicA.js" that no longer exist in the global scope
  • so classic scripts cannot import modules

solution: expose the module(s) globally

  • ideally design an API to control access
  • design your API as the top-level (or entry point) module that imports your other modules
  • expose this API globally
  • now you can access this global API from anywhere, including your classic scripts

There is an example of this in the below section "How to Use React in a Non-SPA Application - OPTION: Classic Scripts + a global Library/API exposing React Module Scripts"

How to Use React in a Non-SPA Application - OPTION: Only Classic Scripts

Classic Scripts - React CDN Script Tag

React source: https://reactjs.org/docs/add-react-to-a-website.html

Since React is a JavaScript library, you can simply use a script tag and use React globally like any other JavaScript library.

With React loaded to the page in this fashion, you can access the React API globally

  • the example below uses these React APIs
    • React.Component
    • React.createElement
    • ReactDOM.render
class MyComponent extends React.Component{
    render(){
        return React.createElement("div", null,"Hello, ", this.props.name)
    }
}

const container = document.getElementById("container");
const myReactComponent = React.createElement(MyComponent, {name: "Charlie"});

ReactDOM.render(myReactComponent, container);

// 2ND EXAMPLE - SAME AS ABOVE BUT WITH JSX ... 
//  - IMPORTANT NOTE - cannot use JSX without a transpiler which is not included in the React CDN scripts

class MyComponent extends React.Component{
    render(){
        return <div>Hello, {this.props.name}</div>
    }
}

const container = document.getElementById("container");
ReactDOM.render(<MyComponent name="Charlie"/>, container);

Is this enough?

This approach can get you pretty far, there's nothing stopping a developer from using this approach and designing complex, interactive web UIs.

Yes, but ...

BUT what if I want to use JSX?

  • browsers don't know about JSX
  • you have to load a babel CDN script that will transpile your JSX to JavaScript that the browser understands
  • this isn't a good production solution, because it presents performance issues
    • you really want this transpilation done before serving to the browser rather than make a user wait for the transpilation
  • if you want to use JSX, you'll need to transpile the JSX to JavaScript before serving it to the browser

BUT what if I want to use TypeScript?

  • browsers don't know about TypeScript
  • if you want to use TypeScript, you'll need to transpile the TypeScript to JavaScript before serving it to the browser

BUT what if I want to use NPM packages?

  • you'll need a package manager to install/uninstall/update any packages you need

But what if I want to use modern JavaScript language features the like the spread operator and destructuring which are all over the place in React examples--I have to support IE 11?

  • you'll have to find polyfills
  • modern JavaScript tooling, can be a big help in alleviating the hassle of supporting older browsers

... well does all this mean using classic scripts is no longer an option?

You can utilize ALL of the above in your web app and still design a React application with classic scripts.

But if you're using all these new things, and especially if your application is or will be large, there's a pretty strong case now for utilizing module scripts as well as a lot of the tooling available to help manage the large application

  • the larger the front-end code base, the greater the challenge in managing the complexity, and the more valuable these modern tools become

BUT what if I want to use JavaScript modules and NOT use the other tooling?

  • fairly decent support exists for JavaScript (es6) modules in modern browsers
  • you would still want a bundling step before serving to the browser so you can serve 1 bundled file rather than many files (1 per module)
  • there are also some browser support concerns if you don't transpile the module syntax to a more widely supported version of JavaScript--e.g. IE 11 support

Okay, module scripts and modern JavaScript tooling all sound awesome.

What are some optional Non-SPA approaches that use these modern tools.

How to Use React in a Non-SPA Application - OPTION: Classic Scripts Isolated from React Module Scripts

What do you mean by "isolated"?

  • classic scripts don't attempt to reference anything in the React module scripts
  • React module scripts don't attempt to reference anything in the class scripts

key aspect of this approach

  • the React module is self-contained--it has no external dependencies

tradeoffs of this approach

  • using this React module in an app is not complex
    • the app loads the bundled module script and that's it
    • the module has everything it needs to do its job, so the app just lets the module do its job
  • the app(s) that use this React module have no control over the module
    • all an app can do is load the script
    • the app cannot pass in any dependencies or arguments to customize the module's behavior

How - basically follow the most typical approach for using React

The typical use of a React component is

  • load the bundled script to the page
  • immediately on load, the React JavaScript executes--looking for its container HTML element
    • the container is an HTML element the component expects to find by the expected HTML id
  • the React component (and the full tree of all of its children) renders itself in that container HTML element
  • the user now sees the rendered UI result and can interact with it

That typical use is great if:

  • if you have an existing application with classic scripts, and you have no need for these classic scripts to interact with the React modules
  • if you want to design a completely self-contained UI component with React which will be responsible for some particular piece of your app
  • this approach could be great for Micro Frontends
  • this also is the approach to use for SPAs
    • there is a single root container HTML element
    • on page load, the entire React app renders itself in that root container HTML element
    • from that point forward, the entire web app is already loaded in the browser, and the web app handles routing

If SPAs use this approach, how is this not a SPA?

  • the non-SPA will not load the whole web app--only what's needed for the requested page
  • the non-SPA will not handle routing--the browser still handles web requests
  • for more explanation see the last section: "How to Use React in a Non-SPA Application - OPTION: all React Module Scripts"

But what about interactions between modules and classic scripts?

  • option: don't support these interactions
    • this option is about isolating the modules from the classic scripts which means no interactions
    • React modules will need to be completely self-contained--meaning that cannot have external dependencies
  • option: classic script global PubSub mechanism
    • define a global PubSub mechanism as a classic script--so your other classic scripts can access it
    • your modules and classic scripts can interact via this global PubSub mechanism
    • your modules must access the PubSub mechanism as an implicit global
    • this is not ideal
      • you typically don't want your modules using implicit globals--avoiding implicit globals is a key purpose of modules
      • modules should import what they need
  • option: utilize a public API to your components
    • in the next section, this is discussed further

How to Use React in a Non-SPA Application - OPTION: Classic Scripts + a global Library/API exposing React Module Scripts

I have a use case for ensuring my classic scripts can interact with my React modules--how can i do this?

You could design a global API to provide your classic scripts access to modules

  • design your API as a module
    • it will be the top-level module (entry-point module) that imports your React modules
    • in this API module, export an object that provides the API
  • expose this API globally
    • OPTION - webpack:
    • OPTION - manual:
      • in your entry point module, expose the API object globally by assigning it to a variable on the global scope (e.g. window or globalThis)

Now you can access this global API from anywhere, including your classic scripts.

example (part 1): a Components API with a renderShoppingCart method which renders a React ShoppingCart component

import { ShoppingCart } from "./ShoppingCart"; // this is the React component for the Shopping Cart

function renderShoppingCart(container, props) {
    ReactDOM.render(
        React.createElement(ShoppingCart, props),
        container
    )
}

// this is the Components API object
const Components = {
    renderShoppingCart: function (container, props){ renderShoppingCart(container, props); }
}

// here we are exporting the Components API
// (this is ONLY available to be imported by other modules--it's not available globally)
export default Components

example (part 2): exposing a global API object called ClientApp which contains a Components API with a renderShoppingCart method

import { default as Components } from "./components"; // this is the Components API module defined above

// MANUAL OPTION - START
// this exposes an object (containing the Components API) globally
// by assigning it to a new "ClientApp" variable on the global scope
const clientApp = {
    Components: Components
}

globalThis.ClientApp = clientApp;
// MANUAL OPTION - END

// WEBPACK OPTION
// this exports an object containing the Components API
//  - configure webpack to expose this object as a global "ClientApp" variable
//  - with webpack's "library" configuration property
export { Components }

example (part 3): (only if using webpack option) webpack.config.js output configuration for exposing the API (source: https://webpack.js.org/guides/author-libraries/#expose-the-library)

var webPackConfig = {
    /* snipped */
    entry: {
        bundle: ["path/to/ClientAppModule/file.js"]
    },
    output: {
        /* snipped */
        library: { // this is where we expose the ClientApp API
            name: "ClientApp",
            type: "var"
        }
    },
    /* snipped */
}

example (part 4): calling that API from a classic script to render the component

// this is just any JavaScript function that represents getting any dependencies the React component needs
var props = getShoppingCartDependencies();

var container = document.getElementById("containerId");

// calling the globally available API here
ClientApp.Components.renderShoppingCart(container, props);

The idea is that the Components API would provide render methods for "root" components

  • each root component is responsible for a piece/area of the web page (or even a whole web page)
  • each root component may have a tree of many child components
  • the caller of the Components API does not know about this tree of components--it just wants to render a Shopping Cart

Why "ClientApp" API containing "Components" API? Why not just expose the "Components" API globally?

  • doing exactly that is a great option too
  • one nice thing about the above approach is you could organize a few Libraries under your global "ClientApp"
    • example: ClientApp.Components, ClientApp.Library2, ClientApp.Library3, etc.

That PubSub approach mentioned earlier sounds nice for interactions between modules and classic scripts

  • when mentioned above, it was discussed as modules accessing an implicit global PubSub--is there a more ideal way to achieve this?

Yes. With the public API approach, classic scripts and modules can interact via PubSub without modules having to use implicit globals

  • the PubSub mechanism could still be a global variable declared/defined in a classic script, and a reference to it could be passed to the module via the public API
    • now the module uses this provided PubSub reference as a dependency that it requires rather than as an implicit global reference
  • the PubSub mechanism could be a module and be exposed as a part of the public API for classic scripts to use anywhere
    • example: ClientApp.PubSub
    • with this approach, modules that need the PubSub module would import it, and classic scripts would access it via the global API

Not only could the PubSub mechanism enable interaction between modules and classic scripts, but also interactions between modules (React components)

  • say you are using a Micro Frontend design, and you have a host app that's loading a main web page which loads 2 separate areas of the page as React Micro Frontends
  • then say you need these 2 separate React areas of your page to interact
  • they could do so via the PubSub mechanism
  • in thinking of this scenario, the PubSub mechanism could be defined in a classic script as a global object and a reference to it passed to each Micro Frontend

That sounds awesome! now I'm wondering about scenarios where I not only want event-based interaction, but maybe also the ability to operate on shared state

  • now we're getting into a use case where Redux (and numerous alternatives) comes in handy
  • what is Redux: https://redux.js.org/
    • Redux is pretty complex though and it's recommended to use it only for large, complex apps that need a lot of shared state
  • the general approach is to have a centralized mechanism that
    • manages state intended to be shared across the application
    • provides a way for anyone to make state changes in a controlled fashion
      • Redux uses "reducers" for this
      • an important concept is that the state is immutable
        • so any values held in the state object are not changed
        • rather a new state object is created with the updated state
    • provides a way for anyone to subscribe to state changes
      • when a certain piece of state changes any subscribers to that state are notified

If you need a way to handle centralized state such that one place in the app can change the state, and in turn other places in the app do things based on the new state, something like Redux is great.

If you just need message-based interaction, PubSub is great.

And, it could certainly make sense to have both of these mechanisms in an app depending what types of problems you need to solve.

How to Use React in a Non-SPA Application - OPTION: all React Module Scripts

This is very similar to the above "OPTION: Classic Scripts Isolated from React Module Scripts"

  • keyword being "isolated"

Treating the modules as isolated from any classic scripts would be quite similar to not having any classic scripts in the first place.

You can design your whole web app in React, exclusively using module scripts, and still not create a SPA.

A couple of key characteristics of a SPA

  • the whole app is loaded from the start
    • everything needed to run the web app is loaded on the first web request--after this there are only Ajax requests and partial page updates
  • routing is handled by the web app
    • the web app prevents the browser from handling web requests
    • the web app handles the request and updates the page as needed based on the requested route
    • the web app updates the browser history and the browser URL

So designing a whole web app using React, exclusively using module scripts, and still not creating a SPA--how could this look at a high level?

  • have an html page with one container HTML element in which you render your tree of React components (just like a SPA) ... but
  • the web app does NOT load the entire client-side code on the first page load--just what's needed for the current page that is loading
  • the web app does NOT handle routing as described above in routing for SPAs--the browser handles web requests the same way it always does
  • every non-Ajax web request results in a full page reload
  • on every page load a brand new script bundle is served to the browser, and a brand new tree of React components is rendered in the container HTML element
  • there would still be Ajax partial-page updates and highly interactive UIs all facilitated by React
⚠️ **GitHub.com Fallback** ⚠️