-
Notifications
You must be signed in to change notification settings - Fork 661
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
Is the rewind
approach safe?
#216
Comments
It is up to the implementer to call Unclear what you are asking about "new requests" or "safe". "New Requests" on server create a totally new server connection and result in a new Please use Issues to report problems; your questions might be more appropriate for StackOverflow: http://stackoverflow.com/search?q=react-helmet |
Thanks for the fast reply! I did a bit more research and I found some related issues (seems like my concern is related to the underlying API of
The important takeaway for me is that there should only be synchronous logic between There are however also data fetching libraries that are rendering continuously in order to recursively fetch data for all components in the tree that need data (e.g. react-apollo does it – which is what I'm using – and also react-resolver I think). As far as I understand the node.js event loop, for these scenarios the data in I explained my findings in a bit more detail as maybe they could be helpful to someone else who is wondering about the same problem ;-). |
The short answer is: no, it is inherently thread-unsafe. |
@potench can we reopen this issue? Async rendering is going to become more and more popular in the very near future and this library takes advantage that React rendering is done synchronously at the moment in order to implement the I don't think we need a solution just yet but it's definitely worth starting to think about how a library like this would work in an async world. |
For anyone interested, I refactored my framework today replacing The approach used is:
It works in asynchronous environment because doesn't use |
If React 16 is an option here’s a small lib that does head tag management in a thread-safe manner using React Portals: react-head 👍 |
@tizmagik I looked at your library description and seems like it's no different from |
Thanks for taking a look @catamphetamine -- the example app shows usage with |
In
It's already there |
My understanding is that it's not enabled/exposed in the latest release of React 16.0.0. If that's not the case, how do you enable it? Then I can run some tests and try it out! |
@tizmagik Server-side asynchronous rendering is already there while client-side asynchronous rendering will be enabled in future versions. |
@catamphetamine my understanding is that no async rendering features have been enabled in React 16 yet:
https://reactjs.org/blog/2017/09/26/react-v16.0.html#new-core-architecture |
@tizmagik They are enabled on the server side |
Ok, well, if that's true then it seems to be working 😁 |
I mean, asynchronous rendering is working but your library isn't: it's still synchronous on the server without the benefits of being asynchronous. |
PRs are open 😁 |
@tizmagik No PR can fix that, so it would have to be a completely different library. |
You can use The problem with these libraries and streaming is that when you're streaming the response the The best thing I can think of is you either pause streaming until you've received all the relevant header information. Or you switch to defining header overrides at the top-level, ideally tied to a route. |
There's no need for tests because I can already tell you that it won't work
Not "might not", just "won't"
The "relevant header information" could be anywhere by design.
I'm doing that in my own library |
Thanks for the explanation @goatslacker -- yea it seems that streaming is fundamentally incompatible with being able to affect Note that stream rendering and asynchronous rendering are not necessarily the same things! It will be interesting to see what React ends up doing as they roll out async rendering features in later version of React 16. 😁 @catamphetamine I think your use case is different. The whole point behind react-head and react-helmet is that you do not know statically (at the route level) what the |
My use case is like everybody else's
Edit: no, that won't work and it won't be "eventually consistent"
They are, in terms of this discussion
Asynchronous rendering features are already available on the server right now |
Oh, actually, no, it is not. |
There's a middle-ground that could be had here where we're able to modify the header info within children in a React tree but not necessarily at "the end". In that case, pausing the stream until that point is reached is doable. I've implemented a prototype of this approach already and it's 👍 |
@goatslacker It's unclear how do you define "that point" because as |
I disagree 😁 |
Can't argue with that |
I forked this repository and create react-safety-helmet to resolve this problem. Everyone can simply migrate codes. import { createHelmetStore, HelmetProvider, Helmet } from 'react-safety-helmet';
// helmetStore must be created per request.
const helmetStore = createHelmetStore();
ReactDOMServer.renderToString(
<HelmetProvider store={helmetStore}>
<Helmet>
<title>Fancy title</title>
</Helmet>
</HelmetProvider>,
container
);
const head = helmetStore.rewind(); Also, React 16 renderToNodeStream is supported. import stream from 'stream';
class DrainWritable extends stream.Writable {
constructor(options) {
super(options);
this.buffer = '';
}
_write(chunk, encoding, cb) {
this.buffer += chunk;
cb();
}
}
new Promise((resolve, reject) => {
const writable = new DrainWritable();
const helmetStore = createHelmetStore();
ReactDOMServer.renderToNodeStream(
<HelmetProvider store={helmetStore}>
<App />
</HelmetProvider>,
).pipe(writable);
writable.on('finish', () => {
resolve({
body: writable.buffer,
head: helmetStore.rewind(),
});
});
writable.on('error', reject);
}).then(({body, head}) => {
// Create html with body and head object
}); If possible, I will create a PR to react-helmet, but it will break the compatibility. |
@kouhin Looks ok, but I'm still not sure why do people want to not use streamed rendering. |
I'm going to reopen this issue for further investigation. Does anyone have a public repo I can fork and recreate the issue? |
@tmbtech here's some code to get you started const React = require('react');
const { Helmet } = require('react-helmet');
const { renderToNodeStream } = require('react-dom/server');
function Application() {
return React.createElement('div', null,
React.createElement(Helmet, null,
React.createElement('meta', { charSet: 'utf-8' }),
React.createElement('title', null, 'My Title'),
React.createElement('link', { rel: 'canonical', href: 'http://mysite.com/example' })
)
);
}
const element = React.createElement(Application);
renderToNodeStream(element).pipe(process.stdout); The problem here is figuring out where to put FWIW I ended up pulling title, meta, and friends off of the route config (which is static in our case) so we can resolve all of this before we start React rendering. It's still defined using React on the route config so you get the benefits of configurable meta per route without needing side effects. |
Wow, I've been using this library for years on many projects and had no idea it had such a critical bug! Our crawler started to see information from protected routes 🤦🏻♂️ (fortunately nothing critical to date but the door is wide open here!) We're possibly more exposed because in addition to Apollo's Surely @kouhin's solution needs to be implemented here; same approach used by StaticRouter in react router exposing per-request state via a provider? Or at the very least there should be a HUGE warning in the documentation explaining the risks? |
In case somebody else is interested, we ended up using https://github.com/staylor/react-helmet-async which solves the issue. |
I've created a PR #614 to add a warning |
I'm wondering how the
rewind
feature works internally. As far as I understand it simply outputs and removes the last rendered data that can be rendered on the server into a head. So is there something like a queue inside that the head data is being pushed on?This assumes that the node.js server calls
rewind
as soon as possible after the render, right? Couldn't it happen that a new request comes in after a render that kicks off a new render and the rewind for the first request returns the data for the second request?I might be totally wrong though – thanks in advance for the explanation! Very useful library btw.!
The text was updated successfully, but these errors were encountered: