hacking variation delivery and performance with ReactJS, Webpack and some NodeJS

This article builds upon a prior piece on UI Modules and explores a particularly useful side effect of a modular UI architecture. Understanding the preceding article on modular interfaces provides helpful context, though intermediate-to-advanced React and Webpack knowledge enables independent reading.

Modular UIs in React for Advanced Experimentation?

The article series demonstrates how different JSX and CSS can be loaded programmatically on a case-by-case basis. This approach provides significant flexibility for conducting advanced A/B tests safely and elegantly. Rather than simple HTML string replacement, entire page sections or full pages can morph dynamically based on fetched content. Performance considerations are addressed throughout.

Aim

The objective involves using dynamically fetched strings to include or select DOM and style elements obtained from APIs, cookies, or other sources. These identifiers function similarly to A/B test names within a unified paradigm.

Disclaimer: This represents R&D pursuit of specific solutions and is battle-tested, though optimization opportunities exist for particular use cases.

Structure

Webpack’s require implementation can leave certain requires unresolved, enabling this technique:

export default class ExampleComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            styleId: 'base',
            domId: 'base'
        };

        this.view = require(`./${this.state.domId}.example.jsx`).default;
    }
}

Default style and DOM identifiers are set, with initial DOM variations stored as .jsx files prefixed with corresponding IDs (base, abtest1, etc.) in the same directory.

Important Notes

  1. “Require” encompasses all code inclusion methods—require and import in JavaScript, @import and url() in CSS.

  2. Require is used here rather than import because imports must occur at the file top, whereas dynamic inclusion within classes requires require’s flexibility. Both can coexist without issues.

  3. Keep paths simple and relative during dynamic requires. Webpack cannot resolve exact files dynamically, so it recursively parses the last fully determinable directory. Overly broad paths significantly increase build size and compilation time.

this.view

The render method implementation follows:

render() {
    return this.view();
}

Content within .jsx files can be arrow functions accepting UI ID strings as parameters (more idiomatic for testing) or regular functions with UI IDs in state (more ReactJS-idiomatic). These identifiers always occupy the same state location for consistent subscription and render triggering upon changes.

import React from 'react';
export default function() {
    let styles = require(`./${this.state.styleId}.example.css`);
        return (<div className={styles.container} >
...

The style handling mirrors DOM handling. Simple state updates trigger re-renders:

this.setState({
    styleId: <new_id>
})

This sets new styleIds and triggers re-renders, redrawing affected components.

Async Require

Require operations consume time, though minimally. Since renders trigger immediately upon state changes, fine-grained lifecycle event control becomes necessary. The render method must update before component updates:

componentWillUpdate(nextProps, nextState) {
    this.view = require(`./${nextState.domId}.example.jsx`).default;
}

Triggering componentDidUpdate instead causes the first render to use old this.view values, with changes appearing only in subsequent renders. State updates and re-renders occur immediately with outdated views, and require executes asynchronously afterward, delaying view updates to the next render cycle. Concurrent style ID changes would also trigger updated DOM rendering.

Purpose and Best Use

Several questions merit addressing:

Frequency of Use: This technique isn’t essential for most applications, yet it represents a clean, React-idiomatic approach to code modification. React’s virtual DOM maintains clean component structure without duplicate content or display:none hiding of large sections.

Applications:

  • Multiple view modifications (such as charts) occupying single viewports with shared data, without requiring multiple DOM elements at page load or incurring redraw penalties elsewhere (with proper CSS)
  • A/B testing enabling layout and design changes across entire pages, typically reusing props, state, and interaction code
  • Inherited UI applications changing specific components—webpack require.context mechanisms load inherited or new UI based on identifiers, applicable to building application skins and themes

This approach proves valuable for game development given its virtual DOM compatibility and visual resource manipulation, mitigating errors from constant HTML injection, hiding, and revealing sequences.

Additional Performance Bonus for Server-Side Rendering and Code Delivery

Server-side rendering benefits from natural, performance-friendly DOM switching.

Download sizes represent the critical optimization opportunity. Multiple DOM and style versions (including inlined images and fonts) can be heavily optimized. Required components only get packaged.

Despite dynamic require resolution and Webpack’s recursive directory parsing, when A/B testing or delivering specific visual themes to users, necessary resources are known at request time.

Using Webpack’s NodeJS API

Webpack’s NodeJS API loads webpack configurations without disk operations, returning code as strings:

const webpack = require("webpack");

const compiler = webpack(require('your/webpack.config.js'));

compiler.run((err, stats) => {
  // what you wanna do with compiled code
});

On-demand webpack configurations can be generated receiving style and DOM identifiers:

const compiler = webpack(require('your/webpack.config.js')('ui_id'));

Dynamic loader regexes replace static patterns:

/\.(js|jsx)?$/

becomes:

(^(base|uiid).*\.(js|jsx)?$|index.js)\

This regex filters directories containing multiple theme and style files, parsing only files prefixed with “base” (primary) and specific UI IDs, plus index.js.

For directories containing:

  • base.example.js
  • base.example.jsx
  • uiid.example.js
  • other.example.jsx
  • index.js
  • other.example.js

Only bolded files are included in builds.

Important Note: Set content type to “text/javascript” when delivering JavaScript to clients.

Illustration Without Technical Details

Users visiting websites receive A/B test variation assignments (via cookies, for example). Subsequently, all JavaScript requests receive specialized builds containing only variation-specific data rather than all variations. This represents one aspect of the extensive control this stack provides, with each component addressing specific needs through hackable solutions enabling high flexibility for customized implementation.