paint-brush
Super charged DOM and style control and deliveryby@metaphorical
1,201 reads
1,201 reads

Super charged DOM and style control and delivery

by Rastko VukasinovicApril 25th, 2017
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

This article is based on <a href="https://hackernoon.com/building-modular-interfaces-a4e4076b4307" target="_blank">my other article on UI Modules</a> and represents spinoff regarding really useful feature, or rather <em>side effect, </em>of my modular UI stack. I <a href="https://hackernoon.com/tagged/recommend" target="_blank">recommend</a> reading mentioned article (linked above) first, to get scope of the stack, and then jumping into this. You can even read this without the first article, but you have to have <a href="https://hackernoon.com/tagged/medium" target="_blank">medium</a> to advanced experience in react+webpack stack.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Super charged DOM and style control and delivery
Rastko Vukasinovic HackerNoon profile picture

hacking delivery and performance with ReactJS, Webpack and some NodeJS

This article is based on my other article on UI Modules and represents spinoff regarding really useful feature, or rather side effect, of my modular UI stack. I recommend reading mentioned article (linked above) first, to get scope of the stack, and then jumping into this. You can even read this without the first article, but you have to have medium to advanced experience in react+webpack stack.

As I wrote about in above mentioned article we can programmatically load different JSX and CSS on case-to case basis. This means a lot for any kind of web UI, giving high flexibility and ability to do advanced AB tests easily and safely. Most of all, this is simple and most elegant way to morph pages on the fly based on dynamically loaded stuff, and I am talking about big parts or full pages, not just simple html string replacement. There is also spect of performance, which I’ll tackle as well.

Aim

You want ability to use dynamically fetched string to include/select stuff. We can treat these like DOM id and style id, which we get from API, cookie or in some other way. To stick with single paradigm for the moment, you can look at these as AB test names.

Disclaimer: Like any technique I am writing about, this is result of R&D and pursuit for certain solution, it is example illustrating solution. I am not describing, nor I am focusing on best practices. In lots of cases, this is how you do stuff, but you can optimize it further for your use case for sure. On the other hand, I assure you it is battle tested and that it will do the thing properly and flawlessly.

Structure

Core of this idea is ability of Webpack’s require implementation to leave some requires unresolved.

We can:

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;

In this code I am setting default style and DOM ids and require-ing the initial (base) DOM. I keep all the DOM variation in .jsx files starting with DOM id string (e.g. base, abtest1 …) in the same directory.

Couple notes before we continue:

  1. When I write require, I actually mean any kind of code include — require and import in JS, @import, url() etc in CSS…
  2. I am using require in example above because imports need to be done at the top, and I want to hack my code inclusion in class, to be able to do it dynamically. Yes, you can use require and import in the same file without creating a rupture in time-space continuum… Magic!
  3. Make sure to make your paths simple and relative when doing this — when doing dynamic requires, Webpack can not resolve exact file, so it makes sure you can get what you need by recursively parsing last fully determinable directory the dynamically generated file path points to, so, if you dynamically create broader path, you can significantly increase your build size and time .

this.view

Looking at the above code, you can see that I am setting this.view… This is render method, and here is the snippet from the same class:



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

Depending on your personal taste for writing this, and nothing else, what is in the .jsx file can either be a arrow function accepting your ui id string as param (which is more idiomatic approach testing wise) or be regular function() and ui id is in the state (lot more ReactJS idiom). You always put these ids at the same place in state so everything can subscribe to it, and render gets triggered when it is changed.

Change is immediate and only html in this component is the one shown

This also means that components using this technique sometimes (more often then regular) need shouldComponentUpdate lifecycle event.

So, here it is:

import React from 'react';

export default function() {

let styles = **require**(\`./${this.state.styleId}.example.css\`);

    **return** (<div className={styles.container} >

...

Most important piece is identical to the way we handle DOM…

When written like this it gets packed in the way that ensures you dynamic switch, so doing simple thing like:



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

will set new styleId, trigger re-render (because that is how React rolls :)), which will then just redraw/repaint changed.

Important thing about require — it takes some time

That time is minimal, but render is triggered as soon as state changes. This causes DOM update to need little finer control on the side of lifecycle events of the component. Render method needs to be updated before component updates:



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

If you trigger componentDidUpdate, first render would not contain DOM change, second one will contain change from the first etc, because state gets updated and re-render is done imediately with old this.view, and then require is done so this.view waits for next re-render. If we change something else in the process (like style id) it would also render updated dom.

Looking at gif above you can see how state change triggers render immediately, but DOM is late due having to require new render function asynchronously and render is done using old one before new one is loaded.

Purpose and best use

There are probably couple questions to answer:

We do not need this often do we? No, you do not, it is just really clean and smooth technique to change something in the code, I consider it very React way to do it. With React’s virtual DOM, you get ability to always keep your DOM clean, and never to have duplicate content or display:none to hide big chunks.

What is this used for? You can just play around with it :D On the other hand, I used it to:

  • Put several view mods (charts to be exact), in the same viewport (component) using same data, without having to have multiple things in the DOM on page load, and especially without having to have redraw penalties on other parts of the page (assuming css is done properly).
  • Do AB tests - this concept can be used to change and reintroduce any layout and any design of the page. Usually it is ideal if props, state and interaction code get fully reused
  • [bonus]Inherit UI app to app and just change some parts — for this, I developed mechanism that uses webpack require.context to load either inherited app’s ui or use your new ui based on ids. This has broad application in building application skins, themes etc

I find it really useful for game development as well (did not use it for that), since it is very virtual DOM friendly visual resource manipulation, which vastly helps neutralizing potential errors in the code caused by constantly injecting, hiding and revealing HTML code in sequence of external touches. This is also part of the series on ui modules, and it is ideal structure for game development.

Additional performance bonus for server side rendering and code delivery

Following section is not detailed enough, it is just rough overview of what can be done and contains hints and small snippets that will be highly useful to people with some strong experience in the field

Handling DOM switches in this way is natural and, actually, very performance friendly when rendering on server.

But who cares about server performance for this small stuff… But…

What we do care about is download sizes, and we can highly optimize these when working with multiple versions of DOM and styles that includes inlining images and fonts and what not. We can pack just needed stuff.

Even tho requires are dynamically resolved in the code, so webpack recursively parses whole directory, if we are doing AB test or we are delivering chosen visual theme to certain user, we know what we need to deliver at time the resource is requested.

Using Webpack’s NodeJS API

Webpack’s NodeJS API is basically loading your webpack config and using it to parse your code, it is just not saving to disk, but returning code to you as a string.

const webpack = require("webpack");const compiler = webpack(require('your/webpack.config.js'));compiler.run((err, stats) => {  // what you wanna do with compiled code});

So, this is how we can optimize build sizes on the fly:

We should generate webpack config on demand, so we create method receiving style and DOM id (or ui Id) and returning this special config.

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

We dynamically inject special loader regexes, so, instead of:

/\.(js|jsx)?$/

we should do something like:

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

Pretty arbitrary regex above should do following for you - in directory that has lots of files dedicated to different themes and styles, generating different DOMs, parse only ones that are prefixed with base (main one and ) and uiid plus index.js which is important for module itself.

So, if we got directory with:






base.example.jsbase.example.jsxuiid.example.jsother.example.jsx**index.js**other.example.js

one file that is not boded will not be included in the build.

Important note: Do not forget to set content type to “text/javascript” when sending js to client.

Finally, to illustrate without technical details

Your user enters the website, gets AB test variation attached to its user data (cookie for instance), from that moment on, when ever user pulls js from server, it will receive special build that contains just data required for his variation, not all the variations like if we did not know what file will be included.

This was just a glance at kind of super control this kind of stack gives you. Each of its pieces is covering one aspect with wide variety of hackable solutions to provide you with very high flexibility to make it all just right for your particular use case.