Skip to content

renderHook Server #1120

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

Open
childrentime opened this issue Sep 6, 2022 · 24 comments
Open

renderHook Server #1120

childrentime opened this issue Sep 6, 2022 · 24 comments
Labels
question Further information is requested

Comments

@childrentime
Copy link

childrentime commented Sep 6, 2022

What is your question:

How can I migrate this code from @testing-library/react-hooks to @testing-library/react to use react18 and use the ssr environment?

old code

import { renderHook as renderHookSSR } from '@testing-library/react-hooks/server';

 it('should ', () => {
    const { result, hydrate } = renderHookSSR(() => true);
    expect(result).toBe(true);
    hydrate();
    expect(result.current).toBe(true);
  });
@childrentime childrentime added the question Further information is requested label Sep 6, 2022
@joshuaellis
Copy link
Member

Sorry, did you mean to create this issue?

@childrentime
Copy link
Author

Sorry, missing some words.
how can i migratie example used in ssr environment with @testing-library/react?

@joshuaellis
Copy link
Member

This question is better suited for @testing-library/react, i'll move the issue.

@joshuaellis joshuaellis transferred this issue from testing-library/react-hooks-testing-library Sep 7, 2022
@chambo-e
Copy link

Hello 👋
Tiny up on this conversation :)

@faessler
Copy link

I faced almost the same problem as OP and wanted to share my solution here in case someone else stumbles over this issue.

First I extracted the code I needed from @testing-library/react-hooks/server and updated it to be compatible with react@18 (hydrateRoot). I also skipped a lot of code I didn't need for my case, so you might need to extend my renderHookServer function to match your specific case.

import type { ReactNode } from 'react';
import { hydrateRoot } from 'react-dom/client';
import { renderToString } from 'react-dom/server';
import { act } from 'react-dom/test-utils';

export const renderHookServer = <Hook extends () => any>(
    useHook: Hook,
    {
        wrapper: Wrapper,
    }: {
        wrapper?: ({ children }: { children: ReactNode }) => JSX.Element;
    } = {}
): { result: { current: ReturnType<Hook> }; hydrate: () => void } => {
    // Store hook return value
    const results: Array<ReturnType<Hook>> = [];
    const result = {
        get current() {
            return results.slice(-1)[0];
        },
    };
    const setValue = (value: ReturnType<Hook>) => {
        results.push(value);
    };

    const Component = ({ useHook }: { useHook: Hook }) => {
        setValue(useHook());
        return null;
    };
    const component = Wrapper ? (
        <Wrapper>
            <Component useHook={useHook} />
        </Wrapper>
    ) : (
        <Component useHook={useHook} />
    );

    // Render hook on server
    const serverOutput = renderToString(component);

    // Render hook on client
    const hydrate = () => {
        const root = document.createElement('div');
        root.innerHTML = serverOutput;
        act(() => {
            hydrateRoot(root, component);
        });
    };

    return {
        result: result,
        hydrate: hydrate,
    };
};

I also had to add this to jest setupFiles:

import { TextEncoder } from 'util';
global.TextEncoder = TextEncoder;

Finally I was able to use it like this:

import { renderHookServer } from '../../testing/renderHookServer';
import { useHasMounted } from '../useHasMounted';

describe('useHasMounted', () => {
    it('returns false first and then true after hydration', () => {
        const { result, hydrate } = renderHookServer(useHasMounted);
        expect(result.current).toBe(false);
        hydrate();
        expect(result.current).toBe(true);
    });
});

@xobotyi
Copy link

xobotyi commented May 14, 2023

It literally freaks me out that so called "merge"of libraries cut out half of functionality and in case you need to test hooks agains SSR environment - "screw you - go make your own testing library for that, you're not welcome here".

@slorber
Copy link

slorber commented Jun 2, 2023

Same here: I was looking to upgrade Docusaurus to React 18 and couldn't find a simple official solution to migrate this test:

import React from 'react';
import {renderHook} from '@testing-library/react-hooks/server';
import {BrowserContextProvider} from '../browserContext';
import useIsBrowser from '../exports/useIsBrowser';

describe('BrowserContextProvider', () => {
  const {result, hydrate} = renderHook(() => useIsBrowser(), {
    wrapper: ({children}) => (
      <BrowserContextProvider>{children}</BrowserContextProvider>
    ),
  });
  it('has value false on first render', () => {
    expect(result.current).toBe(false);
  });
  it('has value true on hydration', () => {
    hydrate();
    expect(result.current).toBe(true);
  });
});

The docs say:

hydrate: If hydrate is set to true, then it will render with ReactDOM.hydrate. This may be useful if you are using server-side rendering and use ReactDOM.hydrate to mount your components.

https://testing-library.com/docs/react-testing-library/api#hydrate

It is unclear to me how to use this option and how it "may be useful". Who has ever used it in practice and how? An example would be very welcome

@juliencrn
Copy link

Same for usehooks-ts, how to migrate that

import { renderHook as renderHookCsr } from '@testing-library/react-hooks/dom'
import { renderHook as renderHookSsr } from '@testing-library/react-hooks/server'

import { useIsClient } from './useIsClient'

describe('useIsClient()', () => {
  it('should be false when rendering on the server', (): void => {
    const { result } = renderHookSsr(() => useIsClient())
    expect(result.current).toBe(false)
  })

  it('should be true when after hydration', (): void => {
    const { result, hydrate } = renderHookSsr(() => useIsClient())
    hydrate()
    expect(result.current).toBe(true)
  })

  it('should be true when rendering on the client', (): void => {
    const { result } = renderHookCsr(() => useIsClient())
    expect(result.current).toBe(true)
  })
})

@childrentime
Copy link
Author

@eps1lon hello, can you have a notice on this. I find the render option has {hydrate: true}, but it don't use renderToString like the old behavior, it is directly using hydrateRoot to the test component. So we can't get the value by renderToString in server anymore.

@eps1lon
Copy link
Member

eps1lon commented Jun 11, 2023

Should result contain the earliest possible result or the result after all the data has streamed in?

@childrentime
Copy link
Author

@eps1lon I think it is the earliest possible reuslt, which means the value when react render in server return, should not changed by effects

@childrentime
Copy link
Author

There is something important in react, like we should return the same value in server and client at the first render, so there must have a way to check it

@abhimanyu-singh-uber
Copy link

Is there an update on this?

@childrentime
Copy link
Author

I tried cloning the repository and modifying the source code, but it was difficult to implement. The reason is that for a container, once you have already used createRoot to call it, you cannot use hydrateRoot to continue calling it. So, I think there should be a separate test function for React 18 that returns the createRoot and hydrateRoot functions. The hooks should only be rendered when you call that function, instead of rendering the hooks when you use renderHook().

@childrentime
Copy link
Author

There is a simpler approach where we can add a renderToString function to the return value of renderHook, which would return the result of calling the hook with ReactDomServer.renderToString.

@childrentime
Copy link
Author

Hi, Guys. What do you think?

@childrentime
Copy link
Author

@eps1lon It is evident that the current code does not meet the requirements for React server testing because your container does not include the server-side HTML. Therefore, it will not detect common React errors like Warning: Expected server HTML to contain a matching <div> in <div>

@nickserv
Copy link
Member

nickserv commented Aug 8, 2023

Would a bundler or React server be necessary for testing some SSR or RSC hooks? I'm thinking about possible architectures for server components, and if there's overlap with server hooks.

@childrentime
Copy link
Author

I think it's generally unnecessary to consider React server components. Custom hooks are typically used only in client-side components.
https://github.com/facebook/react/blob/493f72b0a7111b601c16b8ad8bc2649d82c184a0/packages/react/src/ReactSharedSubset.js

@nickserv
Copy link
Member

nickserv commented Aug 8, 2023

Yes, but custom hooks should still be able to use the following React hooks in RSCs:

  • use
  • useId
  • useCallback
  • useContext
  • useDebugValue
  • useMemo

@childrentime
Copy link
Author

childrentime commented Apr 19, 2024

Hello everyone. This is how I currently conduct Server Side Rendering (SSR) testing, and I hope it can serve as a reference for you.
childrentime/reactuse#81

 it("should throw mismatch during hydrating when not set default state in dark", async () => {
    const TestComponent = createTestComponent(() => usePreferredDark());
    const element = document.createElement("div");
    document.body.appendChild(element);

    try {
      const markup = ReactDOMServer.renderToString(<TestComponent />);
      element.innerHTML = markup;
      window.matchMedia = createMockMediaMatcher({
        "(prefers-color-scheme: dark)": true,
      }) as any;

      await act(() => {
        return ReactDOMClient.hydrateRoot(element, <TestComponent />);
      });
      expect((console.error as jest.Mock).mock.calls[0].slice(0, 3))
        .toMatchInlineSnapshot(`
        [
          "Warning: Text content did not match. Server: "%s" Client: "%s"%s",
          "false",
          "true",
        ]
      `);
    }
    finally {
      document.body.removeChild(element);
    }
  });

The test environment "* @jest-environment ./.test/ssr-environment" is copied from the React source code. I found that it seems that this is how testing is done in the React source code.

@somewhatabstract
Copy link

somewhatabstract commented Apr 19, 2024

I encountered this issue. We need to verify that our custom hooks are returning the appropriate thing on initial render (i.e. the server render). I made this function as a drop-in replacement (ymmv, I only covered our use cases) for the old renderHook import from @testing-library/react/server.

Perhaps this will be useful to others who are missing the old function for verifying their hooks do the right thing before effects are run.

import * as React from "react";
import * as ReactDOMServer from "react-dom/server";

type Options<Props> = {
    /**
     * Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating
     *  reusable custom render functions for common data providers.
     */
    wrapper?: React.JSXElementConstructor<{children: React.ReactNode}>;

    initialProps?: Props;
};

type RenderHookServerResult<Result> = {
    result: {
        current: Result;
    };
};

/**
 * Render a hook within a test React component as if on the server.
 *
 * This is useful for seeing what the initial render might be for a hook before
 * any effects are run.
 */
export const renderHookServer = <Result, Props>(
    render: (initialProps: Props) => Result,
    {wrapper, initialProps}: Options<Props> = {},
): RenderHookServerResult<Result> => {
    let result: Result;
    function TestComponent({renderCallbackProps}) {
        result = render(renderCallbackProps);
        return null;
    }

    const component = <TestComponent renderCallbackProps={initialProps} />;

    const componentWithWrapper =
        wrapper == null
            ? component
            : React.createElement(wrapper, null, component);

    ReactDOMServer.renderToString(componentWithWrapper);

    // @ts-expect-error Variable 'result' is used before being assigned. ts(2454)
    return {result: {current: result}};
};

@wojtekmaj
Copy link

With @testing-library/react-hooks not supporting React 19, this matter is even more burning than it was before.

@eps1lon
Copy link
Member

eps1lon commented Jul 10, 2024

What would renderHookServer offer other than

function Component() {
  return useHookUnderTest()
}

const result = ReactDOMServer.renderToString()

?

For assertions before and after hydration, the proposed API is problematic since it creates a mixed client/server environment that you'd never encounter in the real world. Server-only and client-only tests should give you most of the coverage and the remaining hydration bits can be tested with e2e tests that give you the proper confidence.

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

No branches or pull requests