I'm using a few third-party React hook libraries that aren't required for the initial render. E.g. react-use-gesture, react-spring, and react-hook-form. They all provide interactivity, which can wait until after the UI is rendered. I want to dynamically load these using Webpack's codesplitting (i.e. import()) after I render my component.
However, I can't stub out a React hook because it's essentially a conditional hook, which React doesn't support.
The 2 solutions that I can think of are:
Somehow extract the hook into a component and use composition
Force React to reconstruct a component after the hook loads
Both solutions seem hacky and it's likely that future engineers will mess it up. Are there better solutions for this?
As you say it, there are two ways to go about using lazy loaded hooks:
Load library in a Parent Component, conditionally render Component using library when available
Something along the lines of
let lib
const loadLib = () => {...}
const Component = () => {
const {...hooks} = lib
...
}
const Parent = () => {
const [loaded, setLoaded] = useState(false)
useEffect(() => loadComponent().then(() => setLoaded(true)), [])
return loaded && <Component/>
}
This method is indeed a little hacky and a lot of manual work for each library
Start loading a component using the hook, fail, reconstruct the component when the hook is loaded
This can be streamlined with the help of React.Suspense
<Suspense fallback={"Loading..."}>
<ComponentWithLazyHook/>
</Suspense>
Suspense works similar to Error Boundary like follows:
Component throws a Promise during rendering (via React.lazy or manually)
Suspense catches that Promise and renders Fallback
Promise resolves
Suspense re-renders the component
This way is likely to get more popular when Suspense for Data Fetching matures from experimental phase.
But for our purposes of loading a library once, and likely caching the result, a simple implementation of data fetching can do the trick
const cache = {}
const errorsCache = {}
// <Suspense> catches the thrown promise
// and rerenders children when promise resolves
export const useSuspense = (importPromise, cacheKey) => {
const cachedModule = cache[cacheKey]
// already loaded previously
if (cachedModule) return cachedModule
//prevents import() loop on failed imports
if (errorsCache[cacheKey]) throw errorsCache[cacheKey]
// gets caught by Suspense
throw importPromise
.then((mod) => (cache[cacheKey] = mod))
.catch((err) => {
errorsCache[cacheKey] = err
})
};
const SuspendedComp = () => {
const { useForm } = useSuspense(import("react-hook-form"), "react-hook-form")
const { register, handleSubmit, watch, errors } = useForm()
...
}
...
<Suspense fallback={null}>
<SuspendedComp/>
</Suspense>
You can see a sample implementation here.
Edit:
As I was writing the example in codesandbox, it completely escaped me that dependency resolution will behave differently than locally in webpack.
Webpack import() can't handle completely dynamic paths like import(importPath). It must have import('react-hook-form') somewhere statically, to create a chunk at build time.
So we must write import('react-hook-form') ourselves and also provide the importPath = 'react-hook-form' to use as a cache key.
I updated the codesanbox example to one that works with webpack, the old example, which won't work locally, can be found here
Have you considered stubbing the hooks? We used something similar to async load a large lib, but it was not a hook, so YMMV.
// init with stub
let _useDrag = () => undefined;
// load the actual implementation asynchronously
import('react-use-gesture').then(({useDrag}) => _useDrag = useDrag);
export asyncUseDrag = (cb) => _useDrag(cb)
Related
I have two components in my react app:
Component A
Performs a lazy fetch of users. This looks like:
const ComponentA = () => {
const [trigger, {data}] = useLazyLoadUsers({
fixedCacheKey: fixedLoadUsersKey,
});
useEffect(() => {
trigger();
}, []);
return <div>{data.map(user => user.id)}</div>
}
Component B
Wants to render a loading indicator while useLazyLoadUsers's isLoading property equals true. This component looks like this:
const ComponentB = () => {
const [, {isLoading}] = useLazyLoadUsers({
fixedCacheKey: fixedLoadUsersKey,
});
if (!isLoading) {
return <div>Users loaded</div>
}
return <div>Loading users</div>
}
The issue
While this works well (the states are in sync via the fixedLoadUsersKey), I'm struggling to find documentation or examples on how to test Component B.
Testing Component A is well documented here https://redux.js.org/usage/writing-tests#ui-and-network-testing-tools.
I already have an overwritten react testing library render method that provides a real store (which includes all my auto-generated queries).
What I would like to do is testing that Component B loading indicator renders - or not - based on a mocked isLoading value. I want to keep my current or similar implementation, not duplicating the isLoading state into another slice.
So far, I have tried mocking useLazyLoadUsers without success. I also tried dispatching an initiator before rendering the test, something like
it('should render the loading indicator', async () => {
const store = makeMockedStore();
store.dispatch(myApi.endpoints.loadUsers.initiate());
render(<ComponentB />, {store});
expect(await screen.findByText('Loading users')).toBeVisible();
})
This didn't work either.
Does someone have a hint on how to proceed here or suggestions on best practices?
I have a React component that doesn't render until fetchUser status is not loading. My test fails because the component is not rendered the first time and therefore can not find page loaded text. How can I mock my const [fetchStatusLoading, setFetchStatusLoading] = useState(false) on my test so that the page would get rendered instead of loading text?
const fetchUser = useSelector((state) => (state.fetchUser));
const [fetchStatusLoading, setFetchStatusLoading] = useState(true);
useEffect(() => {
setFetchStatusLoading(fetchUser .status === 'loading');
}, [fetchStatusLoading, fetch.status]);
useEffect(() => {
// this is a redux thunk dispatch that sets fetch.status to loading/succeeded
dispatch(fetchAPI({ username }));
}, []);
if(fetchStatusLoading) return 'loading..';
return (<>page loaded</>)
// test case fails
expect(getByText('page loaded')).toBeInTheDocument();
If you were able to mock the loading state you would only be testing the code on the "render" part and not all the component logic you are supposed to test, as this is the way Testing Library means to shine.
If this is just an asynchronous problem, you could do something like:
test('test page loaded', async () => {
render(<MyComponent />);
expect(await screen.findByText('page loaded')).toBeInTheDocument();
});
But it seems that your component contains an API request, so if the component accepts a function you could mock it and return the value, or you could mock fetch. Here are some reasons why you should not do that.
Using nock or mock-service-worker you can have a fake server that responds to your API requests and therefore run your component test without having to mock any internal state.
Testing Library just renders the component in a browser-like environment and does not provide an API to modify any of the props or the state. It was created with this purpose, as opposite of Enzyme, which provides an API to access the component props and state.
Check out this similar question: check state of a component using react testing library
I know React suspense is new and hasn't been officially released for production yet but I was wondering what the main benefits of it are/or using it would be?
I can't see anything it does as being "new" or replacing something that doesn't already exist?
I know it allows me to load stuff from top to bottom but I can do that anyway in react using my own components
can anyone give me some ideas as to what it may be good for?
it has several benefits to use:
it makes code splitting easy
(with new usecase) it makes data fetching so easy! Read this
it just suspends your component rendering and renders a fallback component until your component makes itself ready to show, by that you can create a skeleton flow for your async components so easily event with a simple UI ( imagine instead of created a loading login by useState Api or something else )
these were just simple benefits of Reacts Suspense/lazy api.
First of all, I would like to mention that Suspense is officially released since React 16.6. It is production-ready and it is not limited only to code-splitting. Any asynchronous code can be integrated with it.
As of the benefits, consider the following use-case:
We have several components that all use some asynchronous code inside them (like fetching remote resources)
We need to display a loading indicator until all components are finished doing their job
We need to display an appropriate error if some of the components have failed to do their duty
Old way
The good old way of doing this would be:
Create a wrapper component for showing loading indicator and error messages
Keep track of loading and error state inside of each component and inform the wrapper component of state changes
Does this all look like unnecessary, hard to change boilerplate? Yes, it does).
New way
React introduced the Suspense component and Error Boundaries to eliminate this boilerplate and to declaratively describe the desired behavior.
Check this out:
<Exception fallback="An error has occured">
<Suspense fallback="Loading...">
<OurComponent1 />
<OurComponent2 />
<OurComponent3 />
</Suspense>
</Exception>
Example
Suppose we want to fetch users' data from the remote resource.
const fetchUsers = async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await response.json();
console.log("Users data", users);
return users;
};
I will use makeSuspendableHook to integrate our asynchronous fetch within <Suspense> and Error boundary.
const useUsers = makeSuspendableHook(fetchUsers());
In our component, all we should care about is the actual data and its representation.
const Users = () => {
const users = useUsers();
return (
<div>
List fetched users:
<ul>
{users.map(({ name }) => (
<li>{name}</li>
))}
</ul>
</div>
);
}
Finally, I will use Exception as an Error Boundary implementation to stitch everything together.
export default () => (
<Exception fallback="An error has occurred">
<Suspense fallback="Waiting...">
<Users />
</Suspense>
</Exception>
);
Play with web example at codesandbox.io
Play with native example at snack.expo.io
let's assume this easy example with a component and a unit test
component.jsx
const theme = process.env.THEME
export const Component = ({ title }) => {
return (
<>
<h2>{theme} {title}</h2>
</>
)
}
component.test.jsx
import { Component } from './Component'
describe('<Component />', () => {
it('renders properly', () => {
const wrapper = shallow(<Context {...props} />)
expect(wrapper).toMatchSnapshot()
})
})
The project I'm running depends on the const theme, so in my pipeline I've more builds, more deploys, one per theme.
Of course the snapshots fail due to the fact that every time I run the toMatchSnapshot() functionality, it writes / checks the output in the folder ./__snapshots__, so the test for the first theme runs succesfully, then it fails for the others.
Are there solutions to this problem?
Provide the theme as prop to every component (terrible approach)
Avoid snapshots for these components (not so good approach because I would like to keep this functionality on for my test suite)
Use React.createContext (good approach but it requires a bit of refactoring across the project)
Is there something even better I could try to use?
Thanks everyone in advance
React.createContext with a mocked context data for testing is the best approach indeed.
I have a container component where I'm fetching the data via ajax operator from rxjs
const data = ajax(someUrl).pipe(map(r => r.response));
And in my componentDidMount I have
data.subscribe((data) => {
this.setState({ data });
});
// test.js
import React from 'react';
import { mount } from 'enzyme';
import { ajax } from 'rxjs/ajax'
import App from '../src/App';
describe('<App />', () => {
const wrap = mount(<App />);
const data = [{ 1: 'a' }];
const mock = ajax('http://url.com').pipe(map(() => data));
it('renders', () => {
console.log(mock.subscribe(x => x));
expect(wrap.find(App).exists()).toBe(true);
});
});
How do I go about mocking the response so that when I run the test it I can pass that data on to other components and check if they render?
All the testing examples I've found have been redux-Observable ones which I'm not using.
Thanks a lot!
First you need to understand that you should be testing one thing at a time.
Meaning that testing your async method execution should be separated from testing your components rendering proper content.
To test async methods you can mock your data and than mock timers in Jest.
https://jestjs.io/docs/en/tutorial-async
https://jestjs.io/docs/en/asynchronous
https://jestjs.io/docs/en/timer-mocks.html
with jest.useFakeTimers() and techniques mentioned above.
For testing component proper rendering use jest snapshots and e2e testing (can be done with ex. TestCafe)
To connect those approaches you need to design you app in a way that will allow you to:
The API you call in your component, should be external to component and be called from that external source (different file, different class, however you design it), so you can replace it in test.
Whole API should be modular, so you can take one module and test it without initializing whole API just for this case.
If you design your app in such manner, you can initialize part of the API with mock data, than render your component in test and as it will call mocked API, you can check if it renders what you expect it to.