Open chrischen opened 5 years ago
Hi @chrischen, Yeah, it is possible for sure. You would want to create a simple server plugin. Here is an example of a what the server plugin would look like if you wanted to support Aphrodite and Apollo https://gist.github.com/toddw/971f9a1acebd406ba0ffac5f3e422276
You can easily refactor out the Aphrodite code if you are not using it.
If you wanted to do this without a plugin, you could try to do something similar. You inject data by using dangerouslySetInnerHTML
. Be careful though, this can open you up to XSS attacks if you don't escape. JSON.stringify
does not sanitize </script><script>alert('XSS')
Will take a look at the plugin method, but for the injecting script tag method, where would I do that from the application code? I'm assuming the script tag would be parsed after mounting which might be after when I would normally initialize ApolloProvider?
A couple of things you do to determine when you are on the server or on the client:
if(typeof window === "undefined")
import something from "filename.server.js
In the browser something
will be null
and on the server it will return whatever filename.server.js
exports.
Ok I decided to go with the non-plugin method so that I could keep client and server Apollo code relatively synchronized. For anyone else wondering how to implement this here are the steps I took:
src/apps/main/Index.js This file is run on the server so I create ApolloProvider here with ssrMode: true. Also injected the apollo state like so
<script
key="apollo-state"
dangerouslySetInnerHTML={{
__html: `window.__APOLLO_STATE__=${JSON.stringify(
client.extract(),
).replace(/</g, '\\u003c')}`,
}}
/>
src/apps/main/routes.js
This file is run on both client and server, so I do a check if we're on the client and wrap window.__APOLLO_STATE__
which should have been inserted on server render.
Ok I found an issue with this implementation.
The ApolloClient sometimes may need to send HTTP headers (for authorization, cookies, etc), which in my case are stored in cookies and sent to the server as part of the request headers. On browser renders I can access cookies, but on initial server renders I can't seem to access cookies until inside a container component.
From what I can tell, I can only access serverProps or renderProps to access headers/cookies from the gsBeforeRoute
function in container components.
Is there any way to access the gluestick http headers from src/main/Index.js or from routes.js?
The function used to return routes in routes.js gets passed the store
and httpClient
. The httpClient
is returned from getHttpClient and does some work to mirror the cookies of the client: getHttpClient#L98-L130
This is pretty buried but it's all done around here https://github.com/TrueCar/gluestick/blob/55d1da8c6fe9f81fe70629942c4126838f16d448/packages/gluestick/src/generator/templates/EntryWrapper.js
So, if you use the httpClient
which is just a configured axios
instance. A request that comes in through the browser includes cookies. Those cookies are copied along with any http request that the server makes using the configured httpClient
so long as the host matches.
I'm using Apollo's HttpLink which would do its own HTTP request. Is there any way to extract the cookies that are cached from the axios instance from the client request?
Maybe you can use an axios instance with HttpLink with https://github.com/lifeomic/axios-fetch ?
Ok I'll try that and report back.
On that topic though, is there a way to use axios from redux-middlewares? I've been using isomorphic-fetch in a middleware and using cookies set in redux store from gsBeforeRoute.
Yes, if you use the built in promise middleware it exposes httpClient
which is the configured axios instance mentioned above with cookie handling. https://github.com/TrueCar/gluestick/blob/develop/packages/gluestick/shared/lib/promiseMiddleware.js does this so your action creator would look something like this:
export function getData() {
return {
promise: (httpClient) => {
return httpClient.get("/……")
}
}
}
As it turns out when I wrapped <Route>
in routes.js and <html>
in main/Index.js with <ApolloProvider>
it did not actually work. The Apollo context was not provided to the rest of the app.
So that brings me back to square 1.
I got it to partially work by wrapping the <Root>
component in gluestick/EntryWrapper.js, but of course the top of the file is prefixed with /** DO NOT MODIFY **/
. I'm assuming this file gets generated. In any case I still cannot find a good place to inject the __APOLLO_STATE__
. It was being injected in
My requirements are:
1) main/Index.js
can inject it but wrapping ApolloProvider here doesn't provide context for the rest of the app.
Potential workarounds:
1) Wrap each component in each route with Doesn't work.
2) Inject<ApolloProvider>
__APOLLO_STATE__
in MasterLayout and every top-level layout. Wrap MasterLayout. Problem is how can I access the axios instance here?
3) Do server plugin and wrap the Root element there. Problem is cannot seem to get axios client this way?
Ok here's the final solution:
gluestick/EntryWrapper.js
I wrap <Root>
with <ApolloProvider>
and create Apollo's client here with the httpClient (axios client). Client/Server differentiation is done here to selectively create Apollo client in ssrMode.
MasterLayout.js
In every MasterLayout.js I inject __APOLLO_STATE__
<Helmet {...config.head}>
<script type="text/javascript"
key="apollo-state"
>
{`window.__APOLLO_STATE__=${JSON.stringify(
apolloClient.extract(),
).replace(/</g, '\\u003c')}`}
</script>
</Helmet>
I'm trying to implement Apollo-client support while maintaining server-side-rendering compatibility. It seems that I can add ApolloProvider in the routes.js file but the only way to access application state (that can be passed to client) is through the redux store (not ideal as redux is superseded by apollo-client and its ability to manage client-only data). Is there a way to inject data to the "window" object from any of the application-side code to hydrate the apollo store on the client from data from the server render?