reactjs / React.NET

.NET library for JSX compilation and server-side rendering of React components
https://reactjs.net/
MIT License
2.28k stars 940 forks source link

Loadable-Components #1250

Open LorenDorez opened 3 years ago

LorenDorez commented 3 years ago

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?

edavis1993 commented 3 years ago

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

LorenDorez commented 3 years ago

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

edavis1993 commented 3 years ago

That would be appreciated. Thanks

LorenDorez commented 3 years ago

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.

KoriSeng commented 3 years ago
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()")}";
        }
    }