Dec 18, 2019

SPFx/React DEBUG vs PRODUCTION builds

Hi SharePoint and React devs,

If you have a bit of experience with SPFx and/or React, you probably know that you can usually build your bundles with at least two configurations: DEBUG and PRODUCTION. In this post I want to tackle an issue I faced a couple times and once again recently.

DEBUG != PRODUCTION build

Indeed, while, the DEBUG build is very handy while developing your app; it includes Webpack original sources (e.g. your TypeScript sources, ...), not minified JS code, etc... Bundles built for PRODUCTION contains a version of the code that is minified, uglified, optimized for better runtime performance, etc...

In some cases, the PRODUCTION and DEBUG output can be different enough that, at runtime, it might break or behaves in an unexpected way. As a reminder, in the case of SPFx, when you run gulp build or gulp bundle, by default you get a DEBUG build, in order to have a PRODUCTION build, you rather use gulp build --ship or gulp bundle --ship

On my side, when I work on a SPFx project, during development time, I usually use DEBUG build and deploy a solution package that refers to the locally served JS assets. I guess it is a pretty common scenario

Two problematic cases

Let me present 2 cases I ran into in which everything worked just fine in DEBUG build and crashes miserably in PRODUCTION build config.

Case #1 SPFx localized strings results in different transpiled JS

In one of my projects, I needed to implement a service that takes a SPFx localized string key as parameter. To avoid passing the keys as strings at risk of mispelling it, I implemented the method so you can either pass the key as string or as an arrow function that return, let's say strings.myKey.

Let's see a example of code:

import * as strings from 'myStrings';

const getLocalizedStringKey: (keyResolver: (() => string)) => string = (nameResolver: () => string) => {

    console.log("getLocalizedStringKey() - name resolver: ", nameResolver);
    const dotFormNamePattern = /function\s*\(\)\s*\{\s*return (.*)\.(.*)\s*\}/;
    const dotFormNameMatch = dotFormNamePattern.exec(nameResolver + '');

    let labelKey: string = null;
    if (dotFormNameMatch == null) {
        const arrayIndexingFormNamePattern = /return (.*)\[\"(.*)\"\]/;
        const arrayIndexingFormNameMatch = arrayIndexingFormNamePattern.exec(nameResolver + '');
        console.log("getLocalizedStringKey() - uses array indexing form");
        if (arrayIndexingFormNameMatch == null) {
            throw new Error("The keyResolver is invalid. Make sure to use '() => strings.LabelKey'");
        }

        labelKey = arrayIndexingFormNameMatch[2];
    } else {
        console.log("getLocalizedStringKey() - uses dot form");
        labelKey = dotFormNameMatch[2];
    }

    return labelKey;
};

const getKey = (labelKey: string | (() => string)) => {
    let key: string = null;
    if (!labelKey) {
        throw new Error('The labelKey argument is not set');
    }

    if (typeof labelKey === 'string') {
        key = labelKey as string;
    } else if (typeof labelKey === 'function') {
        key = getLocalizedStringKey(labelKey as (() => string));
    } else {
        key = (labelKey as any).toString();
    }

    return key;
};

export const doStuff = (labelKey: string | (() => string)) => {
    const effectiveKey = getKey(labelKey);
    // Do stuff with key here ...
};

// I can than use
doStuff(() => strings.MyKey);           // <= Compilation will succeed
//doStuff(() => strings.MyKy);          // <= Compilation would fail

// instead of
doStuff("MyKey");                       // <= Compilation will succeed and code will work fine
doStuff("Myky");                        // <= Compilation will succeed and code won't work as expected

Let's run the exact same solution in both build configurations and let's take a look at the value of the name resolver

In DEBUG build config

keyResolver_debug_build In PRODUCTION build config keyResolver_prod_build Note: see in the code exhibit above how I handled the differences using two different regex patterns.

Case #2 React child has different form in DEBUG and PRODUCTION

One of my React components is implemented to handle only those of its children that are instances of a particular class. I implemented it this way:

import * as React from 'react';

export interface IPlaceholderProps {
    key: string;
    title: string;
    zoneId: string;
    children?: React.ReactElement | React.ReactElement[];
}

export class Placeholder extends React.Component<IPlaceholderProps, {}> {}
export interface ILayoutProps { }

export class LayoutComponent extends React.Component<ILayoutProps, {}> {
    private _renderZone(zoneId: string): JSX.Element {
        const childProps = React.Children.toArray(this.props.children)
            .filter((reactChild: React.ReactElement<IPlaceholderProps>) => {
                console.log("_renderZone() - Current child type: ", reactChild.type);
                console.log("_renderZone() - Current child type name: ", (reactChild.type as any).name);
                return reactChild.type
                    // Initial implemetation => WORKED IN DEBUG BUILD
                    // && (reactChild.type as any).name == "Placeholder"
                    && reactChild.type == Placeholder
                    && (reactChild.props.zoneId == zoneId);
            })
            .map((reactChild: React.ReactElement<IPlaceholderProps>) => reactChild.props);

        return <div>
            {childProps.map((c, i) => <div key={i}>
                {c.children}
            </div>)}
        </div>;
    }

    public render() {
        return <div>
            <h1>Zone 1</h1>
            {this._renderZone("1")}
            <h1>Zone 2</h1>
            {this._renderZone("2")}
            <h1>Zone 3</h1>
            {this._renderZone("3")}
        </div>;
    }
}
  
  // Some client code
  export default class ProdVsDebugLab extends React.Component<IProdVsDebugLabProps, {}> {

  public render(): React.ReactElement<IProdVsDebugLabProps> {

    doStuff(() => strings.DescriptionFieldLabel);

    return (
      <div className={styles.prodVsDebugLab}>
        <LayoutComponent>
          <Placeholder key="ph1" zoneId="3" title="This is it !">
            <div>Hello World !</div>
          </Placeholder>
          <Placeholder key="ph2" zoneId="1" title="This is foo !">
            <div>Foo !</div>
          </Placeholder>
          <Placeholder key="ph3" zoneId="2" title="This is bar !">
            <div>Bar !</div>
          </Placeholder>
        </LayoutComponent>
      </div>
    );
  }
}

Take a closer look at lines 21,22 It worked fine in DEBUG mode where the type's name of a React child was the name of the type until I built it for PRODUCTION. In this configuration, the type's name is minified so I had to adapt my code to make it work there as well... Let's see the difference with some logging done from both build configurations

In DEBUG build config

react_child_debug

In PRODUCTION build config

react_child_prod

Conclusion

Definitely, these issues were there because of bad code I wrote I guess, I should probably have written it differently in the first place. The fact is, I couldn't realize it before it jumps at my face ! And the only way it did was to run from a bundle built for PRODUCTION. In this project, I have been working on quite big features that needed time to be stable enough before being deployed to a common testing environment.

That's the main reason why I did not build a PROD bundle sooner... Anyway, a lesson learned for me that I share with you today: Build more often PROD bundles so I can be aware of these kind of issues in advance.

Hopefully you will find that post useful !

Best regards,

Yannick

Other posts