Skip to content
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

feat: Implement SSR. #44

Merged
merged 87 commits into from
Jan 8, 2019
Merged

feat: Implement SSR. #44

merged 87 commits into from
Jan 8, 2019

Conversation

avocadowastaken
Copy link
Contributor

@avocadowastaken avocadowastaken commented Dec 23, 2018

Depends on #39 and #42. Tests copied from react-apollo.

Closes #8.

@seeden
Copy link

seeden commented Jan 3, 2019

Hi guys. please merge it :) I really need this feature

@avocadowastaken avocadowastaken changed the title WIP: feat: Implement SSR. feat: Implement SSR. Jan 3, 2019
@avocadowastaken
Copy link
Contributor Author

@trojanowski ready for review

const ssrManager = useContext(SSRContext);

// Modify fetch policy for SSR mode.
const fetchPolicy =
Copy link
Contributor Author

@avocadowastaken avocadowastaken Jan 3, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also a good place for #15

src/useQuery.ts Outdated Show resolved Hide resolved
// available
throw observableQuery.result();
if (currentResult.partial) {
if (suspend) {
Copy link
Owner

@trojanowski trojanowski Jan 3, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd change the order here - SSR should work also for queries using the default suspense mode (at least until suspense-ready official React server renderer is available). So rather:

  if (currentResult.partial) {
    // Register request only when `ssr: true`.
    if (ssrInUse) {
      ssrManager.register(observableQuery.result());
    } else if (suspend) {
      // throw a promise - use the react suspense to wait until the data is
      // available
      throw observableQuery.result();
    }
  }

Also tests should check both suspense and non-suspense mode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suspense does not work in react-dom

Copy link
Owner

@trojanowski trojanowski Jan 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suspense does not work in react-dom

I'm not sure I understand. I changed the code to the one in the snipped above and removed suspend: false from useQuery invocations in getMarkupFromTree-test.tsx and tests passed. Or do you mean that the <Suspense /> component is not supported by the current version of react-dom-server renderer? We could workaround it by creating our custom helper component and recommending requiring to use it instead of <Suspense /> from React:

export const SuspenseSsr = (props: SuspenseProps) => {
  const ssrManager = useContext(SSRContext);
  if (ssrManager) {
    return <>{props.children}</>;
  }

  return <Suspense {...props} />;
};

Copy link
Contributor Author

@avocadowastaken avocadowastaken Jan 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it’s better to force developers pass suspend: false if they want to use ssr, otherwise they will have different behavior on ssr and client side.
suspense assures user that data is not null on render, and if ignore suspense on ssr - this can create random null pointer errors. Current approach will escalate suspended promises to react-dom and it will give good error message.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, i’m writing from phone and didn’t saw your last message.

but my previous message also covers your comment.

from my experience when you’re writing ssr app - you have to write it ssr first otherwise it would be hacky and buggy. it’s better to wait for officials suspense support from react. if someone REALLY wants to use suspense for components that also rendered on server and deal with all the problem it produeses - they can write their own suspense component

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn’t it better to name it “unstable_SuspenseSSR” then 🤔. i like the idea, i’ll try to implement it tonight

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn’t it better to name it “unstable_SuspenseSSR” then 🤔.

👍

i like the idea, i’ll try to implement it tonight

Thank you :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@trojanowski I could't wait 🙂. So I start catching thrown promises in getMarkupFromTree and add tests for suspend: true.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can not wait too 👍 thanks guys

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@umidbekkarimov Looks great, thanks :) I added a few comments about testing.

);
}

it('should run through all of the queries that want SSR with suspense', async () => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests are actually the same for suspene / non-suspense versions. We could make it more DRY with something like that:

[true, false].forEach(suspend => {
  const prefix = suspend ? 'with' : 'without';

  describe(`${prefix} suspense`, () => {
    it('should run through all of the queries that want SSR with suspense', async () => {
      const client = createMockClient();

      await expect(
        getMarkupFromTree({
          renderFunction: renderToString,
          tree: <UserDetailsWrapper client={client} suspend={suspend} />,
        })
      ).resolves.toBe('James');
    });

    // the same for other tests
  });
});

Inline snapshots wouldn't work in that case but it shouldnt't be a problem - we were checking simple strings so we could easily replace toMatchInlineSnapshot with toBe.

Copy link
Contributor Author

@avocadowastaken avocadowastaken Jan 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used describe.each instead and all tests MUST have same output so toMatchInlineSnapshot works just fine.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea.

src/__tests__/SuspenseSSR-test.tsx Outdated Show resolved Hide resolved

function UserDetailsWrapper({ client, ...props }: UserWrapperProps) {
return (
<TestErrorBoundary>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of using <TestErrorBoundary> here? Tests are passing also without it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to test this block

} catch (e) {
if (!isPromiseLike(e)) {
throw e;
}
ssrManager.register(e);
}

But I guess I found the better way and forgot to remove 🤔

<UserDetailsWrapper client={client} suspend={suspend} />
);

if (suspend) {
Copy link
Contributor Author

@avocadowastaken avocadowastaken Jan 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept using toMatchInlineSnapshot to keep it "visual" and without manual labor 🙂

@trojanowski trojanowski merged commit 664edc2 into trojanowski:master Jan 8, 2019
@alidcast
Copy link

@trojanowski when will a release with this PR be done?

also, is server-rendering compatible with react-apollo, or will it only allow using one library?

@trojanowski
Copy link
Owner

when will a release with this PR be done?

@alidcastano I'm going to add documentation for this feature and then release it - I'll try to do it tomorrow.

also, is server-rendering compatible with react-apollo, or will it only allow using one library?

Right now it's not compatible with react-apollo, but I'm going to add a compatibility layer.

@randyridge randyridge mentioned this pull request Jan 16, 2019
@trojanowski
Copy link
Owner

@alidcastano This PR is published as a part of release 0.3.0.

const ssrManager = useContext(SSRContext);

return ssrManager ? (
<>{children}</>
Copy link

@sandiiarov sandiiarov Feb 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious. Was there a specific reason to wrap children into fragment?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants