-
Notifications
You must be signed in to change notification settings - Fork 1
Conversation
|
||
export default class ClientSideOnly extends Component { | ||
static propTypes = { | ||
onServerRender: func, |
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.
onServerRender
is optional for the cases where you really just don't want anything rendered
onServerRender: () => null | ||
}; | ||
|
||
static isSSR() { |
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 a static
method, rather than just an inline
check in render
, so that a 3rd party SSR implementor could override this logic if they want, and have it automagically applied to every usage of ClientSideOnly
in a theme.
Other alternatives include:
-
The export from this module is a factory fn that takes 1 argument (
isSSR
function), and returns aClientSideOnly
component -
Make people pass
isSSR
as a prop every time
I chose the static
method option because it only requires overriding in 1 place, and I can't think of a good argument for a theme or app having multiple different implementations of isSSR
logic
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 static method works fine. If we need to change it at build time, it's a small change, and it allows for a lot of runtime flexibility.
Of course, prototypal inheritance allows us to make it an instance method and still affect all instances at once...
ClientSideOnly.prototype.isSSR = () => true;
But I think that confuses most people, so your way is better.
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.
Yeah, can be in the proto chain. I tend to only put things on the prototype that rely on instance-specific context, and other related things end up as static properties. Of course, that's purely a (personal) stylistic thing.
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 do the same thing. Leave it static!
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.
If you wanna change anything, do, but I approve
onServerRender: () => null | ||
}; | ||
|
||
static isSSR() { |
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 static method works fine. If we need to change it at build time, it's a small change, and it allows for a lot of runtime flexibility.
Of course, prototypal inheritance allows us to make it an instance method and still affect all instances at once...
ClientSideOnly.prototype.isSSR = () => true;
But I think that confuses most people, so your way is better.
render() { | ||
return ClientSideOnly.isSSR() | ||
? this.props.onServerRender() | ||
: this.props.children; |
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 not the most voracious consumer of React components, so this is a stylistic issue that most people might not have. Paging @jimbo for a second opinion. But I think it might be confusing to take a function-as-prop for server content, but regular old children for client content. If this was an intentional choice to encourage writing client-only content rather than server-only content, I dig it...but I'm still not sure how readable it is.
What if maybe:
import OnlyRender from '..';
// [...]
test('Returns value from invoking serverSide prop in SSR mode', () => {
jest.spyOn(OnlyRender, 'isSSR').mockImplementation(() => true);
const wrapper = shallow(
<OnlyRender
serverSide={() => <div>Server Content</div>}
clientSide={() => <span>Client Content</span>}
/>
);
expect(wrapper.text()).toEqual('Server Content');
OnlyRender.isSSR.mockReset();
});
test('Returns default value (null) when serverSide prop is not provided', () => {
jest.spyOn(OnlyRender, 'isSSR').mockImplementation(() => true);
const wrapper = shallow(<OnlyRender clientSide={() => <div>Client Content</div>} />);
expect(wrapper.text()).toEqual('');
OnlyRender.isSSR.mockReset();
});
test('Returns value from invoking clientSide prop in non-SSR mode', () => {
jest.spyOn(OnlyRender, 'isSSR').mockImplementation(() => false);
const wrapper = shallow(
<OnlyRender
serverSide={() => <div>Server Content</div>}
clientSide={() => <span>Client Content</span>}
/>
);
expect(wrapper.text()).toEqual('Client Content');
OnlyRender.isSSR.mockReset();
});
Or what about just letting people specify server-only and client-only content independently?
import OnlyRender from '..';
// [...]
test('Renders children of ServerSide component only in SSR mode', () => {
jest.spyOn(OnlyRender, 'isSSR').mockImplementation(() => true);
// OnlyRender itself is a pass-thru for a convenient top-level node
const wrapper = shallow(
<OnlyRender>
<OnlyRender.ServerSide><div>Server Content</div></OnlyRender.ServerSide>
<OnlyRender.ClientSide><div>Client Content</div></OnlyRender.ClientSide>
</OnlyRender>
);
expect(wrapper.text()).toEqual('Server Content');
OnlyRender.isSSR.mockReset();
});
test('Renders children of ClientSide component only in non-SSR mode', () => {
jest.spyOn(OnlyRender, 'isSSR').mockImplementation(() => false);
const wrapper = shallow(
<OnlyRender>
<OnlyRender.ServerSide><div>Server Content</div></OnlyRender.ServerSide>
<OnlyRender.ClientSide><div>Client Content</div></OnlyRender.ClientSide>
</OnlyRender>
);
expect(wrapper.text()).toEqual('Client Content');
OnlyRender.isSSR.mockReset();
});
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 don't feel especially strongly about this, so if neither of those suggestions are compelling, feel free to brush them off--your API is just fine, and I'm only trying to think about the newbie.)
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.
tbh, the current API is the way it is just because it was the first idea that came to mind.
I'm not a huge fan of the OnlyRender.ClientSide
/OnlyRender.ServerSide
API, but I have no objections if it's preferred that we use render props for both instead of render prop/children.
I'll go with whichever one of the ideas @jimbo ends up preferring
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.
Good discussion. I think @DrewML has the right idea with the original implementation here. This is a pass-through component (render signature of props => props.children
) with a single exception (isSSR: true
). Idiomatic and easy to follow.
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.
10-4, sounds like your current API will be great.
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.
Two questions:
-
This component only accepts one prop, a function named
onServerRender
that is essentially a render function. React Router v4'sRoute
component offers three different props for this purpose—component
,render
, andchildren
—each suited for a different use case. If our component doesn't offer all three, are we at least offering the best one? -
If I recall correctly, when the tree rendered by the client doesn't exactly match the tree rendered by the server, React throws an error. Are we going to encounter that error when using this component?
render() { | ||
return ClientSideOnly.isSSR() | ||
? this.props.onServerRender() | ||
: this.props.children; |
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.
Good discussion. I think @DrewML has the right idea with the original implementation here. This is a pass-through component (render signature of props => props.children
) with a single exception (isSSR: true
). Idiomatic and easy to follow.
@jimbo To your first question, I think Router has a different use case. We wouldn't want a To the second question--I don't know! Maybe React has to blow the whole UI away rather than build a reconciliation. |
That's....a very good point that I hadn't considered. Some relevant reading:
Abramov made the following comment that is worth calling out: Thoughts? |
Thanks, Dan, that's a good idea. Seems like this requires passing a function-as-prop (or function-as-child?) which receives |
Abramov's example is correct, but isn't the best solution. I don't think local component state is the right place to maintain the
Only the first render of a user's session is done on the server. Every subsequent render is done by the client—even visiting new routes—so we once we're client-side, we shouldn't use the double-render flow. If the Since the server's render is a one-time global event, we should probably store the flag in global state—in the Redux store—and change it from
|
@jimbo has a good point that local state seems wasteful for a one-time global event. I wonder, though, if this can be a compile-time optimization... components/foo.js/** global ClientOnly */
import React from 'react'
// note that ClientOnly is not imported; this way it's easier to detect at
// build time by string
export default class Foo extends React.Component {
render() {
return (<ClientOnly onServerRender={<div>Server content</div>}>
<span>Client content</span>
</ClientOnly>)
}
} in a webpack plugincompiler.parser.plugin('<ClientOnly', expr => {
/** here or at some other hook, detect its presence */
}) at server component build time
build/server/components/foo.js/** global ClientOnly */
import React from 'react'
export default class Foo extends React.Component {
render() {
return <div>Server content</div>
}
} at client component build time
in bundle:import React from 'react'
const ClientOnly = p => p.children
export default class Foo extends React.Component {
render() {
return <ClientOnly><span>Client content</span></ClientOnly>
}
} Potentially you could even detect if |
I'd prefer @jimbo's approach. I'm not a big fan of moving things to build time optimizations unless it actually provides some sort of DX or performance improvements (doesn't seem like that will). |
@zetlen That would be viable, but I'm reluctant to add another magic global (similar to a JSX pragma). I would prefer to do something more straightforward. |
Hi! First about the current API, I feel like However, the more broader thing is that this component focus on stripping things out of the server. We have a few use cases in our stack that only changes props, rather than the full implementation. So we just strip the graphql component which is supposed to load For this reason, I feel like a more broader component is more useful. Something along those lines :
Actually it feels a bit weirder with renderProps and it works better for us with HOCs since we use them a lot for the data fetching part.
But hah! Just to say that it's a tiny bit restrictive to phrase it as a |
Thanks, @JulienPradet ! The reason we have to use render props instead of direct children is that without a render lambda, React always renders elements bottom-to-top. Therefore, both server and client content would attempt to fully render as vdom before the |
Thank you for you answer @zetlen :) I did use renderProps in my example. Even though they are within the <IsServerSide>
{isServerSide =>
isServerSide ? <div>Server Content</div> : <span>Client Content</span>
}
</IsServerSide>;
// or
<IsServerSide
render={isServerSide =>
isServerSide ? <div>Server Content</div> : <span>Client Content</span>
}
/>; In case you were talking about the HOC I've shown, indeed, it's slowing down the initialization of the app (not the subsequent renders). About using the the server-side logic on the client side, as @jimbo stated previously, it's mandatory if you want to have a correct hydration phase on the client. In our experience, the only thing that's safe here is to use client side logic that's not available on the server (window, DOM, refs, etc.). You can't really use things specific to the server environment without breaking the hydration (except for caching strategies). |
Too many concurrent things going on with higher priority, so haven't been able to give this much more attn. Will revisit with further discussions about community contribution opportunities for optional SSR later on. |
No description provided.