Skip to content

🏖 Portal - #3767

Closed
oliverlloyd wants to merge 5 commits into
mainfrom
oliver/portal
Closed

oliverlloyd wants to merge 5 commits into
mainfrom
oliver/portal

Conversation

@oliverlloyd

@oliverlloyd oliverlloyd commented Dec 17, 2021 •

Copy link
Copy Markdown
Contributor

What does this change?

This PR introduces a new way to add portals to a page. A 'Portal' is the name given to any content added to the page on the client. This is similar concept to an island, but with the difference that the content is not server side rendered.

Why?

The primary goal for this work is to improve the developer experience of teams working on DCR. Adding content client side previously required an understanding of the wider app architecture and edits to several files. This is now reduce to one api in one location.

In addition, we're making it easier to move away from the App.tsx pattern as discussed here.

Only send the data required

By serialising the props for each component alongside its declaration on the server and not in a global window object, we ensure we only send the data that is needed.

Lazy insertion

Using the same api that was introduced for lazy hydration, we can now download and render components when visible, deferring content below the fold until scrolled into view. We can also tell the browser to render less important content when it is idle, preventing the thread from being blocked.

LegacyPortal

I wanted to use the Portal namespace and also didn't want two things with similar names confusing people so I've sort of deprecated the old component by calling it LegacyPortal and stolen it's name

Usage

// Server
<Portal
    componentName="Onwards"
    when="visible"
    props={{
        onwardsUrl: CAPI.onwardUrl,
        format,
    }}
    placeholderHeight={400}
/>

This is the only code developers need to write to have their content inserted on the client*

*You also need to rename the component being used to include 'importable'. For example, Onwards.importable.tsx.

interface Props {
componentName: string;
when?: 'immediate' | 'idle' | 'visible';
props?: any;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way I can use componentName to get the expected type and use that here? This seems pretty wild but would really help improve the dev ex!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean read it from the existing files? Yes, but you’d have to have some sort of process that generates a declaration file from reading the filesystem, I think.

That’s what the interview tool does in build.ts

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a good idea? It's hard for me to estimate the trade off between not having things typed - which adds friction when inserting a new Portal - and implementing a special solution to make typing work - which might be lots of work and potentially brittle / hard to maintain?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think it’s a lot of work, and that file has another added benefits: It’s a live document containing all the current portals. I’m happy to set half-an-hour aside to have a crack at this.

The question that remains is how the generation steps gets called, but I’m sure there are precendents… make portals?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I'm not 100% following your intention but would what you're thinking of solve the dev ex problem? That is, I want to insert a new Portal and type

<Portal componentName="MyThing" ...

And then when I go to enter the props they are already typed as needed?

@github-actions

github-actions Bot commented Dec 17, 2021 •

Copy link
Copy Markdown

Size Change: +32 B (0%)

Total Size: 3.16 MB

Filename Size Change
dotcom-rendering/dist/frontend.server.js 2.48 MB +17 B (0%)
dotcom-rendering/dist/react.js 139 kB +10 B (0%)
dotcom-rendering/dist/react.legacy.js 145 kB +5 B (0%)
ℹ️ View Unchanged
Filename Size
dotcom-rendering/dist/101.js 21.1 kB
dotcom-rendering/dist/101.legacy.js 21.1 kB
dotcom-rendering/dist/195.js 1.11 kB
dotcom-rendering/dist/195.legacy.js 1.23 kB
dotcom-rendering/dist/atomIframe.js 1.87 kB
dotcom-rendering/dist/atomIframe.legacy.js 2.13 kB
dotcom-rendering/dist/bootCmp.js 7.5 kB
dotcom-rendering/dist/bootCmp.legacy.js 11.1 kB
dotcom-rendering/dist/braze-web-sdk-core.js 36.1 kB
dotcom-rendering/dist/braze-web-sdk-core.legacy.js 36.1 kB
dotcom-rendering/dist/cmp.js 7.78 kB
dotcom-rendering/dist/coreVitals.js 4.03 kB
dotcom-rendering/dist/coreVitals.legacy.js 4.31 kB
dotcom-rendering/dist/dynamicImport.js 2.99 kB
dotcom-rendering/dist/dynamicImport.legacy.js 3.27 kB
dotcom-rendering/dist/EditionDropdown.js 685 B
dotcom-rendering/dist/EditionDropdown.legacy.js 694 B
dotcom-rendering/dist/elements-CalloutBlockComponent.js 4.15 kB
dotcom-rendering/dist/elements-CalloutBlockComponent.legacy.js 4.46 kB
dotcom-rendering/dist/elements-DocumentBlockComponent.js 571 B
dotcom-rendering/dist/elements-DocumentBlockComponent.legacy.js 602 B
dotcom-rendering/dist/elements-InstagramBlockComponent.js 434 B
dotcom-rendering/dist/elements-InstagramBlockComponent.legacy.js 452 B
dotcom-rendering/dist/elements-InteractiveBlockComponent.js 2.96 kB
dotcom-rendering/dist/elements-InteractiveBlockComponent.legacy.js 3.1 kB
dotcom-rendering/dist/elements-InteractiveContentsBlockComponent.js 1.88 kB
dotcom-rendering/dist/elements-InteractiveContentsBlockComponent.legacy.js 1.95 kB
dotcom-rendering/dist/elements-MapEmbedBlockComponent.js 1.88 kB
dotcom-rendering/dist/elements-MapEmbedBlockComponent.legacy.js 1.94 kB
dotcom-rendering/dist/elements-RichLinkComponent.js 3.26 kB
dotcom-rendering/dist/elements-RichLinkComponent.legacy.js 3.3 kB
dotcom-rendering/dist/elements-SpotifyBlockComponent.js 1.8 kB
dotcom-rendering/dist/elements-SpotifyBlockComponent.legacy.js 1.86 kB
dotcom-rendering/dist/elements-VideoFacebookBlockComponent.js 1.88 kB
dotcom-rendering/dist/elements-VideoFacebookBlockComponent.legacy.js 1.94 kB
dotcom-rendering/dist/elements-VineBlockComponent.js 579 B
dotcom-rendering/dist/elements-VineBlockComponent.legacy.js 594 B
dotcom-rendering/dist/elements-YoutubeBlockComponent.js 2.57 kB
dotcom-rendering/dist/elements-YoutubeBlockComponent.legacy.js 2.7 kB
dotcom-rendering/dist/embedIframe.js 1.88 kB
dotcom-rendering/dist/embedIframe.legacy.js 2.13 kB
dotcom-rendering/dist/ga.js 3.88 kB
dotcom-rendering/dist/ga.legacy.js 4.13 kB
dotcom-rendering/dist/GetMatchStats.js 3.29 kB
dotcom-rendering/dist/GetMatchStats.legacy.js 3.36 kB
dotcom-rendering/dist/guardian-braze-components-banner.js 9.78 kB
dotcom-rendering/dist/guardian-braze-components-banner.legacy.js 9.79 kB
dotcom-rendering/dist/guardian-braze-components-end-of-article.js 6.56 kB
dotcom-rendering/dist/guardian-braze-components-end-of-article.legacy.js 6.57 kB
dotcom-rendering/dist/hydration.js 5.84 kB
dotcom-rendering/dist/hydration.legacy.js 6.46 kB
dotcom-rendering/dist/MostViewedFooterData.js 6.27 kB
dotcom-rendering/dist/MostViewedFooterData.legacy.js 6.36 kB
dotcom-rendering/dist/MostViewedRightWrapper.js 3.89 kB
dotcom-rendering/dist/MostViewedRightWrapper.legacy.js 4.07 kB
dotcom-rendering/dist/newsletterEmbedIframe.js 1.83 kB
dotcom-rendering/dist/newsletterEmbedIframe.legacy.js 2.08 kB
dotcom-rendering/dist/OnwardsLower.js 9.62 kB
dotcom-rendering/dist/OnwardsLower.legacy.js 9.86 kB
dotcom-rendering/dist/OnwardsUpper.js 14 kB
dotcom-rendering/dist/OnwardsUpper.legacy.js 14.3 kB
dotcom-rendering/dist/ophan.js 7.18 kB
dotcom-rendering/dist/ophan.legacy.js 7.37 kB
dotcom-rendering/dist/portals.js 5.83 kB
dotcom-rendering/dist/portals.legacy.js 6.46 kB
dotcom-rendering/dist/relativeTime.js 2.41 kB
dotcom-rendering/dist/relativeTime.legacy.js 2.67 kB
dotcom-rendering/dist/sentry.js 677 B
dotcom-rendering/dist/sentry.legacy.js 687 B
dotcom-rendering/dist/sentryLoader.js 4.74 kB
dotcom-rendering/dist/sentryLoader.legacy.js 7.69 kB
dotcom-rendering/dist/shimport.js 2.75 kB
dotcom-rendering/dist/shimport.legacy.js 2.76 kB
dotcom-rendering/dist/SignInGateMain.js 1.82 kB
dotcom-rendering/dist/SignInGateMain.legacy.js 1.85 kB

compressed-size-action

[k: `${string}Variant`]: "variant";
[k: `${string}Control`]: "control";
[k: `${string}Variant`]: 'variant';
[k: `${string}Control`]: 'control';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prettier 🤷

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it, though? 😆

name: string;
when?: 'immediate' | 'idle' | 'visible';
props: any;
children: React.ReactNode;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

children is for when we use a Placeholder

* We expect the element to always be a `gu-*` custom element
*
* @param marker : The html element that we want to read the name attribute from;
* @param element : The html element that we want to read the name attribute from;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Respecting a comment made by @sndrs over in libs for consistency

import { getName } from './getName';
import { getProps } from './getProps';
import { getName } from '../getName';
import { getProps } from '../getProps';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved these to the common root folder so they can be shared. We can consider a different folder structure, this just worked for now

// that will not cause the child component being passed in to be mounted
// again. Instead it is simply rerendered
// https://github.com/preactjs/preact/blob/df748d106fb78fbd46d14563b4712f921ccf0300/compat/src/portals.js
export const LegacyPortal = ({ rootId, children }: Props) => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file was renamed without edits

* @param placeholderHeight - Height in pixels. If provided, a Placeholder is server rendered
*
*/
export const Portal = ({

@SiAdcock SiAdcock Dec 21, 2021 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Naming Things is Hard question: is it confusing to repurpose the name Portal, which is a first-class concept in (P)react that does something similar-ish but not really?

Without fully understanding the purpose of this pattern, would a more descriptive name be something like RenderOnClient? As in:

<RenderOnClient
    componentName="Onwards"
    when="visible"
    props={{
        onwardsUrl: CAPI.onwardUrl,
        format,
    }}
    placeholderHeight={400}
/>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess what this component is doing is marking a place in the server side rendered content and saying, put some html here on the client using these attributes.

I quite like RenderOnClient although it is perhaps a bit verbose? What about Render? That nicely aligns with the ReactDOM.hydrate vs ReactDOM.render.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thanks @oliverlloyd, I found this helpful in clarifying the purpose of this component 🙂

You're right that Render does align nicely with that React method, assuming people realise that this component name is a reference to that. Personally, I see the word Render appearing more and more, and hence meaning less and less! The naming of React's render method hasn't scaled well, which is why we see more verbose names like renderToString and renderToStaticMarkup crop up later. This is why I prefer the more verbose name.

I'm not sure if that's a me thing, and I would be interested to hear other opinions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, totally. I'm like you, I have my preference but that's based on my own feelings. Render is aligned and concise but it could could also be confusing.

A better way to choose a name should be with wider input and a focus on what will make sense to new people coming to the code with less context.

Aside. It's perhaps interesting to think about how Astro solved this problem. They continued to use their client api and incorporated this scenario into that. That wouldn't work for as but maybe there are other examples for similar abstractions we can align with?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking this api through some more, I'm starting to think a better approach might be to fold this logic into Hydrate and not have Portal at all. We simply could ask Hydrate to not always server render it's children, which essentially has the same effect as what this PR is proposing.

The api for Hydrate might look something like

<Hydrate when="immediate" where="client">
  <MyClientOnlyThing />
</Hydrate>

or

<Hydrate when="immediate" ssr={false}>
  <MyClientOnlyThing />
</Hydrate>

@oliverlloyd

Copy link
Copy Markdown
Contributor Author

I'm marking this PR as draft while we consider if we want to change the Hydrate api instead

@oliverlloyd
oliverlloyd marked this pull request as draft December 21, 2021 16:52
@oliverlloyd oliverlloyd mentioned this pull request Dec 22, 2021
@oliverlloyd

Copy link
Copy Markdown
Contributor Author

Closing in favour of #3784

@shtukas
shtukas deleted the oliver/portal branch February 1, 2023 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants