React + Jest: Test that setTimeOut didn't get called - reactjs

I currently have a setTimeOut inside a useEffect, I was able to test it by using:
jest.useFakeTimers();
jest.advanceTimersByTime(5000);
And it works great. However, the setTimeout is getting triggered a few times as successMessage changes. How can I write a test that checks that setTimeOut is only called when successMessage is not an empty string?
I'm using jest and react-testing-library.
Here is my react code.
enum HELPER_MESSAGES {
SUCCESS = 'Congratulations you signed up!',
ERROR = 'Email address invalid',
}
export function EmailCapture (): ReactElement {
const [inputValue, setInputValue] = useState<string>('');
const [errorMessage, setErrorMessage] = useState<string>('');
const [successMessage, setSuccessMessage] = useState<string>('');
function handleOnClick (): void {
const EMAIL_REGEX: RegExp = /^[a-zA-Z0-9]+([-._][a-zA-Z0-9]+)*#([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*.)+\.+[a-zA-Z]{2,}$/;
const isInputValid: boolean = EMAIL_REGEX.test(inputValue) && inputValue !== '';
if (isInputValid) {
setSuccessMessage(HELPER_MESSAGES.SUCCESS);
} else {
setErrorMessage(HELPER_MESSAGES.ERROR);
}
}
useEffect((): () => void => {
const resetAfterSuccessTimer: NodeJS.Timeout = setTimeout((): void => {
setSuccessMessage('');
}, 5000);
return (): void => {
clearTimeout(resetAfterSuccessTimer);
};
}, [successMessage]);
function handleOnChange (e: ChangeEvent<HTMLInputElement>): void {
setInputValue(e.target.value);
setErrorMessage('');
setSuccessMessage('');
}
return (
<TextInput
onChange={handleOnChange}
error={errorMessage}
success={successMessage}
value={inputValue}
/>
);
}
I'm following TDD, so I'm looking to write a test that will make me write the following code:
useEffect((): () => void => {
let resetAfterSuccessTimer: NodeJS.Timeout;
if (successMessage) {
resetAfterSuccessTimer= setTimeout((): void => {
setSuccessMessage('');
setButtonText('Sign up!');
console.log('sexy');
}, 5000);
}
return (): void => {
clearTimeout(resetAfterSuccessTimer);
};
}, [successMessage]);
The sexy word should only get console logged once.

Figured it out based on their documentation: https://jestjs.io/docs/timer-mocks , you can just do the following:
expect(setTimeout).not.toHaveBeenCalled();
And when you want to check that it got called once, you can do:
expect(setTimeout).toHaveBeenCalledTimes(1);
Note: This is testing an implementation detail, so be aware of that.

Related

Lexicaljs receive editor state json and text content using debounce in react project

Requirement
I have a requirement to get the editor state in JSON format as well as the text content of the editor. In addition, I want to receive these values in the debounced way.
I wanted to get these values (as debounced) because I wanted to send them to my server.
Dependencies
"react": "^18.2.0",
"lexical": "^0.3.8",
"#lexical/react": "^0.3.8",
You don't need to touch any of Lexical's internals for this; a custom hook that reads and "stashes" the editor state into a ref and sets up a debounced callback (via use-debounce here, but you can use whatever implementation you like) is enough.
getEditorState is in charge of converting the editor state into whichever format you want to send over the wire. It's always called within editorState.read().
function useDebouncedLexicalOnChange<T>(
getEditorState: (editorState: EditorState) => T,
callback: (value: T) => void,
delay: number
) {
const lastPayloadRef = React.useRef<T | null>(null);
const callbackRef = React.useRef<(arg: T) => void | null>(callback);
React.useEffect(() => {
callbackRef.current = callback;
}, [callback]);
const callCallbackWithLastPayload = React.useCallback(() => {
if (lastPayloadRef.current) {
callbackRef.current?.(lastPayloadRef.current);
}
}, []);
const call = useDebouncedCallback(callCallbackWithLastPayload, delay);
const onChange = React.useCallback(
(editorState) => {
editorState.read(() => {
lastPayloadRef.current = getEditorState(editorState);
call();
});
},
[call, getEditorState]
);
return onChange;
}
// ...
const getEditorState = (editorState: EditorState) => ({
text: $getRoot().getTextContent(false),
stateJson: JSON.stringify(editorState)
});
function App() {
const debouncedOnChange = React.useCallback((value) => {
console.log(new Date(), value);
// TODO: send to server
}, []);
const onChange = useDebouncedLexicalOnChange(
getEditorState,
debouncedOnChange,
1000
);
// ...
<OnChangePlugin onChange={onChange} />
}
Code
File: onChangeDebouce.tsx
import {$getRoot} from "lexical";
import { useLexicalComposerContext } from "#lexical/react/LexicalComposerContext";
import React from "react";
const CAN_USE_DOM = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined';
const useLayoutEffectImpl = CAN_USE_DOM ? React.useLayoutEffect : React.useEffect;
var useLayoutEffect = useLayoutEffectImpl;
type onChangeFunction = (editorStateJson: string, editorText: string) => void;
export const OnChangeDebounce: React.FC<{
ignoreInitialChange?: boolean;
ignoreSelectionChange?: boolean;
onChange: onChangeFunction;
wait?: number
}> = ({ ignoreInitialChange= true, ignoreSelectionChange = false, onChange, wait= 167 }) => {
const [editor] = useLexicalComposerContext();
let timerId: NodeJS.Timeout | null = null;
useLayoutEffect(() => {
return editor.registerUpdateListener(({
editorState,
dirtyElements,
dirtyLeaves,
prevEditorState
}) => {
if (ignoreSelectionChange && dirtyElements.size === 0 && dirtyLeaves.size === 0) {
return;
}
if (ignoreInitialChange && prevEditorState.isEmpty()) {
return;
}
if(timerId === null) {
timerId = setTimeout(() => {
editorState.read(() => {
const root = $getRoot();
onChange(JSON.stringify(editorState), root.getTextContent());
})
}, wait);
} else {
clearTimeout(timerId);
timerId = setTimeout(() => {
editorState.read(() => {
const root = $getRoot();
onChange(JSON.stringify(editorState), root.getTextContent());
});
}, wait);
}
});
}, [editor, ignoreInitialChange, ignoreSelectionChange, onChange]);
return null;
}
This is the code for the plugin and it is inspired (or copied) from OnChangePlugin of lexical
Since, lexical is in early development the implementation of OnChangePlugin might change. And in fact, there is one more parameter added as of version 0.3.8. You can check the latest code at github.
The only thing I have added is calling onChange function in timer logic.
ie.
if(timerId === null) {
timerId = setTimeout(() => {
editorState.read(() => {
const root = $getRoot();
onChange(JSON.stringify(editorState), root.getTextContent());
})
}, wait);
} else {
clearTimeout(timerId);
timerId = setTimeout(() => {
editorState.read(() => {
const root = $getRoot();
onChange(JSON.stringify(editorState), root.getTextContent());
});
}, wait);
}
If you are new to lexical, then you have to use declare this plugin as a child of lexical composer, something like this.
File: RichEditor.tsx
<LexicalComposer initialConfig={getRichTextConfig(namespace)}>
<div className="editor-shell lg:m-2" ref={scrollRef}>
<div className="editor-container">
{/* your other plugins */}
<RichTextPlugin
contentEditable={<ContentEditable
className={"ContentEditable__root"} />
}
placeholder={<Placeholder text={placeHolderText} />}
/>
<OnChangeDebounce onChange={onChange} />
</div>
</div>
</LexicalComposer>
In this code, as you can see I have passed the onChange function as a prop and you can also pass wait in milliseconds like this.
<OnChangeDebounce onChange={onChange} wait={1000}/>
Now the last bit is the implementation of onChange function, which is pretty straightforward
const onChange = (editorStateJson:string, editorText:string) => {
console.log("editorStateJson:", editorStateJson);
console.log("editorText:", editorText);
// send data to a server or to your data store (eg. redux)
};
Finally
Thanks to Meta and the lexical team for open sourcing this library. And lastly, the code I have provided works for me, I am no expert, feel free to comment to suggest an improvement.

How to make a custom debounce hook works with a useCallback?

I did search for those related issues and found some solutions, but most about the lodash debounce. In my case, I create useDebounce as a custom hook and return the value directly.
My current issue is useCallback works with an old debounced value.
Here are my code snips.
//To makes sure that the code is only triggered once per user input and send the request then.
export const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timeout = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timeout);
}, [value, delay]);
return debouncedValue;
};
useDebounce works as expected
export const ShopQuantityCounter = ({ id, qty }) => {
const [value, setValue] = useState(qty);
const debounceInput = useDebounce(value, 300);
const dispatch = useDispatch();
const handleOnInputChange = useCallback((e) => {
setValue(e.target.value);
console.info('Inside OnChange: debounceInput', debounceInput);
// dispatch(updateCartItem({ id: id, quantity: debounceInput }));
},[debounceInput]);
console.info('Outside OnChange: debounceInput', debounceInput);
// To fixed issue that useState set method not reflecting change immediately
useEffect(() => {
setValue(qty);
}, [qty]);
return (
<div className="core-cart__quantity">
<input
className="core-cart__quantity--total"
type="number"
step="1"
min="1"
title="Qty"
value={value}
pattern="^[0-9]*[1-9][0-9]*$"
onChange={handleOnInputChange}
/>
</div>
);
};
export default ShopQuantityCounter;
Here are screenshots with console.info to explain what the issue is.
Current quantity
Updated with onChange
I do appreciate it if you have any solution to fix it, and also welcome to put forward any code that needs updates.
This might help you achieve what you want. You can create a reusable debounce function with the callback like below.
export const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
let timeout;
const setDebounce = (newValue) => {
clearTimeout(timeout);
timeout = setTimeout(() => setDebouncedValue(newValue), delay);
};
return [debouncedValue, setDebounce];
};
And use the function on your code like this.
export const ShopQuantityCounter = ({ id, qty }) => {
const [value, setValue] = useState(qty);
const [debounceInput, setDebounceInput] = useDebounce(value, 300);
const dispatch = useDispatch();
const handleOnInputChange = useCallback((e) => {
setDebounceInput(e.target.value);
console.info('Inside OnChange: debounceInput', debounceInput);
// dispatch(updateCartItem({ id: id, quantity: debounceInput }));
},[debounceInput]);
console.info('Outside OnChange: debounceInput', debounceInput);
// To fixed issue that useState set method not reflecting change immediately
useEffect(() => {
setValue(qty);
}, [qty]);
return (
<div className="core-cart__quantity">
<input
className="core-cart__quantity--total"
type="number"
step="1"
min="1"
title="Qty"
value={value}
pattern="^[0-9]*[1-9][0-9]*$"
onChange={handleOnInputChange}
/>
</div>
);
};
export default ShopQuantityCounter;

How to re write code without react.usememo

I am new to react hooks. I need to rewrite following snippet without hooks.Please help me.
const subHeaderComponentMemo = React.useMemo(() => {
const handleClear = () => {
if (filterText) {
setResetPaginationToggle(!resetPaginationToggle);
setFilterText('');
}
};
return <FilterComponent onFilter={e => setFilterText(e.target.value)} onClear={handleClear} filterText={filterText} />;
}, [filterText, resetPaginationToggle]);

react hooks setTimeout after setState

I recently wanted to design an input component with react hooks.
The component would check validation after entering input in 0.5 second.
my code like
const inputField = ({ name, type, hint, inputValue, setInput }) => {
// if there is default value, set default value to state
const [value, setValue] = useState(inputValue);
// all of validation are true for testing
const validCheck = () => true;
let timeout;
const handleChange = e => {
clearTimeout(timeout);
const v = e.target.value;
setValue(v);
timeout = setTimeout(() => {
// if valid
if (validCheck()) {
// do something...
}
}, 500);
};
return (
<SCinputField>
<input type={type} onChange={handleChange} />
</SCinputField>
);
};
unfortunately, it's not worked, because the timeout variable would renew every time after setValue.
I found react-hooks provide some feature like useRef to store variable.
Should I use it or shouldn't use react-hooks in this case?
Update
add useEffect
const inputField = ({ name, type, hint, inputValue, setInput }) => {
// if there is default value, set default value to state
const [value, setValue] = useState(inputValue);
// all of validation are true for testing
const validCheck = () => true;
let timeout;
const handleChange = e => {
const v = e.target.value;
setValue(v);
};
// handle timeout
useEffect(() => {
let timeout;
if (inputValue !== value) {
timeout = setTimeout(() => {
const valid = validCheck(value);
console.log('fire after a moment');
setInput({
key: name,
valid,
value
});
}, 1000);
}
return () => {
clearTimeout(timeout);
};
});
return (
<SCinputField>
<input type={type} onChange={handleChange} />
</SCinputField>
);
};
It looks worked, but I am not sure about it's a right way to use.
Here's how I would do it:
import React, {useState, useEffect, useRef} from 'react';
function InputField() {
const [value, setValue] = useState(''); // STATE FOR THE INPUT VALUE
const timeoutRef = useRef(null); // REF TO KEEP TRACK OF THE TIMEOUT
function validate() { // VALIDATE FUNCTION
console.log('Validating after 500ms...');
}
useEffect(() => { // EFFECT TO RUN AFTER CHANGE IN VALUE
if (timeoutRef.current !== null) { // IF THERE'S A RUNNING TIMEOUT
clearTimeout(timeoutRef.current); // THEN, CANCEL IT
}
timeoutRef.current = setTimeout(()=> { // SET A TIMEOUT
timeoutRef.current = null; // RESET REF TO NULL WHEN IT RUNS
value !== '' ? validate() : null; // VALIDATE ANY NON-EMPTY VALUE
},500); // AFTER 500ms
},[value]); // RUN EFFECT AFTER CHANGE IN VALUE
return( // SIMPLE TEXT INPUT
<input type='text'
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
WORKING EXAMPLE ON SNIPPET BELOW:
function InputField() {
const [value, setValue] = React.useState('');
const timeoutRef = React.useRef(null);
function validate() {
console.log('Validating after 500ms...');
}
React.useEffect(() => {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(()=> {
timeoutRef.current = null;
value !== '' ? validate() : null;
},500);
},[value]);
return(
<input type='text' value={value} onChange={(e) => setValue(e.target.value)}/>
);
}
ReactDOM.render(<InputField/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
You don't need to keep the reference to the timeout between renders. You can just return a function from the useEffect to clear it:
React.useEffect(() => {
const timeout = setTimeout(()=> {
if (value !== '') {
validate();
}
}, 500);
return () => {
clearTimeout(timeout); // this guarantees to run right before the next effect
}
},[value, validate]);
Also, don't forget to pass all the dependencies to the effect, including the validate function.
Ideally, you would pass the value as a parameter to the validate function: validate(value) - this way, the function has fewer dependencies, and could even be pure and moved outside the component.
Alternatively, if you have internal dependencies (like another setState or an onError callback from props), create the validate function with a useCallback() hook :
const validate = useCallback((value) => {
// do something with the `value` state
if ( /* value is NOT valid */ ) {
onError(); // call the props for an error
} else {
onValid();
}
}, [onError, onValid]); // and any other dependencies your function may use
This will keep the same function reference between the renders if the dependencies don't change.
You can move timeout variable inside handleChange method.
const inputField = ({ name, type, hint, inputValue, setInput }) => {
// if there is default value, set default value to state
const [value, setValue] = useState(inputValue);
// all of validation are true for testing
const validCheck = () => true;
const handleChange = e => {
let timeout;
clearTimeout(timeout);
const v = e.target.value;
setValue(v);
timeout = setTimeout(() => {
// if valid
if (validCheck()) {
// do something...
}
}, 500);
};
return (
<SCinputField>
<input type={type} onChange={handleChange} />
</SCinputField>
);
};

Lodash debounce with React Input

I'm trying to add debouncing with lodash to a search function, called from an input onChange event. The code below generates a type error 'function is expected', which I understand because lodash is expecting a function. What is the right way to do this and can it be done all inline? I have tried nearly every example thus far on SO to no avail.
search(e){
let str = e.target.value;
debounce(this.props.relay.setVariables({ query: str }), 500);
},
With a functional react component try using useCallback. useCallback memoizes your debounce function so it doesn't get recreated again and again when the component rerenders. Without useCallback the debounce function will not sync with the next key stroke.
`
import {useCallback} from 'react';
import _debounce from 'lodash/debounce';
import axios from 'axios';
function Input() {
const [value, setValue] = useState('');
const debounceFn = useCallback(_debounce(handleDebounceFn, 1000), []);
function handleDebounceFn(inputValue) {
axios.post('/endpoint', {
value: inputValue,
}).then((res) => {
console.log(res.data);
});
}
function handleChange (event) {
setValue(event.target.value);
debounceFn(event.target.value);
};
return <input value={value} onChange={handleChange} />
}
`
The debounce function can be passed inline in the JSX or set directly as a class method as seen here:
search: _.debounce(function(e) {
console.log('Debounced Event:', e);
}, 1000)
Fiddle: https://jsfiddle.net/woodenconsulting/69z2wepo/36453/
If you're using es2015+ you can define your debounce method directly, in your constructor or in a lifecycle method like componentWillMount.
Examples:
class DebounceSamples extends React.Component {
constructor(props) {
super(props);
// Method defined in constructor, alternatively could be in another lifecycle method
// like componentWillMount
this.search = _.debounce(e => {
console.log('Debounced Event:', e);
}, 1000);
}
// Define the method directly in your class
search = _.debounce((e) => {
console.log('Debounced Event:', e);
}, 1000)
}
This is how I had to do it after googling the whole day.
const MyComponent = (props) => {
const [reload, setReload] = useState(false);
useEffect(() => {
if(reload) { /* Call API here */ }
}, [reload]);
const callApi = () => { setReload(true) }; // You might be able to call API directly here, I haven't tried
const [debouncedCallApi] = useState(() => _.debounce(callApi, 1000));
function handleChange() {
debouncedCallApi();
}
return (<>
<input onChange={handleChange} />
</>);
}
That's not so easy question
On one hand to just work around error you are getting, you need to wrap up you setVariables in the function:
search(e){
let str = e.target.value;
_.debounce(() => this.props.relay.setVariables({ query: str }), 500);
}
On another hand, I belive debouncing logic has to be incapsulated inside Relay.
A lot of the answers here I found to be overly complicated or just inaccurate (i.e. not actually debouncing). Here's a straightforward solution with a check:
const [count, setCount] = useState(0); // simple check debounce is working
const handleChangeWithDebounce = _.debounce(async (e) => {
if (e.target.value && e.target.value.length > 4) {
// TODO: make API call here
setCount(count + 1);
console.log('the current count:', count)
}
}, 1000);
<input onChange={handleChangeWithDebounce}></input>
Improving on this answer: https://stackoverflow.com/a/67941248/2390312
Using useCallback and debounce is known to cause an eslint exhaustive deps warning.
Here's how to do it with functional components and useMemo
import { useMemo } from 'react';
import { debounce } from 'lodash';
import axios from 'axios';
function Input() {
const [value, setValue] = useState('');
const debounceFn = useMemo(() => debounce(handleDebounceFn, 1000), []);
function handleDebounceFn(inputValue) {
axios.post('/endpoint', {
value: inputValue,
}).then((res) => {
console.log(res.data);
});
}
function handleChange (event) {
setValue(event.target.value);
debounceFn(event.target.value);
};
return <input value={value} onChange={handleChange} />
}
We are using useMemo to return a memoized value, where this value is the function returned by debounce
Some answers are neglecting that if you want to use something like e.target.value from the event object (e), the original event values will be null when you pass it through your debounce function.
See this error message:
Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property nativeEvent on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist().
As the message says, you have to include e.persist() in your event function. For example:
const onScroll={(e) => {
debounceFn(e);
e.persist();
}}
Then of course, your debounceFn needs to be scoped outside of the return statement in order to utilize React.useCallback(), which is necessary. My debounceFn looks like this:
const debounceFn = React.useCallback(
_.debounce((e) =>
calculatePagination(e),
500, {
trailing: true,
}
),
[]
);
#Aximili
const [debouncedCallApi] = useState(() => _.debounce(callApi, 1000));
looks strange :) I prefare solutions with useCallback:
const [searchFor, setSearchFor] = useState('');
const changeSearchFor = debounce(setSearchFor, 1000);
const handleChange = useCallback(changeSearchFor, []);
for your case, it should be:
search = _.debounce((e){
let str = e.target.value;
this.props.relay.setVariables({ query: str });
}, 500),
class MyComp extends Component {
debounceSave;
constructor(props) {
super(props);
}
this.debounceSave = debounce(this.save.bind(this), 2000, { leading: false, trailing: true });
}
save() is the function to be called
debounceSave() is the function you actually call (multiple times).
This worked for me:
handleChange(event) {
event.persist();
const handleChangeDebounce = _.debounce((e) => {
if (e.target.value) {
// do something
}
}, 1000);
handleChangeDebounce(event);
}
This is the correct FC approach
#
Aximili answers triggers only one time
import { SyntheticEvent } from "react"
export type WithOnChange<T = string> = {
onChange: (value: T) => void
}
export type WithValue<T = string> = {
value: T
}
// WithValue & WithOnChange
export type VandC<A = string> = WithValue<A> & WithOnChange<A>
export const inputValue = (e: SyntheticEvent<HTMLElement & { value: string }>): string => (e.target as HTMLElement & { value: string }).value
const MyComponent: FC<VandC<string>> = ({ onChange, value }) => {
const [reload, setReload] = useState(false)
const [state, setstate] = useState(value)
useEffect(() => {
if (reload) {
console.log('called api ')
onChange(state)
setReload(false)
}
}, [reload])
const callApi = () => {
setReload(true)
} // You might be able to call API directly here, I haven't tried
const [debouncedCallApi] = useState(() => _.debounce(callApi, 1000))
function handleChange(x:string) {
setstate(x)
debouncedCallApi()
}
return (<>
<input
value={state} onChange={_.flow(inputValue, handleChange)} />
</>)
}
const delayedHandleChange = debounce(eventData => someApiFunction(eventData), 500);
const handleChange = (e) => {
let eventData = { id: e.id, target: e.target };
delayedHandleChange(eventData);
}

Resources