I want to write Unit Test for the customhook useImageLoad. I want to check how many times "onImageLoad" parameter is called.
Initially I was using onImageLoad inside useEffect but it was rendering every seconds continously. So I have to use it with useCallback hook to run only once. Now its working fine as expected but I want to write a unit test which could get me how many times onImageLoad function has been called.
Below is the function:-
useImageLoad.ts
import { useEffect, useCallback, useState } from 'react';
export const useImageLoad = (src: string, onImageLoad: () => void = () => {}) => {
const [isImageLoaded, setImageIsLoaded] = useState(false);
useCallback(() => {
onImageLoad();
}, [onImageLoad]); // render 2 times
useEffect(() => {
const newImage = new Image();
newImage.src = src;
newImage.onload = () => {
setImageIsLoaded(true);
};
return () => {
newImage.onload = null;
};
}, [src]);
return isImageLoaded;
};
I am trying some way by looking into some tutorial but couldn't help myself how to excetute and run test.
useImageLoad.spec.ts
import React from 'react';
import { render } from '#testing-library/react';
import { useImageLoad } from './useImageLoad';
type HookWrapperProps = {
src: string;
onImageLoad: () => void;
};
// This is just a simple wrapper component since it's awkward to test hooks that rely on
// React lifecycle methods.
const HookWrapper = ({ src, onImageLoad }: HookWrapperProps) => {
const isImageLoaded = useImageLoad(src, onImageLoad);
console.log(isImageLoaded, 'isImageLoaded');
return <div>{isImageLoaded}</div>;
};
describe('UseImageLoad tests', () => {
it('should return a repository locator', async () => {
const result = render(<HookWrapper src="test" onImageLoad={() => {}} />);
});
});
Is it possible to achieve same with renderHook ?
I am totally noob in unit test. Excuse my code. Any help will be greatly appreciated. Thanks in advance.
Related
import { useEffect } from "react";
const GoogleTranslate = () => {
useEffect(() => {
let addScript = document.createElement("script");
addScript.setAttribute(
"src",
"//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"
);
document.body.appendChild(addScript);
window.googleTranslateElementInit = googleTranslateElementInit;
}, []);
const googleTranslateElementInit = () => {
return new window.google.translate.TranslateElement(
{
pageLanguage: "en",
layout: google.translate.TranslateElement.InlineLayout.BUTTON,
},
"google_translate_element"
);
};
return <div id="google_translate_element"></div>;
};
export default GoogleTranslate;
Here is my Component. I have used in a single page. But instead of once I am getting two separate instance of google translate button.
So, I want to know how to render only once this button.
Here is the UI image
Double Google translate button bug
useEffect may run twice despite an empty dependency array in multiple circumstances (for example, if a parent component is re-rendering, or if you have React StrictMode enabled).
Hence, you are probably creating 2 elements inside useEffect.
You could analyze your code and check for potential unwanted re-renders or simply replace useEffect with this useEffectOnce hook:
import { useEffect, useRef, useState } from 'react';
export const useEffectOnce = (effect: () => void | (() => void)) => {
const effectFn = useRef<() => void | (() => void)>(effect);
const destroyFn = useRef<void | (() => void)>();
const effectCalled = useRef(false);
const rendered = useRef(false);
const [, setVal] = useState<number>(0);
if (effectCalled.current) {
rendered.current = true;
}
useEffect(() => {
if (!effectCalled.current) {
destroyFn.current = effectFn.current();
effectCalled.current = true;
}
setVal(val => val + 1);
return () => {
if (!rendered.current) {
return;
}
if (destroyFn.current) {
destroyFn.current();
}
};
}, []);
};
Just create a file with this code, and import and use it instead of useEffect.
We have a functionality, when the content is not visible on the screen then we scroll content till the end of content. here is the hooks i have written. quite new to testing Lib it would be great if someone have already written unit test for something like this.
import { useEffect, useRef } from 'react';
export default function useOnScreen() {
const ref = useRef<HTMLElement | null>();
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
const scrollToView = (timer: number) => {
const scrollRef = ref?.current;
if (ref) {
timeoutRef.current && clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
scrollRef?.current.scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'end',
});
}, timer);
}
};
return [ref, scrollToView];
}
Need help here to write test case
import useOnScreen from '../useOnScreen';
import {renderHook, act} from '#testing-library/react-hooks';
describe('useOnScreen', () => {
it('useOnScreen', () => {
const [ref, scrollToView] = renderHook(() => useOnScreen());
});
});
I apologize if this question has been asked before. I did some searching but couldn't find an answer. I'm fairly new to React, so I may be missing something obvious.
I have inherited some TypeScript React code that includes a custom React hook to display a prompt when a user reloads or exits a page when there are edits on the page.
import { useState, useEffect } from 'react';
const initBeforeUnload = (showExitPrompt: boolean) => {
window.onbeforeunload = (event: BeforeUnloadEvent) => {
if (showExitPrompt) {
const e = event || window.event;
e.preventDefault();
if (e) {
e.returnValue = '';
}
return '';
}
};
};
// Hook
const useExitPrompt = (
bool: boolean
): [boolean, (showPrompt: boolean) => void] => {
const [showExitPrompt, setShowExitPrompt] = useState<boolean>(bool);
window.onload = () => {
initBeforeUnload(showExitPrompt);
};
useEffect(() => {
initBeforeUnload(showExitPrompt);
}, [showExitPrompt]);
useEffect(() => () => {
initBeforeUnload(false);
}, []);
return [showExitPrompt, setShowExitPrompt];
};
export default useExitPrompt;
No unit tests were included with this code when I inherited it, so I'm writing tests now. So far I've come up with the following:
/**
* #jest-environment jsdom
*/
import { renderHook, act } from '#testing-library/react-hooks';
import useExitPrompt from './useExitPrompt';
describe('useExitPrompt', () => {
it('should update the value of showExitPrompt', () => {
const { result } = renderHook(({ usePrompt }) => useExitPrompt(usePrompt),
{
initialProps: {
usePrompt: true
}
});
const [initialShowExitPrompt, setShowExitPrompt] = result.current;
expect(initialShowExitPrompt).toBe(true);
act(() => {
setShowExitPrompt(false);
});
const [updatedShowExitPrompt] = result.current;
expect(updatedShowExitPrompt).toBe(false);
});
});
This test passes, but it doesn't really get to the meat of what the hook is about: when showExitPrompt is true and the page is unloaded, is the exit prompt displayed? (In fact, it really only tests that the useState hook works, which is not what I want at all.) Unfortunately, I've run into a brick wall in trying to figure out how to test this; any suggestions?
I'm checking if a component is unmounted, in order to avoid calling state update functions.
This is the first option, and it works
const ref = useRef(false)
useEffect(() => {
ref.current = true
return () => {
ref.current = false
}
}, [])
....
if (ref.current) {
setAnswers(answers)
setIsLoading(false)
}
....
Second option is using useState, which isMounted is always false, though I changed it to true in component did mount
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
return () => {
setIsMounted(false)
}
}, [])
....
if (isMounted) {
setAnswers(answers)
setIsLoading(false)
}
....
Why is the second option not working compared with the first option?
I wrote this custom hook that can check if the component is mounted or not at the current time, useful if you have a long running operation and the component may be unmounted before it finishes and updates the UI state.
import { useCallback, useEffect, useRef } from "react";
export function useIsMounted() {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
Usage
function MyComponent() {
const [data, setData] = React.useState()
const isMounted = useIsMounted()
React.useEffect(() => {
fetch().then((data) => {
// at this point the component may already have been removed from the tree
// so we need to check first before updating the component state
if (isMounted()) {
setData(data)
}
})
}, [...])
return (...)
}
Live Demo
Please read this answer very carefully until the end.
It seems your component is rendering more than one time and thus the isMounted state will always become false because it doesn't run on every update. It just run once and on unmounted. So, you'll do pass the state in the second option array:
}, [isMounted])
Now, it watches the state and run the effect on every update. But why the first option works?
It's because you're using useRef and it's a synchronous unlike asynchronous useState. Read the docs about useRef again if you're unclear:
This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.
BTW, you do not need to clean up anything. Cleaning up the process is required for DOM changes, third-party api reflections, etc. But you don't need to habit on cleaning up the states. So, you can just use:
useEffect(() => {
setIsMounted(true)
}, []) // you may watch isMounted state
// if you're changing it's value from somewhere else
While you use the useRef hook, you are good to go with cleaning up process because it's related to dom changes.
This is a typescript version of #Nearhuscarl's answer.
import { useCallback, useEffect, useRef } from "react";
/**
* This hook provides a function that returns whether the component is still mounted.
* This is useful as a check before calling set state operations which will generates
* a warning when it is called when the component is unmounted.
* #returns a function
*/
export function useMounted(): () => boolean {
const mountedRef = useRef(false);
useEffect(function useMountedEffect() {
mountedRef.current = true;
return function useMountedEffectCleanup() {
mountedRef.current = false;
};
}, []);
return useCallback(function isMounted() {
return mountedRef.current;
}, [mountedRef]);
}
This is the jest test
import { render, waitFor } from '#testing-library/react';
import React, { useEffect } from 'react';
import { delay } from '../delay';
import { useMounted } from "./useMounted";
describe("useMounted", () => {
it("should work and not rerender", async () => {
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
callback(isMounted())
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
expect(callback.mock.calls).toEqual([[true]])
unmount();
expect(callback.mock.calls).toEqual([[true]])
})
it("should work and not rerender and unmount later", async () => {
jest.useFakeTimers('modern');
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
(async () => {
await delay(10000);
callback(isMounted());
})();
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
await waitFor(() => expect(callback).toBeCalledTimes(0));
jest.advanceTimersByTime(5000);
unmount();
jest.advanceTimersByTime(5000);
await waitFor(() => expect(callback).toBeCalledTimes(1));
expect(callback.mock.calls).toEqual([[false]])
})
})
Sources available in https://github.com/trajano/react-hooks-tests/tree/master/src/useMounted
This cleared up my error message, setting a return in my useEffect cancels out the subscriptions and async tasks.
import React from 'react'
const MyComponent = () => {
const [fooState, setFooState] = React.useState(null)
React.useEffect(()=> {
//Mounted
getFetch()
// Unmounted
return () => {
setFooState(false)
}
})
return (
<div>Stuff</div>
)
}
export {MyComponent as default}
If you want to use a small library for this, then react-tidy has a custom hook just for doing that called useIsMounted:
import React from 'react'
import {useIsMounted} from 'react-tidy'
function MyComponent() {
const [data, setData] = React.useState(null)
const isMounted = useIsMounted()
React.useEffect(() => {
fetchData().then((result) => {
if (isMounted) {
setData(result)
}
})
}, [])
// ...
}
Learn more about this hook
Disclaimer I am the writer of this library.
Near Huscarl solution is good, but there is problem with using these hook with react router, because if you go from example news/1 to news/2 useRef value is set to false because of unmount, but value keep false. So you need init ref value to true on each mount.
import {useRef, useCallback, useEffect} from "react";
export function useIsMounted(): () => boolean {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
isMountedRef.current = true;
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
It's hard to know without the larger context, but I don't think you even need to know whether something has been mounted. useEffect(() => {...}, []) is executed automatically upon mounting, and you can put whatever needs to wait until mounting inside that effect.
I'm trying to test a simple hook i've made for intercepting offline/online events:
import { useEffect } from 'react';
const useOfflineDetection = (
setOffline: (isOffline: boolean) => void
): void => {
useEffect(() => {
window.addEventListener('offline', () => setOffline(true));
window.addEventListener('online', () => setOffline(false));
return () => {
window.removeEventListener('offline', () => setOffline(true));
window.removeEventListener('online', () => setOffline(false));
};
}, []);
};
export default useOfflineDetection;
------------------------------------
//...somewhere else in the code
useOfflineDetection((isOffline: boolean) => Do something with 'isOffline');
But I'm not sure I'm using the correct way to return value and moreover I'm not sure to get how to test it with jest, #testing-library & #testing-library/react-hooks.
I missunderstand how to mount my hook and then catch the return provide by callback.
Is someone can help me ? I'm stuck with it :'(
Thanks in advance!
EDIT:
Like Estus Flask said, I can use useEffect instead callback like I design it first.
import { useEffect, useState } from 'react';
const useOfflineDetection = (): boolean => {
const [isOffline, setIsOffline] = useState<boolean>(false);
useEffect(() => {
window.addEventListener('offline', () => setIsOffline(true));
window.addEventListener('online', () => setIsOffline(false));
return () => {
window.removeEventListener('offline', () => setIsOffline(true));
window.removeEventListener('online', () => setIsOffline(false));
};
}, []);
return isOffline;
};
export default useOfflineDetection;
------------------------------------
//...somewhere else in the code
const isOffline = useOfflineDetection();
Do something with 'isOffline'
But if I want to use this hook in order to store "isOffline" with something like redux or other, the only pattern I see it's using useEffect:
const isOffline = useOfflineDetection();
useEffect(() => {
dispatch(setIsOffline(isOffline));
}, [isOffline])
instead of just:
useOfflineDetection(isOffline => dispatch(setIsOffline(isOffline)));
But is it that bad ?
The problem with the hook is that clean up will fail because addEventListener and removeEventListener callbacks are different. They should be provided with the same functions:
const setOfflineTrue = useCallback(() => setOffline(true), []);
const setOfflineFalse = useCallback(() => setOffline(false), []);
useEffect(() => {
window.addEventListener('offline', setOfflineTrue);
...
Then React Hooks Testing Library can be used to test a hook.
Since DOM event targets have determined behaviour that is supported by Jest DOM to some extent, respective events can be dispatched to test a callback:
const mockSetOffline = jest.fn();
const wrapper = renderHook(() => useOfflineDetection(mockSetOffline));
expect(mockSetOffline).not.toBeCalled();
// called only on events
window.dispatchEvent(new Event('offline'));
expect(mockSetOffline).toBeCalledTimes(1);
expect(mockSetOffline).lastCalledWith(false);
window.dispatchEvent(new Event('online'));
expect(mockSetOffline).toBeCalledTimes(2);
expect(mockSetOffline).lastCalledWith(true);
// listener is registered once
wrapper.rerender();
expect(mockSetOffline).toBeCalledTimes(2);
window.dispatchEvent(new Event('offline'));
expect(mockSetOffline).toBeCalledTimes(3);
expect(mockSetOffline).lastCalledWith(false);
window.dispatchEvent(new Event('online'));
expect(mockSetOffline).toBeCalledTimes(4);
expect(mockSetOffline).lastCalledWith(true);
// cleanup is done correctly
window.dispatchEvent(new Event('offline'));
window.dispatchEvent(new Event('online'));
expect(mockSetOffline).toBeCalledTimes(4);