react-testing-library icon indicating copy to clipboard operation
react-testing-library copied to clipboard

renderHook Server

Open childrentime opened this issue 3 years ago • 24 comments

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 avatar Sep 06 '22 18:09 childrentime

Sorry, did you mean to create this issue?

joshuaellis avatar Sep 07 '22 07:09 joshuaellis

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

childrentime avatar Sep 07 '22 07:09 childrentime

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

joshuaellis avatar Sep 07 '22 07:09 joshuaellis

Hello 👋 Tiny up on this conversation :)

chambo-e avatar Nov 25 '22 13:11 chambo-e

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);
    });
});

faessler avatar Apr 20 '23 11:04 faessler

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".

xobotyi avatar May 14 '23 09:05 xobotyi

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

slorber avatar Jun 02 '23 15:06 slorber

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)
  })
})

juliencrn avatar Jun 06 '23 22:06 juliencrn

@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.

childrentime avatar Jun 11 '23 07:06 childrentime

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

eps1lon avatar Jun 11 '23 11:06 eps1lon

@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 avatar Jun 11 '23 12:06 childrentime

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

childrentime avatar Jun 11 '23 12:06 childrentime

Is there an update on this?

abhimanyu-singh-uber avatar Jul 08 '23 00:07 abhimanyu-singh-uber

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 avatar Jul 14 '23 18:07 childrentime

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 avatar Jul 14 '23 18:07 childrentime

Hi, Guys. What do you think?

childrentime avatar Jul 14 '23 18:07 childrentime

@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>

childrentime avatar Jul 14 '23 18:07 childrentime

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.

nickserv avatar Aug 08 '23 09:08 nickserv

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

childrentime avatar Aug 08 '23 09:08 childrentime

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

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

nickserv avatar Aug 08 '23 10:08 nickserv

Hello everyone. This is how I currently conduct Server Side Rendering (SSR) testing, and I hope it can serve as a reference for you. https://github.com/childrentime/reactuse/pull/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.

childrentime avatar Apr 19 '24 04:04 childrentime

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}};
};

somewhatabstract avatar Apr 19 '24 21:04 somewhatabstract

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

wojtekmaj avatar Apr 26 '24 13:04 wojtekmaj

What would renderHookServer offer other than

function Component() {
  return useHookUnderTest()
}

const result = ReactDOMServer.renderToString(<Component />)

?

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.

eps1lon avatar Jul 10 '24 15:07 eps1lon