React.NET icon indicating copy to clipboard operation
React.NET copied to clipboard

Loadable-Components

Open LorenDorez opened this issue 3 years ago • 5 comments

Ive read a few others who claim they got Loadable-Components to work. I can get this to work with client side just fine.

However, on SSR the component never actually renders under the client loads and calls the hydrate. I keep getting a 'document' is not defined error that ive atleast tracked down to the ChunkExtractor.

Has anyone else had success with this setup using ReactJS.net?

LorenDorez avatar May 03 '21 20:05 LorenDorez

Having this same issue. Would be awesome to hear someone got this working with an example.

edavis1993 avatar May 07 '21 15:05 edavis1993

I managed to solve this issue but last thing I need to do is remove/update the initialize JS that gets call to have it wrapped in the loadableready().

If you can bare with me a few days until I can fix that last part fix I'll post a walk through of what I have done to get this all up and working

LorenDorez avatar May 07 '21 15:05 LorenDorez

That would be appreciated. Thanks

edavis1993 avatar May 07 '21 15:05 edavis1993

OK so here are the steps I took, im going to try and get some of this integrated into a PR for this project as well.

  1. Separate your webpack bundles into a client and server. The client will need the LoadablePlugin() from Loadable-components. On the server i just have it compile out 1 file as there no sense in chunks really there. I tried to use XtronCode found here https://github.com/reactjs/React.NET/issues/731 as a sample but just did mine from scratch since our project is a bit unique.
  2. You have to setup RenderFunction on the server side code to handle the loadable-components Server stuff found here https://loadable-components.com/docs/server-side-rendering/. Again had to make adjustment for how we do things but a basic example again can be found in XtronCode sample project.
  3. The issue i ran into was i needed to not have ReactJS.net export the JS when i call the Html.ReactInitJavaScript() code. There a skipLazyInit property but its not currently exposed on the HtmlExtension today so i created my own and dplicated some of the code from the HtmlExtension and set that to true. You then will need to call .RenderJavaScript() on the returned component manually and store it somewhere to load it in your layout later.

@edavis1993 I hope this helps. Feel free to Message me directly.

LorenDorez avatar May 13 '21 18:05 LorenDorez

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const LoadableWebpackPlugin = require('@loadable/webpack-plugin')
const webpack = require("webpack")
const NodePolyfillPlugin = require("node-polyfill-webpack-plugin")

const commonConfig = {
    node: {
        global: true,
        //process: true,
        __dirname: true,
        //Buffer: true,
        //setImmediate: true
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                loader: 'babel-loader',
                exclude: [/node_modules/, /\.stories\.tsx?$/]
            },
            {
                test: /\.css$/i,
                use: [MiniCssExtractPlugin.loader, 'css-loader'],
            }
        ],
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.js'],
    }
}

const serverConfig = {
    ...commonConfig,
    entry:  ["@babel/polyfill", './Scripts/server.tsx'],
    devtool: "source-map",
    output: {
        filename: 'server.js',
        globalObject: 'this',
        path: path.resolve(__dirname, 'dist'),
        publicPath: '/',
    },

    plugins: [new webpack.optimize.LimitChunkCountPlugin({
            maxChunks: 1
        }),
        new NodePolyfillPlugin()
    ],
    resolve: {
        ...commonConfig.resolve,
        fallback: {
            //fs: require.resolve("browserify-fs"),
            // path: require.resolve("path-browserify"),
            // stream: require.resolve("stream-browserify"),
            // buffer: require.resolve("buffer/"),
            fs: false,
            //path: false,
        }
    }

}

const clientConfig = {
    ...commonConfig,
    target: "web",
    entry: './Scripts/client.tsx',
    output: {
        filename: 'client-[name].[contenthash:8].js',
        globalObject: 'this',
        path: path.resolve(__dirname, 'wwwroot/dist'),
        publicPath: '/dist/'
    },
    optimization: {
        runtimeChunk: {
            name: 'runtime', // necessary when using multiple entrypoints on the same page
        },
        splitChunks: {
            cacheGroups: {
                commons: {
                    test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
                    name: 'vendor',
                    chunks: 'all',
                },
            },
        },
    },
    plugins: [
        new MiniCssExtractPlugin(),
        new LoadableWebpackPlugin({
            filename: '../../dist/loadable-stats.json',
        }),
    ]
}
clientConfig.module.rules.push();

module.exports = [serverConfig, clientConfig];

public class LoadableFunction : RenderFunctionsBase
    {
        private readonly string nonce;

        /// <summary>
        ///     HTML style tag containing the rendered scripts.
        /// </summary>
        public string Scripts { get; private set; }

        /// <summary>
        ///     HTML style tag containing the rendered links.
        /// </summary>
        public string Links { get; private set; }

        /// <summary>
        ///     HTML style tag containing the rendered styles.
        /// </summary>
        public string Styles { get; private set; }

        private string Component { get; set; }

        public LoadableFunction(string nonce = "")
        {
            this.nonce = nonce;
        }

        /// <summary>
        ///     Implementation of PreRender.
        /// </summary>
        /// <param name="executeJs"></param>
        public override void PreRender(Func<string, string> executeJs)
        {
            string json = File.ReadAllText( @"dist/loadable-stats.json");
            executeJs(@"var extractor = new Loadable.ChunkExtractor({ stats: "+ json +@"});");
        }

        /// <summary>
        ///     Implementation of WrapComponent.
        /// </summary>
        /// <param name="componentToRender"></param>
        /// <returns></returns>
        public override string WrapComponent(string componentToRender)
        {
            this.Component = componentToRender;
            return this.Component;
        }

        /// <summary>
        ///     Implementation of PostRender.
        /// </summary>
        /// <param name="executeJs"></param>
        public override void PostRender(Func<string, string> executeJs)
        {
            executeJs($"extractor.collectChunks({this.Component})");
            this.Scripts = !string.IsNullOrEmpty(this.nonce) ? $"{executeJs("extractor.getScriptTags({ nonce: '" + this.nonce + "' })")}" : $"{executeJs("extractor.getScriptTags()")}";
            this.Styles = !string.IsNullOrEmpty(this.nonce) ? $"{executeJs("extractor.getStyleTags({ nonce: '" + this.nonce + "' })")}" : $"{executeJs("extractor.getStyleTags()")}";
            this.Links = !string.IsNullOrEmpty(this.nonce) ? $"{executeJs("extractor.getLinkTags({ nonce: '" + this.nonce + "' })")}" : $"{executeJs("extractor.getLinkTags()")}";
        }
    }

KoriSeng avatar Jun 29 '21 23:06 KoriSeng