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

Question: How to know if the loading status of Suspense resolved #14577

Closed
chenesan opened this issue Jan 12, 2019 · 11 comments
Closed

Question: How to know if the loading status of Suspense resolved #14577

chenesan opened this issue Jan 12, 2019 · 11 comments

Comments

@chenesan
Copy link
Contributor

Do you want to request a feature or report a bug?

Question

What is the current behavior?

I'm trying to implement supports of React.lazy and React.Suspense in enzyme (enzymejs/enzyme#1975). I'd like to have something like waitUntilLazyLoaded so we can write such test:

// in DynamicComponent.js

class DynamicComponent extends React.Component {
  render() {
    // render something
  }
}

// in test.js

const LazyComponent = lazy(() => import('./DynamicComponent'));
const Fallback = () => <div />;
const SuspenseComponent = () => (
  <Suspense fallback={<Fallback />}>
    <LazyComponent />
  </Suspense>
);

// mount the react element
const wrapper = mount(<SuspenseComponent />)

// On starter this should render Fallback
expect(wrapper.find('Fallback')).to.have.lengthOf(1)
expect(wrapper.find('DynamicComponent')).to.have.lenghtOf(0)

// wait for LazyComponent loading DynamicComponent and update of rendering
await wrapper.waitUntilLazyLoaded()

// render loaded component now
expect(wrapper.find('Fallback')).to.have.lengthOf(0)
expect(wrapper.find('DynamicComponent')).to.have.lengthOf(1)

Inside the mount implementation we call the ReactDOM.render to render it and get the root fiber node. Now my problem is: Given a Fiber node of Suspense, how could we know the loading status of Suspense so I can make sure whether (1) the module is loaded successfully loaded (or failed) and (2) React has rendered (or not rendered) the new component tree?

I'm not familiar with the implementation detail of Fiber and I'm still trying to investigate into this. That would be great if someone familiar with this could answer. Thanks!

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

^16.6

@chenesan chenesan changed the title How to know if the loading status of Suspense resolved Question: How to know if the loading status of Suspense resolved Jan 12, 2019
@gaearon
Copy link
Collaborator

gaearon commented Jan 18, 2019

The existing introspection is already flaky and is painful to maintain. I don't think extending that to guess Suspense state from Fibers is something we'd want to support even as a temporary measure. We need to seriously rethink the testing strategy going forward.

@chenesan
Copy link
Contributor Author

After some discussion in enzymejs/enzyme#1917 I think we will not rush to build an api for waiting lazy component loading. But this is still worth to do.
For now I could think of some ways to make it testable:

  1. have an handler api on Suspense like onLoad when something (or all of them) get loaded successfully / failed.

  2. have a way to preload(or wrap/mock) React.lazy or the returned lazy component so we can make lazy component loaded / failed in initial render. I'm not sure how this could be done, though. Naive use case may be:

// in component

// when `React.lazy` called it will cache the returned component
const LazyComponent = React.lazy(() => import("./DynamicComponent"))

// in test

// in `preloadLazy` we check if the passed component(s) is as same as the component(s) we've seen before, If so we'll preload it.
await React.preloadLazy(LazyComponent)
// will render DynamicComponent inside Suspense in initial mount
ReactDOM.render(<Suspense fallback={<Fallback />}>
  <LazyComponent />
</Suspense>)

@gaearon
Copy link
Collaborator

gaearon commented Jan 18, 2019

The whole notion of "waiting" for Suspense in tests is suspicious to me. Your components are available synchronously so why wait (unless you specifically test the placeholder)?

If the issue is with React not giving you a mechanism maybe #14626 can fix it.

@chenesan
Copy link
Contributor Author

Thanks for reply and this looks good! I think it would be helpful in some cases.
However, (maybe I'm wrong...) I couldn't see how we can easily pass the synchronous component in the test code

// in LazyComponent.js
const LazyComponent = React.lazy(() => import("./InternalComponent"))

// in some component using LazyComponent
const ComponentToTest = () => {
  return (<Suspense fallback={<Fallback />}>
    <LazyComponent />
  </Suspense)
}

// in test code testing(rendering) `ComponentToTest` how could we pass the synchronous version of `LazyComponent` into `ComponentToTest`?

I know we can make LazyComponent a prop of ComponentToTest, e.g.:

// in ComponentToTest.js
const ComponentToTest = (props) => {
  const { LazyComponentProp } = props
  return (<Suspense fallback={<Fallback />}>
    <LazyComponentProp />
  </Suspense)
}

// in test code we can pass InternalComponent directly
testRender.render(<ComponentToTest LazyComponentProp={InternalComponent} />)

But if we have many lazy components inside ComponentToTest this would be tedious to make every used lazy component a prop.

I think in such case if we can something diretly set LazyComponent as available synchronous component so we can render it synchronously would be great. Maybe like:

const LazyComponent = React.lazy(() => import("./InternalComponent"))

// in test code

LazyComponent.setLoadedComponent(InternalComponent)
// So In test code(or future rendering) react will view LazyComponent as loaded InternalComponent

I'm not sure if I miss anything here, though.

@chenesan
Copy link
Contributor Author

chenesan commented Jan 18, 2019

Or, is it possible to reset the dynamic loading function into lazy compoment so we can make it load the synchronous thenable in test env?

Example:

const LazyComponent = React.lazy(() => import("./InternalComponent")
// replace passed loader func with sync one
LazyComponent.resetLoader(() => { then() { return InternalComponent } })

@chenesan
Copy link
Contributor Author

Ah, I found out that to test lazy synchronously we can just handle it with mock utility in jest or sinon, no need to have resetLoader.

// in LazyComponent.js
const LazyComponent = React.lazy(() => import("./InternalComponent"))

// in some component using LazyComponent
const ComponentToTest = () => {
  return (<Suspense fallback={<Fallback />}>
    <LazyComponent />
  </Suspense)
}

// in test code (using jest)

beforeAll(() => {
  jest.mock('/path/to/LazyComponent', () => InternalComponent) // or pass synchronous thenable to React.lazy
})

// will render ComponentToTest with InternalComponent in initial mount.
const ComponentToTest = require('./ComponentToTest')
ReactDOM.render(<ComponentToTest />)

Still thanks for discussion :)

@gaearon
Copy link
Collaborator

gaearon commented Jan 20, 2019

Or, is it possible to reset the dynamic loading function into lazy compoment so we can make it load the synchronous thenable in test env?

Node doesn't have import() so you already use something to run in Node. Maybe some Babel plugin. I'm suggesting to use a different plugin that would transform it into a sync thenable.

@nwaughachukwuma
Copy link

nwaughachukwuma commented Apr 30, 2019

Hi @gaearon and @chenesan , am using lazy and Suspense to dynamically load and display a component in my App, and it works fine. But when testing the component using jest and enzyme, am getting the error: SyntaxError: unexpected token import. Please find my test script for the component below:

import 'react-native';
import React, {Suspense} from 'react';
import { shallow } from 'enzyme';
import HomeEventCard, {EventLike} from '../../components/HomeEventCard';
import Loading from '../../components/Loading';

describe('HomeEventCard render', () => {
   it('renders correctly', async () => {
    const tree = shallow(
        <Suspense fallback={<Loading />}>
            <HomeEventCard />
        </Suspense>
   );
   await EventLike;
   expect(tree).toMatchSnapshot();
  });
});

I also tried with react-test-renderer

import 'react-native';
import React, {Suspense} from 'react';
import { create } from 'react-test-renderer';
import HomeEventCard, {EventLike} from '../../components/HomeEventCard';
import Loading from '../../components/Loading'
describe('HomeEventCard render', () => {

  it('rendered lazily', async()=> {
    const root = create(
      <Suspense fallback={<Loading />}>
        <HomeEventCard/>
      </Suspense>
    );
    await EventLike;
    expect(root).toMatchSnapshot();
  });
});

And they both give same error.

I have used the line below from the documentation in react: https://reactjs.org/docs/code-splitting.html
When using Babel, you’ll need to make sure that Babel can parse the dynamic import syntax but is not transforming it. For that you will need babel-plugin-syntax-dynamic-import. And yet it is not solving the problem. Please what could I be doing wrong?

Edit:
I have tried: babel/babel-loader#493

@RecuencoJones
Copy link

RecuencoJones commented Sep 12, 2019

jest.mock('/path/to/LazyComponent', () => InternalComponent)

@chenesan This is great! Was banging my head about this for a while, just had to make sure InternalComponent is a module with default export or else do something like this:

jest.mock('/path/to/LazyComponent', () => {
  const { InternalComponent } = require('/path/to/InternalComponent');

  return { default: InternalComponent };
});

@tmkasun
Copy link

tmkasun commented Nov 14, 2019

I'm too having this testing issue,Simply my problem is, How to test components with <Suspense/> and lazy loads.
I'm using babel-plugin-dynamic-import-node for dynamic lazy imports, But couldn't find a proper solution for thenable <Suspense/> output.
The most viable method I could found was, mocking the React.Suspense implementation to make it synchronous.

I couldn't understand how @gaearon's #14626 PR could help to solve this testing issue?

@ShailyAggarwalK
Copy link

ShailyAggarwalK commented Dec 9, 2021

I needed to test my lazy component using Enzyme. For now, following worked for me to test on component loading completion:

const myComponent = React.lazy(() => 
          import('@material-ui/icons')
          .then(module => ({ 
             default: module.KeyboardArrowRight 
          })
       )
    );

Test Code ->

//mock actual component inside suspense
   jest.mock("@material-ui/icons", () => { 
       return {
           KeyboardArrowRight: () => "KeyboardArrowRight",
   }
   });
   
   const lazyComponent = mount(<Suspense fallback={<div>Loading...</div>}>
              {<myComponent>}
          </Suspense>);
       
   const componentToTestLoaded  = await componentToTest.type._result; // to get actual component in suspense
       
   expect(componentToTestLoaded.text())`.toEqual("KeyboardArrowRight");

This is hacky but working well for Enzyme library.

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

No branches or pull requests

6 participants