Skip to content

[RFC] RenderingTarget type and varying props by platform(?)Β #7256

@OllysCoding

Description

@OllysCoding

Note: In earlier versions of this RFC, RenderingTarget was called Platform, however it's been rightfully pointed out that Platform as a term is overloaded, so hopefully RenderingTarget better reflects what we're trying to do here!

Background

DCR is being extended to support rendering for both web & apps, and as a result AR will eventually be decommissioned.

Content pages (articles) differ slightly between web & apps, and DCR must now be updated to support these differences, these include:

  • Different reader revenue propositions
  • Different ad stacks
  • Extra content, like follow buttons for apps
  • Web integrations being replaced with native integrations (e.g video, lightbox)

For this to work then, our components in DCR will need to be aware of what they're rendering for, and - occasionally - they'll also need different props and data in order to achieve this.

Introducing RenderingTarget type

The RenderingTarget type will aim to encapsulate the different targets that DCR can render for. We'd likely start with just Web and Apps as options, but in the long term we could include AMP (Which DCR already renders for) and Editions which will come at a later date in the DCAR migration.

How can we define it?

There are really two options to define this new RenderingTarget type:

Option 1 - Enums

export enum RenderingTarget {
    Web,
    Apps,
    // Coming later?
    AMP,
    Editions,
}

Option 2 - String union

export type RenderingTarget = 
    | "Web"
    | "Apps"
    // Coming later?
    | "AMP"
    | "Editions"

While enums would likely make the most sense semantically, they're rarely used in DCR and generally we see string unions used in place.

This RenderingTarget type would then likely be heaving drilled in DCR (since we don't use React Context or any similar global state), to allow components to make decisions such as

<div>
      { renderingTarget === RenderingTarget.Apps ? <AppsLightbox  ... /> : <WebLightbox ... /> } 
</div>

It's worth noting that as the RenderingTarget type expands, these comparisons would become more difficult, leaving us a few options for handling differences between the rendering targets

<div>
     {{
           RenderingTarget.Web: <WebLightbox ... />
           RenderingTarget.Apps: <AppsLightbox  ... />
           RenderingTarget.AMP: <></>
           RenderingTarget.Editions: <></>
     }[renderingTarget]}
</div>

// OR

const renderLightbox= (renderingTarget) = {
     switch(renderingTarget) {
           case RenderingTarget.Web:
               return <WebLightbox ... />
           case RenderingTarget.Apps:
               return <AppsLightbox ... />
           default:
               return <></>
     }
} 

// ...
<div>
     {renderLightbox(renderingTarget)}
</div>

We should carefully consider how we want to define RenderingTarget now, and what kinds of solutions we may use in components in the future.

Differing props by the RenderingTarget type

While working on the original spike, we discovered that we may encounter differences between the required props for each rendering target. As an example, while in DCR we require a contributionsServiceUrl string in order to link users to sign up for a digital subscription, this wouldn't be required in the app - where it would likely be activated natively, through bridget.

As a consequence of this, we took a look at how we can differ types based on the value of the RenderingTarget.

Introducing the CombinedProps pattern

During the spike, @JamieB-gu came up with the follow pattern to enable specific Apps only & Web only props:

Definition:

type AppsProps<Props> = Props extends void
	? { renderingTarget: RenderingTarget.Apps }
	: Props & { renderingTarget: RenderingTarget.Apps };
type WebProps<Props> = Props extends void
	? { renderingTarget: RenderingTarget.Web }
	: Props & { renderingTarget: RenderingTarget.Web };

type AppsOrWeb<Apps, Web> = AppsProps<Apps> | WebProps<Web>;

export type CombinedProps<Common, Apps, Web> = Common & AppsOrWeb<Apps, Web>;

Usage:

/ * Component Defintion */
type CommonProps = {
	format: ArticleFormat;
	...
};

type AppsProps = void;

type WebProps = {
	contributionsServiceUrl: string;
};

const MyComponent = (
             // Because there are different definitions of `props` based the value of renderingTarget, we cannot deconstruct props in the function definition
	props: CombinedProps<CommonProps, AppsProps, WebProps>,
) => {
     const { 
        format, 
        renderingTarget,
        ... 
     } = props

    /* If we tried to access props.contributionServiceUrl here, we'd get a TS error, as it's only
        defined when we've checked that renderingTarget === RenderingTarget.Web */

     return (
          <>
               ...
               {renderingTarget === RenderingTarget.Web && (
                    <AnotherComponent contributionsServiceUrl={props.contributionsServiceUrl} />
               )}
          </>
     )
}

/* Calling the component */

// The component must be defined twice, once with the web props & once with the apps props:
<div>
{renderingTarget === RenderingTarget.Web ? 
       <MyComponent renderingTarget={RenderingTarget.Web} format={format} contributionServiceUrl={'...'} />
       :  <MyComponent renderingTarget={RenderingTarget.Apps} format={format} /> }
</div>

What are the advantages of this approach?

The key advantage of this approach is type safety - Not only do we know for sure which props are defined & when, but we also add safety that people won't try and develop apps features with web props, or vice versa.

Furthermore, it may help with the longevity of the code, as mixed rendering target props could result in eventual confusion on which props should be used when, making it more difficult for future developers to determine what should be used when.

What are the disadvantages of this approach?

This pattern is fairly complex, adding a new pattern to DCR with type definitions that may take some learning for people not familiar with typescript.

It also does not work if you deconstruct the props in the function definition, meaning we'd have a mix of prop deconstruction techniques in DCR going forward.

Another consideration is that this likely wouldn't be a massively common pattern in DCR, as not a lot of components would require differing props, meaning it could be even more jarring to come across.

Alternatives to the CombinedProps pattern

Make everything optional

In this case, we could just make contributionServiceUrl optional, and then do a check such as renderingTarget === RenderingTarget.Web && contributionServiceUrl !== undefined && <AnotherComponent contributionsServiceUrl={props.contributionsServiceUrl} />

For this to work best, you'd want to make DCR variants of the model optional, but have the FE model where DCR still expects contributionsServiceUrl to be defined for web requests. Otherwise we'd compromise the type safety of our model.

Have all the data, all the time

The second option would be to make all apps only and web only props required all the time. This relies on the idea that we will have a single data source for both apps & web, and we'll always be able to provide that data no matter what request we're making.

Both of the alternatives have the disadvantage that we lose the ability to discern the difference between apps and webs data at the point of usage, which could make the system feel more complex for developers.


Thanks for reading this long explanation of the two concepts we could bring to DCR! I'd love to hear your thoughts and ideas on how to solve this problem!

The key thing here is that adding the ability to render for apps requires adding some amount of new complexity to DCR, and we need to carefully think about how we manage this complexity, and what the best way to implement it could be!

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions