-
Notifications
You must be signed in to change notification settings - Fork 31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
trial using context for renderingTarget #8704
Conversation
Size Change: 0 B 🆕 Total Size: 0 B |
3f494e9
to
a2a5b54
Compare
export const ArticleAppsStory = () => { | ||
return ( | ||
<Wrapper> | ||
<ArticleMeta | ||
format={{ | ||
display: ArticleDisplay.Standard, | ||
design: ArticleDesign.Standard, | ||
theme: Pillar.News, | ||
}} | ||
pageId="" | ||
webTitle="" | ||
byline="Lanre Bakare" | ||
tags={tagsWithLargeBylineImage} | ||
primaryDateline="Sun 12 Jan 2020 18.00 GMT" | ||
secondaryDateline="Last modified on Sun 12 Jan 2020 21.00 GMT" | ||
isCommentable={false} | ||
discussionApiUrl="" | ||
shortUrlId="" | ||
ajaxUrl="" | ||
showShareCount={true} | ||
/> | ||
</Wrapper> | ||
); | ||
}; | ||
ArticleAppsStory.args = { renderingContext: { target: 'Apps' } }; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding this additional story to demonstrate how we can use the renderingContextDecorator
to override the default rendering context (which has been set as "Web" in the decorator itself)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this addition to demonstrate this! Perhaps we could put a link to it in a more centralised place of documentation as a reference of "how to override the default rendering context". Not sure where the best place is though 🤔
import type { RenderingTarget } from './renderingTarget'; | ||
|
||
/** | ||
* Context for global values (generic) | ||
* | ||
* This should not contain any properties which are likely to change between re-renders | ||
*/ | ||
export interface RenderingContextType { | ||
target: RenderingTarget; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any additional properties to the context must be added here.
We are still forming our lines in the sand but this could be a good place to add a linting rule to prevent unnecessary things from being added to the context if we want to do that?
Or potentially link out to a doc in the docstring?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From #8696 and our lines in the sand sounds like there is already consensus in:
- no usage of context in Islands
- not be used for JSON responses or non JSX components
I am wondering whether we could create our own custom eslint rules for those two. Sounds like these are good cases for an automated check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Custom ESLint rules can be done without too much hassle: https://stackoverflow.com/a/40979625/1577447
import { RenderingContext } from '../../src/components/RenderingContext'; | ||
|
||
const defaultContext = { target: 'Web' }; | ||
|
||
export const RenderingContextDecorator = ( | ||
Story, | ||
{ args: { renderingContext } }, | ||
) => { | ||
const context = { ...defaultContext, ...renderingContext }; | ||
|
||
// For easy debugging | ||
console.log('Storybook rendering context: \n', context); | ||
|
||
return ( | ||
<RenderingContext.Provider value={context}> | ||
<Story /> | ||
</RenderingContext.Provider> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This Storybook decorator wraps all stories in a <RenderingContext.Provider>
component, with the default value of the context provided which can be overridden at component level
Thanks for raising the PR, putting thought on this taking the time to write the RFC! This makes sense to me for this occasion ("a global Context to DCR for truly global values" as per comment). Plus, it is always therapeutic to see repetitive code being removed. Just leaving some thoughts. |
As per the doc for deprecated ADRs should the following be part of this PR?
|
@@ -137,10 +132,12 @@ export const BylineLink = ({ | |||
); | |||
}); | |||
|
|||
const { target } = useContext(RenderingContext); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hope you don't mind some non-webex input here, but super supportive of using contexts in the specific cases of passing around some immutable data!
What would you think of wrapping the call useContext(RenderingContext)
up into its own function? I copied something similar into AMP from the now-defunct AB test context.
const { target } = useRenderingTarget();
One enhancement off the back of this is you could pre-compute isApps
and isWeb
and optionally destructure them when you use the hook e.g.
const useRenderingTarget = () => {
// ...
return {
target,
isApps: target === "Apps",
isWeb: target === "Web",
};
};
// usages
const { isApps } = useRenderingTarget();
const { isWeb } = useRenderingTarget();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this idea!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely keen for non WebX input here as this repo is shared by many, thanks v much Chris!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 on this suggested abstraction! I'm pretty sure I copied @chrislomaxjones's pattern from the AMP AB test code when I was putting together an RFC to use context for server-side tests.
Looking into that code also led me to How to use React Context effectively by Kent Dodds which I remember finding useful. Thought I'd link just in case it hasn't been surfaced in discussion already :)
fwiw, though I don't work with DCR anymore, rendering targets seem to me like a great use-case for Context and this looks like a great PR!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After trialing this solution, I do really like the derived values of context to help remove unnecessary logic from other places but think this would be best as a phase 2 type thing as this PR is a bit long already!
I've implemented what you've recommended @chrislomaxjones and @bryophyta in terms of the custom hook which is a better and safer use of context. The neat thing in that blog @bryophyta was avoiding specifying a default value and erroring with use outside of the provider. This has been super valuable input, thank you both!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, liking the useConfig
naming a lot.
…ince not practical
This PR has changed quite substantially since my conversation with @JamieB-gu and @arelra earlier today, who both had very good feedback on the approach taken and naming conventions, thank you both! 🙌 . To keep it clear what we're dealing with here, we've agreed on calling the context "config", since that most succinctly describes the type of information we are allowing to exist in this context. Config must be static and immutable (and must not change between renders). It cannot be used outside of the It is impossible to separate islands (client-side rendered components) from SSR (server-side rendered) components so we now add a |
@@ -16,13 +18,14 @@ import { getProps } from './getProps'; | |||
* snapshots | |||
*/ | |||
export const doStorybookHydration = () => { | |||
document.querySelectorAll('gu-island').forEach((element) => { | |||
for (const element of document.querySelectorAll('gu-island')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is eslint automatically fixing on save
@@ -26,12 +28,14 @@ declare global { | |||
* @param data The deserialised props we want to use for hydration | |||
* @param element The location on the DOM where the component to hydrate exists | |||
* @param emotionCache An instance of an emotion cache to use for the island | |||
* @param config Application configuration to be passed to the config context for the hydrated component |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are using the app config in a similar way to emotionCache
<> | ||
<LightboxLayout | ||
imageCount={article.imagesForLightbox.length} | ||
<ConfigProvider value={configContext}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ArticlePage
components are wrapped in the ConfigProvider
/** | ||
* Where is this coming from? | ||
* Config value is set at high in the component tree within a React context | ||
* @see /dotcom-rendering/src/components/ArticlePage.tsx or look for a `<ConfigProvider />` | ||
*/ | ||
const { renderingTarget } = useConfig(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added these comments as an attempt to make this pattern easier to understand on first visit
props={JSON.stringify(children.props)} | ||
clientOnly={clientOnly} | ||
rootMargin={rootMargin} | ||
config={JSON.stringify(config)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the exact same data serialised for each and every island?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this config
value comes from the useConfig
hook, so it's the same for every component in the same rendering request
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll probably end up repeating the same data a lot as it's the same config for each island. I've added a comment to initHydration
, but I wonder if we could put config in one place and have islands look it up from there, instead of passing it as an attribute to every island element?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hydration code isn't JSX/React so I can't use a context hook there to fetch the config, which is why I thought of copying the same method that props
uses.
The only way I can think to do this without repeating the config
for each island is by putting the config on window.guardian
, which is something I wanted to try to avoid if possible...
It might be that I'm trying to avoid it for no reason at all as this might be a better pattern to follow? Is there another way to do this without using window.guardian
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could serialise with JSON.stringify
inside a single script tag with type application/json
on the page?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great suggestions, all. Thank you v much, this is very useful info.
As this PR has been around for a while now, I'm going to try to avoid this it ballooning in size so I'm going to treat this as an optimisation and have ticketed up here: #8795
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great! 🚀🚀🚀
I had a question/suggestion about copying config for each island, off the back of @mxdvl's question, but I don't think it's blocking.
dotcom-rendering/docs/architecture/proposed-adrs/react-context-api.md
Outdated
Show resolved
Hide resolved
The decision to allow use of context more generally rests on the following _"lines in the sand"_: | ||
|
||
- Context should be considered **global, static, and immutable** for rendered components in dotcom-rendering. It can act as a type of configuration for our application. | ||
- Context should **only be used for JSX components** (ie. not for JSON responses or non-JSX helper code) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm think I'm missing something, but I wasn't sure what this means in practice. Do we have any counter-examples of when/why context isn't good for these use-cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Context should be considered global, static, and immutable for rendered components in dotcom-rendering. It can act as a type of configuration for our application.
The reason we don't want it to be mutable is to avoid unnecessary re-renders caused by context changing as this could cause lots of issues
- Context should only be used for JSX components (ie. not for JSON responses or non-JSX helper code)
We can't use the React context API for non-React components
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense! thank you!
@@ -36,14 +37,15 @@ export const initHydration = async ( | |||
): Promise<void> => { | |||
const name = getName(element); | |||
const props = getProps(element); | |||
const config = getConfig(element); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
related to @mxdvl's question about if config will be the same for every island (which I think it will be), I wonder if we need to getConfig
from this element, or if we could store page config once for the page, and each island picks it up from there instead of every island having an identical config
attribute?
props={JSON.stringify(children.props)} | ||
clientOnly={clientOnly} | ||
rootMargin={rootMargin} | ||
config={JSON.stringify(config)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll probably end up repeating the same data a lot as it's the same config for each island. I've added a comment to initHydration
, but I wonder if we could put config in one place and have islands look it up from there, instead of passing it as an attribute to every island element?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fantastic work!!! 🚀 Also agree with @mxdvl and @georgeblahblah's suggestions about islands but leaving this up to you. Well done!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work @cemms1 seeing this through!
Extracting out the serialisation of context outside of the component tree feels like a small optimisation that we can consider when/if we put a proper store in place that we can then facade with hooks.
Accepted strange Chromatic diff for Vimeo component after double checking that the component looks fine on Storybook and in the article https://www.theguardian.com/film/2023/jan/25/oscars-2023-how-to-watch-almost-every-nominated-feature-doco-and-short-in-australia when running the application locally. The visual diff appears to only happen on Chromatic so is being treated as a false positive for now |
What does this change?
Trials using React Context for
renderingTarget
instead of heavy prop drilling.Why?
This PR suggests how we might use React context for the scenario of a property set on entrypoint and that doesn't change throughout the component lifecycle.
Resolves #8711
Off the back of this discussion #8696
Additional notes
I've left the
renderingTarget
prop in for the components fromArticlePage
->StandardLayout
/LiveLayout
(viaDecideLayout
).This is because it became quite tricky to decouple the new props structure of
WebProps | AppsProps
, due to therenderingTarget
being used as the identifier for this discriminated union.It may not be necessary to decouple though, since this is only a few layers of props and since in this case the props have a purpose (not using them simply to pass down to lower layers of the component tree).