How can I clean up function like setTimeout or setInterval in event handler in React? Or is this unnecessary to do so?
import React from 'react'
function App(){
return (
<button onClick={() => {
setTimeout(() => {
console.log('you have clicked me')
//How to clean this up?
}, 500)
}}>Click me</button>
)
}
export default App
Whether it's necessary depends on what the callback does, but certainly if the component is unmounted it almost doesn't matter what it does, you do need to cancel the timer / clear the interval.
To do that in a function component like yours, you use a useEffect cleanup function with an empty dependency array. You probably want to store the timer handle in a ref.
(FWIW, I'd also define the function outside of the onClick attribute, just for clarity.)
import React, {useEffect, useRef} from 'react';
function App() {
const instance = useRef({timer: 0});
useEffect(() => {
// What you return is the cleanup function
return () => {
clearTimeout(instance.current.timer);
};
}, []);
const onClick = () => {
// Clear any previous one (it's fine if it's `0`,
// `clearTimeout` won't do anything)
clearTimeout(instance.current.timer);
// Set the timeout and remember the value on the object
instance.current.timer = setTimeout(() => {
console.log('you have clicked me')
//How to clean this up?
}, 500);
};
return (
<button onClick={onClick}>Click me</button>
)
}
export default App;
An object you store as a ref is usually a useful place to put things you would otherwise have put on this in a class component.
(If you want to avoid re-rendering button when other state in your component changes (right now there's no other state, so no need), you could use useCallback for onClick so button always sees the same function.)
One more solution (Live Demo):
import React, { useState } from "react";
import { useAsyncCallback } from "use-async-effect2";
import { CPromise } from "c-promise2";
export default function TestComponent(props) {
const [text, setText] = useState("");
const click = useAsyncCallback(function* (ms) {
yield CPromise.delay(ms);
setText("done!" + new Date().toLocaleTimeString());
}, []);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button onClick={() => click(2000)}>Click me!</button>
<button onClick={click.cancel}>Cancel scheduled task</button>
</div>
);
}
In case if you want to cancel the previous pending task (Live demo):
import React, { useState } from "react";
import { useAsyncCallback } from "use-async-effect2";
import { CPromise } from "c-promise2";
export default function TestComponent(props) {
const [text, setText] = useState("");
const click = useAsyncCallback(
function* (ms) {
console.log("click");
yield CPromise.delay(ms);
setText("done!" + new Date().toLocaleTimeString());
},
{ deps: [], cancelPrevios: true }
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button onClick={() => click(5000)}>Click me!</button>
<button onClick={click.cancel}>Cancel scheduled task</button>
</div>
);
}
Clear timer when unmount component
import React from 'react'
function App(){
const timerRef = React.useRef(null)
React.useEffect(() => {
return () => {
// clean
timerRef.target && clearTimeout(timerRef.target)
}
},[])
return (
<button onClick={() => {
timerRef.target = setTimeout(() => {
console.log('you have clicked me')
}, 500)
}}>Click me</button>
)
}
export default App
Related
The useEffect would run more than once for some reason (usually twice) and would print my message twice (or more). I have tried multiple solutions even with useMontainEffect but the result is always the same. Any solutions?
import './App.css';
import io from 'socket.io-client'
import { useEffect, useRef, useState } from 'react'
import React from 'react';
import ReactDOM from "react-dom/client";
const socket = io.connect("http://localhost:3001");
function App() {
const [message, setMessage] = useState("");
const [state, setState] = useState([]);
const [chat, setChat] = useState([]);
const socketRef = useRef();
const sendMessage = () => {
socket.emit("send_message", { message });
};
const renderChat = () => {
return (
chat.map(msg => {
return (
<h3>{msg.message['message']}</h3>
)
})
)
}
useEffect(() => {
socket.on("receive_message", message => {
setChat(prevState => [...prevState, {message}]);
});
}, [socket])
return (
<div className="App">
<input placeholder="Message..." onChange={(event) => {
setMessage(event.target.value);}}
/>
<button onClick={sendMessage}>Send Message</button>
<h1>Message:</h1>
{renderChat()}
</div>
);
}
export default App;
In strict mode the component will be mounted, unmounted, then re-mounted. You can add a cleanup function. The double invocation however is expected.
useEffect(() => {
const listener = message => {
setChat(prevState => [...prevState, {message}]);
};
socket.on("receive_message", listener);
return () => socket.off('receive_message', listener);
}, [socket])
If u want to check, U can turn off the strict mode and check exactly how many times the useEffect call back function runs
Note : but don't always turn off the strict mode because
--> strict mode is actually because if we forgot any cleanup function in useEffect , ui behaves differently
--> with that only we came to know that we did something mistake
--> strict mode is present in only development mode, it won't be in production mode
Here's the UI
Where I click the first button, then click the second button, it shows value 1, but I expect it to show value 2, since I set the value to 2. What's the problem and how should I address that?
Here's the code:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import React, {
useState,
useEffect,
useMemo,
useRef,
useCallback
} from "react";
const App = () => {
const [channel, setChannel] = useState(null);
const handleClick = useCallback(() => {
console.log(channel);
}, [channel]);
const parentClick = () => {
console.log("parent is call");
setChannel(2);
};
useEffect(() => {
setChannel(1);
});
return (
<div className="App">
<button onClick={parentClick}>Click to SetChannel 2</button>
<button onClick={handleClick}>Click to ShowChannel 2</button>
</div>
);
};
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(<App />);
Here's the codesandbox
Add a dependency to the useEffect hook, if you don't add any dependency it will just re-run on every state change.
Change this:
useEffect(() => {
setChannel(1);
});
To this:
useEffect(() => {
setChannel(1);
}, []);
useEffect(() => {
setChannel(1);
});
Runs on every render, so it's always reverting back to 1
Your problem is that you are setting channel value to 1 every render.
You have 2 options.
Set initial state value of channel to 1 (see below)
Call this.setState({channel: 1}) in the componentDidMount method.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {channel: 1};
}
handleClick=(evt)=> {
console.log(this.state.channel);
}
parentClick=(evt)=> {
this.setState({channel: 2});
}
render() {
return (
<div className="App">
<button onClick={this.parentClick}>Click to SetChannel 2</button>
<br /><br />
<button onClick={this.handleClick}>Click to ShowChannel 2</button>
</div>
);
}
}
PS: It's not clear what you're trying to do & your sandbox is quite different from the code you have posted here.
I am trying to implement a search that makes a new query on each character change. After n milliseconds, I need to make a change to the object that stores some properties.
//user typing
const onInputChange = (e) => {
let searchInput = e.target.value;
useDebounce(
handleSearchPropsChange({
filter: {
searchInput,
dateRange: {
start,
end
}
}
}), 1000
);
}
The function I am using for the delayed call
import {debounce} from 'lodash';
import {useRef} from 'react';
export function useDebounce(callback = () => {}, time = 500) {
return useRef(debounce(callback, time)).current;
}
But I am getting the error:
Invalid hook call. Hooks can only be called inside of the body of a function component. This
could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
A example without lodash, just Hooks.
UseDebounce.js
import { useEffect, useCallback } from 'react';
export default function useDebounce(effect, dependencies, delay) {
const callback = useCallback(effect, dependencies);
useEffect(() => {
const timeout = setTimeout(callback, delay);
return () => clearTimeout(timeout);
}, [callback, delay]);
}
App.js
import React, { useState } from 'react';
import useDebounce from './useDebounce';
import data from './data';
export default function App() {
const [search, setSearch] = useState('');
const [filteredTitle, setFilteredTitle] = useState([]);
// DeBounce Function
useDebounce(() => {
setFilteredTitle(
data.filter((d) => d.title.toLowerCase().includes(search.toLowerCase()))
);
}, [data, search], 800
);
const handleSearch = (e) => setSearch(e.target.value);
return (
<>
<input
id="search"
type="text"
spellCheck="false"
placeholder="Search a Title"
value={search || ''}
onChange={handleSearch}
/>
<div>
{filteredTitle.map((f) => (
<p key={f.id}>{f.title}</p>
))}
</div>
</>
);
}
Demo : Stackblitz
I am trying to spy on useState React hook but i always get the test failed
This is my React component:
const Counter= () => {
const[counter, setCounter] = useState(0);
const handleClick=() => {
setCounter(counter + 1);
}
return (
<div>
<h2>{counter}</h2>
<button onClick={handleClick} id="button">increment</button>
</div>
)
}
counter.test.js:
it('increment counter correctlry', () => {
let wrapper = shallow(<Counter/>);
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, 'useState');
useStateSpy.mockImplementation((init) => [init, setState]);
const button = wrapper.find("button")
button.simulate('click');
expect(setState).toHaveBeenCalledWith(1);
})
Unfortunately this doesn't work and i get the test failed with that message:
expected 1
Number of calls: 0
diedu's answer led me the right direction and I came up with this solution:
Mock use state from react to return a jest.fn() as useState:
1.1 Also import useState immediately after - that will now be e jest mock (returned from the jest.fn() call)
jest.mock('react', ()=>({
...jest.requireActual('react'),
useState: jest.fn()
}))
import { useState } from 'react';
Later on in the beforeEach, set it to the original useState, for all the cases where you need it to not be mocked
describe("Test", ()=>{
beforeEach(()=>{
useState.mockImplementation(jest.requireActual('react').useState);
//other preperations
})
//tests
})
In the test itself mock it as needed:
it("Actual test", ()=>{
useState.mockImplementation(()=>["someMockedValue", someMockOrSpySetter])
})
Parting notes: While it might be conceptually somewhat wrong to get your hands dirty inside the "black box" one is unit testing, it is indeed super useful at times to do it.
You need to use React.useState instead of the single import useState.
I think is about how the code gets transpiled, as you can see in the babel repl the useState from the single import ends up being different from the one of the module import
_react.useState // useState
_react.default.useState // React.useState;
So you spy on _react.default.useState but your component uses _react.useState.
It seems impossible to spyOn a single import since you need the function to belong to an object, here is a very extensive guide that explains the ways of mocking/spying modules https://github.com/HugoDF/mock-spy-module-import
And as #Alex Mackay mentioned, you probably want to change your mindset about testing react components, moving to react-testing-library is recommended, but if you really need to stick to enzyme you don't need to go that far as to mock react library itself
Annoyingly Codesandbox is currently having trouble with its testing module so I can't post a working example but I will try to explain why mocking useState is generally a bad thing to do.
The user doesn't care if useState has been called, they care about when I click increment the count should increase by one therefore that is what you should be testing for.
// App
import React, { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
</div>
);
}
// Tests
import React from "react";
import App from "./App";
import { screen, render } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
describe("App should", () => {
it('increment count value when "Increment" btn clicked', () => {
// Render the App
render(<App />);
// Get the count in the same way the user would, by looking for 'Count'
let count = screen.getByText(/count:/);
// As long as the h1 element contains a '0' this test will pass
expect(count).toContain(0);
// Once again get the button in the same the user would, by the 'Increment'
const button = screen.getByText(/increment/);
// Simulate the click event
userEvent.click(button);
// Refetch the count
count = screen.getByText(/count:/);
// The 'Count' should no longer contain a '0'
expect(count).not.toContain(0);
// The 'Count' should contain a '1'
expect(count).toContain(1);
});
// And so on...
it('reset count value when "Reset" btn is clicked', () => {});
it('decrement count value when "Decrement" btn is clicked', () => {});
});
Definitely check out #testing-library if you are interested in this style of testing. I switched from enzyme about 2 years ago and haven't touched it since.
just you need to import React in your test file like:
import * as React from 'react';
after that you can use the mock function.
import * as React from 'react';
:
:
it('increment counter correctlry', () => {
let wrapper = shallow(<Counter/>);
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, 'useState');
useStateSpy.mockImplementation((init) => [init, setState]);
const button = wrapper.find("button")
button.simulate('click');
expect(setState).toHaveBeenCalledWith(1);
})
you should use React.useState() instead useState(), But there are other ways...
in React you can set useState without React with this config
// setupTests.js
const { configure } = require('enzyme')
const Adapter = require('#wojtekmaj/enzyme-adapter-react-17')
const { createSerializer } = require('enzyme-to-json')
configure({ adapter: new Adapter() });
expect.addSnapshotSerializer(createSerializer({
ignoreDefaultProps: true,
mode: 'deep',
noKey: true,
}));
import React, { useState } from "react";
const Home = () => {
const [count, setCount] = useState(0);
return (
<section>
<h3>{count}</h3>
<span>
<button id="count-up" type="button" onClick={() => setCount(count + 1)}>Count Up</button>
<button id="count-down" type="button" onClick={() => setCount(count - 1)}>Count Down</button>
<button id="zero-count" type="button" onClick={() => setCount(0)}>Zero</button>
</span>
</section>
);
}
export default Home;
// index.test.js
import { mount } from 'enzyme';
import Home from '../';
import React, { useState as useStateMock } from 'react';
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn(),
}));
describe('<Home />', () => {
let wrapper;
const setState = jest.fn();
beforeEach(() => {
useStateMock.mockImplementation(init => [init, setState]);
wrapper = mount(<Home />);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Count Up', () => {
it('calls setCount with count + 1', () => {
wrapper.find('#count-up').simulate('click');
expect(setState).toHaveBeenCalledWith(1);
});
});
describe('Count Down', () => {
it('calls setCount with count - 1', () => {
wrapper.find('#count-down').props().onClick();
expect(setState).toHaveBeenCalledWith(-1);
});
});
describe('Zero', () => {
it('calls setCount with 0', () => {
wrapper.find('#zero-count').props().onClick();
expect(setState).toHaveBeenCalledWith(0);
});
});
});
We have a React app which communicates with a third party library for phone integration. Whenever someone calls, the third-party library triggers a callback function inside the React app. That has been fine until now, but now this callback function needs to access the current state which seems to pose a problem. The state inside of this callback function, seems to always be at the initial value and never updates.
I have made a small sandbox here to describe the problem: https://codesandbox.io/s/vigorous-panini-0kge6?file=/src/App.js
In the sandbox, the counter value is updated correctly when I click "Internal increase". However, the same function has been added as a callback to ThirdPartyApi, which is called when I click "External increase". When I do that, the counter value reverts to whatever is the default in useState.
How can I make the third library be aware of state updates from inside React?
App.js:
import React, { useState, useEffect } from "react";
import ThirdPartyApi from "./third-party-api";
import "./styles.css";
let api = new ThirdPartyApi();
export default function App() {
const [counter, setCounter] = useState(5);
const increaseCounter = () => {
setCounter(counter + 1);
console.log(counter);
};
useEffect(() => {
api.registerCallback(increaseCounter);
}, []);
return (
<div className="App">
<p>
<button onClick={() => increaseCounter()}>Internal increase</button>
</p>
<p>
<button onClick={() => api.triggerCallback()}>External increase</button>
</p>
</div>
);
}
third-party-api.js:
export default class ThirdPartyApi {
registerCallback(callback) {
this.callback = callback;
}
triggerCallback() {
this.callback();
}
}
You need to wrap increaseCounter() into a callback via React's useCallback.
As it is, api.registerCallback() rerenders because of it, resetting counter.
You can learn more about this behavior here.
import React, { useState, useCallback, useEffect } from "react";
import ReactDOM from "react-dom";
class ThirdPartyApi {
registerCallback(callback) {
this.callback = callback;
}
triggerCallback() {
this.callback();
}
}
let api = new ThirdPartyApi();
function App() {
const [counter, setCounter] = useState(5);
const increaseCounter = useCallback(() => {
setCounter(counter + 1);
console.log(counter);
}, [counter]);
useEffect(() => {
api.registerCallback(increaseCounter);
}, [increaseCounter]);
return (
<div className="App">
<p>
<button onClick={() => increaseCounter()}>Internal increase</button>
</p>
<p>
<button onClick={() => api.triggerCallback()}>External increase</button>
</p>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);