2023-01-19

Using React 18 hydrateRoot or other design patterns for rendering slow component(s)

I have a rather large web application that I'm upgrading from React 16 to React 18 and overhauling completely, it uses .NET 6. Currently, it's not using any of the hydration SSR techniques. I've looked into this for quite some time now, but am not finding any solid examples to help me through this technique. I thought this might be a good thread to make for future developers who may be doing the same. Basically, I have pages with a collection of 'widgets' (React Components), some of the components require more data to load than others, some don't need any data. The issue is that all the data is loaded from the C# controller via API calls and put into a big model/object, which contains the data for each individual widget on the page, so no widgets are loaded until all the data is gathered and passed in from the controller. Give examples of how to make this work best, using available React techniques, I'm sure there is probably a couple of great designs for this. I'd like to get things set-up so that I can at least do one of the two techniques, the first being the preferred method:

  1. Load data from the C# Controller to each individual React Component directly as the data is gathered, having the individual widget render at that time. Then, when the data is ready for another widget, it's passed into the particular component and rendered on screen. This will make it so that no component depends on the data needing to be loaded for other components for it to load.
    • Something I want to avoid: Some of the 'pages' currently use Fetch (GET/POST) calls from the JSX files to load data from the API, rather than from the controller when the page link is clicked. This has caused issues in the past from having too many fetch calls going at the same time from different widgets loading (collisions/etc), which was just causing them to crash a lot, so I've been modifying all of these widget's data to be loaded from the controller beforehand.
  2. Having a 'loading' screen of sorts show up or even just the widgets' container html, appear until all of the data is passed from the controller via the data model.

How I currently have everything set-up:

-> From home page, user clicks on link to go to their Account profile at 'Account\Index\userId'

-> Example of current AccountController.cs Index call:

    [HttpGet]
    public async Task<ActionResult> Index(int? userId) {
        var model = new AccountViewModelContract();

        model.NameData = await ApiClient1.GetNameViewModelAsync(userId);
        model.AccountSecurityData = await ApiClient2.GetAccountSecurityViewModelAsync(userId);
        model.EmailData = await ApiClient2.GetEmailViewModelAsync(userId);
        model.PhoneData = await ApiClient2.GetPhoneViewModelAsync(userId);
        model.AddressData = await ApiClient3.GetAddressViewModelAsync(userId);
        model.LoadedFromController = true;
        return View(model);
    }

-> Example of current Account's View, Index.cshtml

@using App.Models
@model AccountViewModelContract

<input id="accountPageData" type='hidden' value="@System.Text.Json.JsonSerializer.Serialize(Model)" />
<div id="accountDash" style="height:100%;"> </div>

@section ReactBundle{
    <script src="@Url.Content("~/Dist/AccountDashboard.bundle.js")" type="text/javascript"></script>
}

-> Example of current AccountDashboard.jsx:

import React from 'react';
import { createRoot } from 'react-dom/client';
import BootstrapFooter from 'Shared/BootstrapFooter';
import AccountSecurity from './AccountSecurity/AccountSecurity';
import Address from 'Account/Address/Address';
import EmailSettings from 'Account/Email/EmailSettings';
import Name from 'Account/Name/Name';
import Phone from 'Account/Phone/Phone';
import PropTypes from 'prop-types';
import BootstrapHeaderMenu from '../Shared/BootstrapHeaderMenu';

class AccountDashboard extends React.Component {
    constructor(props) {
        super(props);
        const data = JSON.parse(props.accountPageData);
        this.state = { data };
    }

    render() {
        const { data } = this.state;

        return (
            <div className="container">
                <div className="row">
                    <BootstrapHeaderMenu/>
                </div>
                <div>My Account</div>
                <div className="row">
                    <div className="col">
                        <Name value={data.NameData} />
                    </div>
                    <div className="col">
                        <Phone value={data.PhoneData} />
                    </div>
                    <div className="col">
                        <Address value={data.AddressData} />
                    </div>
                    <div className="col">
                        <AccountSecurity value={data.AccountSecurityData} />
                    </div>
                    <div className="col">
                        <EmailSettings value={data.EmailData} />
                    </div>
                </div>
                <div className="row">
                    <BootstrapFooter/>
                </div>
            </div>
        );
    }
}

AccountDashboard.propTypes = {
    accountPageData: PropTypes.string.isRequired
};

createRoot(document.getElementById('accountDash')).render(<AccountDashboard accountPageData={document.getElementById('accountPageData').value} />);

-> Example of one of the Widget's (Name.jsx), code condensed to not complicate things

import React from 'react';
import BlueCard from 'Controls/BlueCard';
import LoadingEllipsis from 'Controls/LoadingEllipsis';

class Name extends React.Component {
    constructor(props) {
        super(props);
        const data = props.value;

        this.state = {
            isLoaded: false,
            .....
            loadedFromController: props.value.LoadedFromController,
        };

        if (props.value.LoadedFromController) {
            ...set more state data  
        } 
    }

    componentDidMount() {
        if (!this.state.isLoaded && !this.state.loadedFromController) {
            ..//go to server to get data, widget sometimes loads this way in odd certain cases, pay no attention
        } else {
            this.setState({
                isLoaded: true
            });
    }

    render() {
        let content = <LoadingEllipsis>Loading Name</LoadingEllipsis>;
        const { ... } = this.state;
        if (isLoaded === true) {
            content = (<div>....fill content from state data</div>);
        }
        return (<BlueCard icon="../../Content/Images/Icon1.png" title="Name" banner={banner} content={content} />);
    }
}

export default Name;

What are some methods, techniques, or design patterns for accomplishing these goals and give detailed examples of how it can be done and why it works better than alternatives. Use the above example class names in your examples.



No comments:

Post a Comment