how to test react-select with react-testing-library - reactjs

App.js
import React, { Component } from "react";
import Select from "react-select";
const SELECT_OPTIONS = ["FOO", "BAR"].map(e => {
return { value: e, label: e };
});
class App extends Component {
state = {
selected: SELECT_OPTIONS[0].value
};
handleSelectChange = e => {
this.setState({ selected: e.value });
};
render() {
const { selected } = this.state;
const value = { value: selected, label: selected };
return (
<div className="App">
<div data-testid="select">
<Select
multi={false}
value={value}
options={SELECT_OPTIONS}
onChange={this.handleSelectChange}
/>
</div>
<p data-testid="select-output">{selected}</p>
</div>
);
}
}
export default App;
App.test.js
import React from "react";
import {
render,
fireEvent,
cleanup,
waitForElement,
getByText
} from "react-testing-library";
import App from "./App";
afterEach(cleanup);
const setup = () => {
const utils = render(<App />);
const selectOutput = utils.getByTestId("select-output");
const selectInput = document.getElementById("react-select-2-input");
return { selectOutput, selectInput };
};
test("it can change selected item", async () => {
const { selectOutput, selectInput } = setup();
getByText(selectOutput, "FOO");
fireEvent.change(selectInput, { target: { value: "BAR" } });
await waitForElement(() => getByText(selectOutput, "BAR"));
});
This minimal example works as expected in the browser but the test fails. I think the onChange handler in is not invoked. How can I trigger the onChange callback in the test? What is the preferred way to find the element to fireEvent at? Thank you

In my project, I'm using react-testing-library and jest-dom.
I ran into same problem - after some investigation I found solution, based on thread: https://github.com/airbnb/enzyme/issues/400
Notice that the top-level function for render has to be async, as well as individual steps.
There is no need to use focus event in this case, and it will allow to select multiple values.
Also, there has to be async callback inside getSelectItem.
const DOWN_ARROW = { keyCode: 40 };
it('renders and values can be filled then submitted', async () => {
const {
asFragment,
getByLabelText,
getByText,
} = render(<MyComponent />);
( ... )
// the function
const getSelectItem = (getByLabelText, getByText) => async (selectLabel, itemText) => {
fireEvent.keyDown(getByLabelText(selectLabel), DOWN_ARROW);
await waitForElement(() => getByText(itemText));
fireEvent.click(getByText(itemText));
}
// usage
const selectItem = getSelectItem(getByLabelText, getByText);
await selectItem('Label', 'Option');
( ... )
}

This got to be the most asked question about RTL :D
The best strategy is to use jest.mock (or the equivalent in your testing framework) to mock the select and render an HTML select instead.
For more info on why this is the best approach, I wrote something that applies to this case too. The OP asked about a select in Material-UI but the idea is the same.
Original question and my answer:
Because you have no control over that UI. It's defined in a 3rd party module.
So, you have two options:
You can figure out what HTML the material library creates and then use container.querySelector to find its elements and interact with it. It takes a while but it should be possible. After you have done all of that you have to hope that at every new release they don't change the DOM structure too much or you might have to update all your tests.
The other option is to trust that Material-UI is going to make a component that works and that your users can use. Based on that trust you can simply replace that component in your tests for a simpler one.
Yes, option one tests what the user sees but option two is easier to maintain.
In my experience the second option is just fine but of course, your use-case might be different and you might have to test the actual component.
This is an example of how you could mock a select:
jest.mock("react-select", () => ({ options, value, onChange }) => {
function handleChange(event) {
const option = options.find(
option => option.value === event.currentTarget.value
);
onChange(option);
}
return (
<select data-testid="select" value={value} onChange={handleChange}>
{options.map(({ label, value }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
);
});
You can read more here.

Finally, there is a library that helps us with that: https://testing-library.com/docs/ecosystem-react-select-event. Works perfectly for both single select or select-multiple:
From #testing-library/react docs:
import React from 'react'
import Select from 'react-select'
import { render } from '#testing-library/react'
import selectEvent from 'react-select-event'
const { getByTestId, getByLabelText } = render(
<form data-testid="form">
<label htmlFor="food">Food</label>
<Select options={OPTIONS} name="food" inputId="food" isMulti />
</form>
)
expect(getByTestId('form')).toHaveFormValues({ food: '' }) // empty select
// select two values...
await selectEvent.select(getByLabelText('Food'), ['Strawberry', 'Mango'])
expect(getByTestId('form')).toHaveFormValues({ food: ['strawberry', 'mango'] })
// ...and add a third one
await selectEvent.select(getByLabelText('Food'), 'Chocolate')
expect(getByTestId('form')).toHaveFormValues({
food: ['strawberry', 'mango', 'chocolate'],
})
Thanks https://github.com/romgain/react-select-event for such an awesome package!

Similar to #momimomo's answer, I wrote a small helper to pick an option from react-select in TypeScript.
Helper file:
import { getByText, findByText, fireEvent } from '#testing-library/react';
const keyDownEvent = {
key: 'ArrowDown',
};
export async function selectOption(container: HTMLElement, optionText: string) {
const placeholder = getByText(container, 'Select...');
fireEvent.keyDown(placeholder, keyDownEvent);
await findByText(container, optionText);
fireEvent.click(getByText(container, optionText));
}
Usage:
export const MyComponent: React.FunctionComponent = () => {
return (
<div data-testid="day-selector">
<Select {...reactSelectOptions} />
</div>
);
};
it('can select an option', async () => {
const { getByTestId } = render(<MyComponent />);
// Open the react-select options then click on "Monday".
await selectOption(getByTestId('day-selector'), 'Monday');
});

An easy way to test is by doing what the user should do
Click on the select field.
Click on one of the items in the dropdown list.
function CustomSelect() {
const colourOptions = [
{ value: 'orange', label: 'Orange', color: '#FF8B00' },
{ value: 'yellow', label: 'Yellow', color: '#FFC400' }
]
return <Select
aria-label="my custom select"
options={colourOptions}
//... props
/>
}
import { act, render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
// another imports
test('show selected item...', async () => {
const { getByText, getByLabelText } = render(<CustomSelect />);
expect(getByText('Orange')).not.toBeInTheDocument();
const myCustomSelect = getByLabelText(/my custom select/i);
await act(async () => userEvent.click(myCustomSelect));
const selectedItem = getByText('Orange');
await act(async () => userEvent.click(selectedItem));
expect(getByText('Orange')).toBeInTheDocument();
});

This solution worked for me.
fireEvent.change(getByTestId("select-test-id"), { target: { value: "1" } });
Hope it might help strugglers.

In case you are not using a label element, the way to go with react-select-event is:
const select = screen.container.querySelector(
"input[name='select']"
);
selectEvent.select(select, "Value");

export async function selectOption(container: HTMLElement, optionText: string) {
let listControl: any = '';
await waitForElement(
() => (listControl = container.querySelector('.Select-control')),
);
fireEvent.mouseDown(listControl);
await wait();
const option = getByText(container, optionText);
fireEvent.mouseDown(option);
await wait();
}
NOTE:
container: container for select box ( eg: container = getByTestId('seclectTestId') )

An alternative solution which worked for my use case and requires no react-select mocking or separate library (thanks to #Steve Vaughan) found on the react-testing-library spectrum chat.
The downside to this is we have to use container.querySelector which RTL advises against in favour of its more resillient selectors.

if for whatever reason there is a label with the same name use this
const [firstLabel, secondLabel] = getAllByLabelText('State');
await act(async () => {
fireEvent.focus(firstLabel);
fireEvent.keyDown(firstLabel, {
key: 'ArrowDown',
keyCode: 40,
code: 40,
});
await waitFor(() => {
fireEvent.click(getByText('Alabama'));
});
fireEvent.focus(secondLabel);
fireEvent.keyDown(secondLabel, {
key: 'ArrowDown',
keyCode: 40,
code: 40,
});
await waitFor(() => {
fireEvent.click(getByText('Alaska'));
});
});
or If you have a way to query your section—for example with a data-testid—you could use within:
within(getByTestId('id-for-section-A')).getByLabelText('Days')
within(getByTestId('id-for-section-B')).getByLabelText('Days')

Because I wanted to test a component that wrapped react-select, mocking it with a regular <select> element wouldn't have worked. So I ended up using the same approach that the package's own tests use, which is supplying a className in props and then using it with querySelector() to access the rendered element in the test:
const BASIC_PROPS: BasicProps = {
className: 'react-select',
classNamePrefix: 'react-select',
// ...
};
let { container } = render(
<Select {...props} menuIsOpen escapeClearsValue isClearable />
);
fireEvent.keyDown(container.querySelector('.react-select')!, {
keyCode: 27,
key: 'Escape',
});
expect(
container.querySelector('.react-select__single-value')!.textContent
).toEqual('0');

To anyone out there - I got mine to select by doing fireEvent.mouseDown on the option instead of click.

Related

React get value from key:value array

Beginner question. I know this is a simple question but I haven't been able to get this to work. I'm passing an object which holds an array of k:v pairs to a component. Eventually this props will contain multiple k:v pairs, but for now I'm just passing the one.
[{goal: 20000}]
In the component I'm trying to grab the value, 20000, so I can display it on screen. I can't seem to get just the number. If I look at props.goal I get the entire k:v.
[{goal: 20000}]
If I try props[0].goal I get 'TypeError: undefined is not an object (evaluating 'props[0].goal')'
What am I missing? Thanks for any help.
Update:
Here is the entire code for the component in question.
import { React, useState } from "react";
import Form from "react-bootstrap/Form";
import { Row, Col, Button } from "react-bootstrap";
import "./../css/Goal.css";
const Goal = (props) => {
// const [goal, setGoal] = useState("");
const [record, setRecord] = useState("");
const monthlyGoal = 2;
console.log("props[0]");
console.log(props[0]); //undefined
console.log("props");
console.log({ props }); //See below
props: Object
goal: Object
goals: [{goal: 20000}] (1)
const handleInput = (event) => {
console.log(event);
event.preventDefault();
setRecord(event.target.value);
console.log(record);
};
const defaultOptions = {
significantDigits: 2,
thousandsSeparator: ",",
decimalSeparator: ".",
symbol: "$",
};
const formattedMonthlyGoal = (value, options) => {
if (typeof value !== "number") value = 0.0;
options = { ...defaultOptions, ...options };
value = value.toFixed(options.significantDigits);
const [currency, decimal] = value.split(".");
return `${options.symbol} ${currency.replace(
/\B(?=(\d{3})+(?!\d))/g,
options.thousandsSeparator
)}${options.decimalSeparator}${decimal}`;
};
return (
<Form>
<Row className="align-items-center flex">
<Col sm={3} className="goal sm={3}">
<Form.Control
id="inlineFormInputGoal"
placeholder="Goal"
// onChange={(e) => setGoal(e.target.value)}
/>
<Button type="submit" className="submit btn-3" onSubmit={handleInput}>
Submit
</Button>
</Col>
<Col>
<h1 className="text-box">
Goal: {formattedMonthlyGoal(monthlyGoal)}
</h1>
</Col>
</Row>
</Form>
);
};
export default Goal;
Update 2:Here is the parent component:
import React, { useEffect, useState } from "react";
import Goal from "./Goal";
import axios from "axios";
const Dashboard = () => {
const [dashboardinfo, setdashboardinfo] = useState([]);
useEffect(() => {
async function fetchData() {
try {
const data = (await axios.get("/api/goals/getgoals")).data;
setdashboardinfo(data);
} catch (error) {
console.log(error);
}
}
fetchData();
}, []);
return (
<React.Fragment>
<Goal dashboardinfo={dashboardinfo} />
</React.Fragment>
);
};
export default Dashboard;
If you get an object like the following from console logging destructured props:
{
dashboardinfo: {goals: [{goal: 20000}]}
}
You need to use props.dashboardinfo.goals[0].goal to get the value.
Your props contains the object "dashboardinfo" so you need to do
props.dashboardinfo.goals[0].goal
or a better way is to destructure your props object like this
const Goal = ({dashboardinfo: { goals }}) => {
...
goals[0].goal
...
}
I believe I've resolved my issue. It wasn't so much a problem with accessing the key:value as I thought, because when the page was initialized I was able to grab the value and display it fine. However, when I refreshed the page I lost all of the props data and that resulted in an error. I tracked it down to the useState didn't seem to be updating the value before I was trying to read it. So I added a useEffect in the child component.
const Goal = (props) => {
const [goal, setgoal] = useState([]);
useEffect(() => {
setgoal(props.goal);
console.log("the goal", goal);
}, [props.goal, goal]);
...
This seems to have worked as I'm getting the information I want and not getting any errors when I refresh. This may not be the ideal way to go about this but it is working.

React-admin Datagrid: expand all rows by default

I have a react-admin (3.14.1) List using a Datagrid, where each row is expandable.
Does anyone know how to expand all the rows by default?
Or expand a row programmatically?
I've checked the Datagrid code in node_modules/ra-ui-materialui/lib/list/datagrid/Datagrid.js, no obvious props...
Datagrid.propTypes = {
basePath: prop_types_1.default.string,
body: prop_types_1.default.element,
children: prop_types_1.default.node.isRequired,
classes: prop_types_1.default.object,
className: prop_types_1.default.string,
currentSort: prop_types_1.default.shape({
field: prop_types_1.default.string,
order: prop_types_1.default.string,
}),
data: prop_types_1.default.any,
// #ts-ignore
expand: prop_types_1.default.oneOfType([prop_types_1.default.element, prop_types_1.default.elementType]),
hasBulkActions: prop_types_1.default.bool,
hover: prop_types_1.default.bool,
ids: prop_types_1.default.arrayOf(prop_types_1.default.any),
loading: prop_types_1.default.bool,
onSelect: prop_types_1.default.func,
onToggleItem: prop_types_1.default.func,
resource: prop_types_1.default.string,
rowClick: prop_types_1.default.oneOfType([prop_types_1.default.string, prop_types_1.default.func]),
rowStyle: prop_types_1.default.func,
selectedIds: prop_types_1.default.arrayOf(prop_types_1.default.any),
setSort: prop_types_1.default.func,
total: prop_types_1.default.number,
version: prop_types_1.default.number,
isRowSelectable: prop_types_1.default.func,
isRowExpandable: prop_types_1.default.func,
};
I have a solution that does something similar, not using jquery. It's a custom hook that makes the first id of a resource expand, which in my case is the first item in the List.
// useExpandFirst.ts
import * as React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Identifier, ReduxState, toggleListItemExpand } from 'react-admin';
type AboutExpansion = { noneExpanded: boolean; firstId: Identifier };
const useExpandFirst = (props) => {
const { resource } = props;
const once = React.useRef(false);
const dispatch = useDispatch();
const { noneExpanded, firstId } = useSelector<ReduxState, AboutExpansion>((state) => {
const list = state?.admin?.resources?.[resource]?.list;
return {
noneExpanded: list?.expanded?.length === 0,
firstId: list?.ids[0],
};
});
React.useEffect(() => {
if (noneExpanded && firstId && !once.current) {
once.current = true;
const action = toggleListItemExpand(resource, firstId);
dispatch(action);
}
}, [noneExpanded, firstId, dispatch, resource]);
};
Instead of using the hook in the component that actually renders the List, I'm using it in some other (not so) random component, for example the app's Layout. That causes way less rerenders of the component that renders the List.
// MyLayout.tsx
const MyLayout: React.FC<LayoutProps> = (props) => {
// expand the first company record as soon as it becomes available
useExpandFirst({ resource: 'companies' });
return (
<Layout
{...props}
appBar={MyAppBar}
sidebar={MySidebar}
menu={MyMenu}
// notification={MyNotification}
/>
);
};
It's not perfect, but it does the job. With just a few modifications, you can alter it to expand all id's. That would mean that you have to dispatch the action for each id (in the useEffect hook function).
I found this question and used this answer to solve the same problem.
export const useExpandDefaultForAll = (resource) => {
const [ids, expanded] = useSelector(
(state) => ([state.admin.resources[resource].list.ids, state.admin.resources[resource].list.expanded])
);
const dispatch = useDispatch();
useEffect(() => {
for (let i = 0; i < ids.length; i++) {
if (!expanded.includes(ids[i])){
dispatch(toggleListItemExpand(resource, ids[i]));
}
}
}, [ids]);
};
And i also call it in my List component:
const OrderList = (props) => {
useExpandDefaultForAll(props.resource);
...
I will be glad if it is useful to someone. If you know how to do it better then please fix it.
Hacked it with jquery, dear dear.
import $ from 'jquery'
import React, {Fragment} from 'react';
import {List, Datagrid, TextField, useRecordContext} from 'react-admin';
export class MyList extends React.Component {
gridref = React.createRef()
ensureRowsExpanded(ref) {
// Must wait a tick for the expand buttons to be completely built
setTimeout(() => {
if (!ref || !ref.current) return;
const buttonSelector = "tr > td:first-child > div[aria-expanded=false]"
const buttons = $(buttonSelector, ref.current)
buttons.click()
}, 1)
}
/**
* This runs every time something changes significantly in the list,
* e.g. search, filter, pagination changes.
* Surely there's a better way to do it, i don't know!
*/
Aside = () => {
this.ensureRowsExpanded(this.gridref)
return null;
}
render = () => {
return <List {...this.props} aside={<this.Aside/>} >
<Datagrid
expand={<MyExpandedRow />}
isRowExpandable={row => true}
ref={this.gridref}
>
<TitleField source="title" />
</Datagrid>
</List>
}
}
const MyExpandedRow = () => {
const record = useRecordContext();
if (!record) return "";
return <div>Hello from {record.id}</div>;
}
Relies on particular table structure and aria-expanded attribute, so not great. Works though.

Run Method after User Stops Typing in TextArea React [duplicate]

How do you perform debounce in React.js?
I want to debounce the handleOnChange.
I tried with debounce(this.handleOnChange, 200) but it doesn't work.
function debounce(fn, delay) {
var timer = null;
return function() {
var context = this,
args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
};
}
var SearchBox = React.createClass({
render: function() {
return <input type="search" name="p" onChange={this.handleOnChange} />;
},
handleOnChange: function(event) {
// make ajax call
}
});
2019: try hooks + promise debouncing
This is the most up to date version of how I would solve this problem. I would use:
awesome-debounce-promise to debounce the async function
use-constant to store that debounced function into the component
react-async-hook to get the result into my component
This is some initial wiring but you are composing primitive blocks on your own, and you can make your own custom hook so that you only need to do this once.
// Generic reusable hook
const useDebouncedSearch = (searchFunction) => {
// Handle the input text state
const [inputText, setInputText] = useState('');
// Debounce the original search async function
const debouncedSearchFunction = useConstant(() =>
AwesomeDebouncePromise(searchFunction, 300)
);
// The async callback is run each time the text changes,
// but as the search function is debounced, it does not
// fire a new request on each keystroke
const searchResults = useAsync(
async () => {
if (inputText.length === 0) {
return [];
} else {
return debouncedSearchFunction(inputText);
}
},
[debouncedSearchFunction, inputText]
);
// Return everything needed for the hook consumer
return {
inputText,
setInputText,
searchResults,
};
};
And then you can use your hook:
const useSearchStarwarsHero = () => useDebouncedSearch(text => searchStarwarsHeroAsync(text))
const SearchStarwarsHeroExample = () => {
const { inputText, setInputText, searchResults } = useSearchStarwarsHero();
return (
<div>
<input value={inputText} onChange={e => setInputText(e.target.value)} />
<div>
{searchResults.loading && <div>...</div>}
{searchResults.error && <div>Error: {search.error.message}</div>}
{searchResults.result && (
<div>
<div>Results: {search.result.length}</div>
<ul>
{searchResults.result.map(hero => (
<li key={hero.name}>{hero.name}</li>
))}
</ul>
</div>
)}
</div>
</div>
);
};
You will find this example running here and you should read react-async-hook documentation for more details.
2018: try promise debouncing
We often want to debounce API calls to avoid flooding the backend with useless requests.
In 2018, working with callbacks (Lodash/Underscore) feels bad and error-prone to me. It's easy to encounter boilerplate and concurrency issues due to API calls resolving in an arbitrary order.
I've created a little library with React in mind to solve your pains: awesome-debounce-promise.
This should not be more complicated than that:
const searchAPI = text => fetch('/search?text=' + encodeURIComponent(text));
const searchAPIDebounced = AwesomeDebouncePromise(searchAPI, 500);
class SearchInputAndResults extends React.Component {
state = {
text: '',
results: null,
};
handleTextChange = async text => {
this.setState({ text, results: null });
const result = await searchAPIDebounced(text);
this.setState({ result });
};
}
The debounced function ensures that:
API calls will be debounced
the debounced function always returns a promise
only the last call's returned promise will resolve
a single this.setState({ result }); will happen per API call
Eventually, you may add another trick if your component unmounts:
componentWillUnmount() {
this.setState = () => {};
}
Note that Observables (RxJS) can also be a great fit for debouncing inputs, but it's a more powerful abstraction which may be harder to learn/use correctly.
< 2017: still want to use callback debouncing?
The important part here is to create a single debounced (or throttled) function per component instance. You don't want to recreate the debounce (or throttle) function everytime, and you don't want either multiple instances to share the same debounced function.
I'm not defining a debouncing function in this answer as it's not really relevant, but this answer will work perfectly fine with _.debounce of underscore or lodash, as well as any user-provided debouncing function.
GOOD IDEA:
Because debounced functions are stateful, we have to create one debounced function per component instance.
ES6 (class property): recommended
class SearchBox extends React.Component {
method = debounce(() => {
...
});
}
ES6 (class constructor)
class SearchBox extends React.Component {
constructor(props) {
super(props);
this.method = debounce(this.method.bind(this),1000);
}
method() { ... }
}
ES5
var SearchBox = React.createClass({
method: function() {...},
componentWillMount: function() {
this.method = debounce(this.method.bind(this),100);
},
});
See JsFiddle: 3 instances are producing 1 log entry per instance (that makes 3 globally).
NOT a good idea:
var SearchBox = React.createClass({
method: function() {...},
debouncedMethod: debounce(this.method, 100);
});
It won't work, because during class description object creation, this is not the object created itself. this.method does not return what you expect because the this context is not the object itself (which actually does not really exist yet BTW as it is just being created).
NOT a good idea:
var SearchBox = React.createClass({
method: function() {...},
debouncedMethod: function() {
var debounced = debounce(this.method,100);
debounced();
},
});
This time you are effectively creating a debounced function that calls your this.method. The problem is that you are recreating it on every debouncedMethod call, so the newly created debounce function does not know anything about former calls! You must reuse the same debounced function over time or the debouncing will not happen.
NOT a good idea:
var SearchBox = React.createClass({
debouncedMethod: debounce(function () {...},100),
});
This is a little bit tricky here.
All the mounted instances of the class will share the same debounced function, and most often this is not what you want!. See JsFiddle: 3 instances are producting only 1 log entry globally.
You have to create a debounced function for each component instance, and not a single debounced function at the class level, shared by each component instance.
Take care of React's event pooling
This is related because we often want to debounce or throttle DOM events.
In React, the event objects (i.e., SyntheticEvent) that you receive in callbacks are pooled (this is now documented). This means that after the event callback has be called, the SyntheticEvent you receive will be put back in the pool with empty attributes to reduce the GC pressure.
So if you access SyntheticEvent properties asynchronously to the original callback (as may be the case if you throttle/debounce), the properties you access may be erased. If you want the event to never be put back in the pool, you can use the persist() method.
Without persist (default behavior: pooled event)
onClick = e => {
alert(`sync -> hasNativeEvent=${!!e.nativeEvent}`);
setTimeout(() => {
alert(`async -> hasNativeEvent=${!!e.nativeEvent}`);
}, 0);
};
The 2nd (async) will print hasNativeEvent=false because the event properties have been cleaned up.
With persist
onClick = e => {
e.persist();
alert(`sync -> hasNativeEvent=${!!e.nativeEvent}`);
setTimeout(() => {
alert(`async -> hasNativeEvent=${!!e.nativeEvent}`);
}, 0);
};
The 2nd (async) will print hasNativeEvent=true because persist allows you to avoid putting the event back in the pool.
You can test these 2 behaviors here: JsFiddle
Read Julen's answer for an example of using persist() with a throttle/debounce function.
Uncontrolled Components
You can use the event.persist() method.
An example follows using underscore's _.debounce():
var SearchBox = React.createClass({
componentWillMount: function () {
this.delayedCallback = _.debounce(function (event) {
// `event.target` is accessible now
}, 1000);
},
onChange: function (event) {
event.persist();
this.delayedCallback(event);
},
render: function () {
return (
<input type="search" onChange={this.onChange} />
);
}
});
Edit: See this JSFiddle
Controlled Components
Update: the example above shows an uncontrolled component. I use controlled elements all the time so here's another example of the above, but without using the event.persist() "trickery".
A JSFiddle is available as well. Example without underscore
var SearchBox = React.createClass({
getInitialState: function () {
return {
query: this.props.query
};
},
componentWillMount: function () {
this.handleSearchDebounced = _.debounce(function () {
this.props.handleSearch.apply(this, [this.state.query]);
}, 500);
},
onChange: function (event) {
this.setState({query: event.target.value});
this.handleSearchDebounced();
},
render: function () {
return (
<input type="search"
value={this.state.query}
onChange={this.onChange} />
);
}
});
var Search = React.createClass({
getInitialState: function () {
return {
result: this.props.query
};
},
handleSearch: function (query) {
this.setState({result: query});
},
render: function () {
return (
<div id="search">
<SearchBox query={this.state.result}
handleSearch={this.handleSearch} />
<p>You searched for: <strong>{this.state.result}</strong></p>
</div>
);
}
});
React.render(<Search query="Initial query" />, document.body);
Edit: updated examples and JSFiddles to React 0.12
Edit: updated examples to address the issue raised by Sebastien Lorber
Edit: updated with jsfiddle that does not use underscore and uses plain javascript debounce.
2019: Use the 'useCallback' react hook
After trying many different approaches, I found using useCallback to be the simplest and most efficient at solving the multiple calls problem of using debounce within an onChange event.
As per the Hooks API documentation,
useCallback returns a memorized version of the callback that only changes if one of the dependencies has changed.
Passing an empty array as a dependency makes sure the callback is called only once. Here's a simple implementation :
import React, { useCallback } from "react";
import { debounce } from "lodash";
const handler = useCallback(debounce(someFunction, 2000), []);
const onChange = (event) => {
// perform any event related action here
handler();
};
After struggling with the text inputs for a while and not finding a perfect solution on my own, I found this on npm: react-debounce-input.
Here is a simple example:
import React from 'react';
import ReactDOM from 'react-dom';
import {DebounceInput} from 'react-debounce-input';
class App extends React.Component {
state = {
value: ''
};
render() {
return (
<div>
<DebounceInput
minLength={2}
debounceTimeout={300}
onChange={event => this.setState({value: event.target.value})} />
<p>Value: {this.state.value}</p>
</div>
);
}
}
const appRoot = document.createElement('div');
document.body.appendChild(appRoot);
ReactDOM.render(<App />, appRoot);
The DebounceInput component accepts all of the props you can assign to a normal input element. Try it out on codepen
I hope it helps someone else too and saves them some time.
There can be a simple approach using react hooks.
Step 1 : define a state to maintain searched text
const [searchTerm, setSearchTerm] = useState('')
Step 2 : Use useEffect to capture any change in search Term
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
if (searchTerm) {
// write your logic here
}
}, 400)
return () => clearTimeout(delayDebounceFn)
}, [searchTerm])
Step 3 : Write a function to handle input change
function handleInputChange(value) {
if (value) {
setSearchTerm(value)
}
}
That's all ! Call this method as and when required
2022 - use a useEffect hook
Your best option at this time is to use the useEffect hook. useEffect lets you set a function that can modify state in response to some async event. Debouncing is asynchronous so useEffect works nicely for this purpose.
If you return a function from the hook, the returned function will be called before the hook is called again. This lets you cancel the previous timeout, effectively debouncing the function.
Example
Here we have two states, value and tempValue. Setting tempValue will trigger a useEffect hook that will start a 1000ms timeout which will call a function to copy tempValue into value.
The hook returns a function that unsets the timer. When the hook is called again (i.e. another key is pressed) the timeout is canceled and reset.
const DebounceDemo = () => {
const [value, setValue] = useState();
const [tempValue, setTempValue] = useState();
// This hook will set a 1000 ms timer to copy tempValue into value
// If the hook is called again, the timer will be cancelled
// This creates a debounce
useEffect(
() => {
// Wait 1000ms before copying the value of tempValue into value;
const timeout = setTimeout(() => {
setValue(tempValue);
}, 1000);
// If the hook is called again, cancel the previous timeout
// This creates a debounce instead of a delay
return () => clearTimeout(timeout);
},
// Run the hook every time the user makes a keystroke
[tempValue]
)
// Here we create an input to set tempValue.
// value will be updated 1000 ms after the hook is last called,
// i.e after the last user keystroke.
return (
<>
<input
onChange={
({ target }) => setTempValue(target.value)
}
/>
<p>{ value }</p>
</>
)
}
I found this post by Justin Tulk very helpful. After a couple of attempts, in what one would perceive to be the more official way with react/redux, it shows that it fails due to React's synthetic event pooling. His solution then uses some internal state to track the value changed/entered in the input, with a callback right after setState which calls a throttled/debounced redux action that shows some results in realtime.
import React, {Component} from 'react'
import TextField from 'material-ui/TextField'
import { debounce } from 'lodash'
class TableSearch extends Component {
constructor(props){
super(props)
this.state = {
value: props.value
}
this.changeSearch = debounce(this.props.changeSearch, 250)
}
handleChange = (e) => {
const val = e.target.value
this.setState({ value: val }, () => {
this.changeSearch(val)
})
}
render() {
return (
<TextField
className = {styles.field}
onChange = {this.handleChange}
value = {this.props.value}
/>
)
}
}
With debounce you need to keep the original synthetic event around with event.persist(). Here is working example tested with React 16+.
import React, { Component } from 'react';
import debounce from 'lodash/debounce'
class ItemType extends Component {
evntHandler = debounce((e) => {
console.log(e)
}, 500);
render() {
return (
<div className="form-field-wrap"
onClick={e => {
e.persist()
this.evntHandler(e)
}}>
...
</div>
);
}
}
export default ItemType;
With functional component, you can do this -
const Search = ({ getBooks, query }) => {
const handleOnSubmit = (e) => {
e.preventDefault();
}
const debouncedGetBooks = debounce(query => {
getBooks(query);
}, 700);
const onInputChange = e => {
debouncedGetBooks(e.target.value)
}
return (
<div className="search-books">
<Form className="search-books--form" onSubmit={handleOnSubmit}>
<Form.Group controlId="formBasicEmail">
<Form.Control type="text" onChange={onInputChange} placeholder="Harry Potter" />
<Form.Text className="text-muted">
Search the world's most comprehensive index of full-text books.
</Form.Text>
</Form.Group>
<Button variant="primary" type="submit">
Search
</Button>
</Form>
</div>
)
}
References -
- https://gist.github.com/elijahmanor/08fc6c8468c994c844213e4a4344a709
- https://blog.revathskumar.com/2016/02/reactjs-using-debounce-in-react-components.html
My solution is hooks based (written in Typescript).
I've got 2 main hooks useDebouncedValue and useDebouncedCallback
First - useDebouncedValue
Let's say we've got a search box, but we want to ask the server for search results after the user has stopped typing for 0,5s
function SearchInput() {
const [realTimeValue, setRealTimeValue] = useState('');
const debouncedValue = useDebouncedValue(realTimeValue, 500); // this value will pick real time value, but will change it's result only when it's seattled for 500ms
useEffect(() => {
// this effect will be called on seattled values
api.fetchSearchResults(debouncedValue);
}, [debouncedValue])
return <input onChange={event => setRealTimeValue(event.target.value)} />
}
Implementation
import { useState, useEffect } from "react";
export function useDebouncedValue<T>(input: T, time = 500) {
const [debouncedValue, setDebouncedValue] = useState(input);
// every time input value has changed - set interval before it's actually commited
useEffect(() => {
const timeout = setTimeout(() => {
setDebouncedValue(input);
}, time);
return () => {
clearTimeout(timeout);
};
}, [input, time]);
return debouncedValue;
}
Second useDebouncedCallback
It just creates a 'debounced' function in the scope of your component.
Let's say we've got a component with a button that will show alert 500ms after you stopped clicking it.
function AlertButton() {
function showAlert() {
alert('Clicking has seattled');
}
const debouncedShowAlert = useDebouncedCallback(showAlert, 500);
return <button onClick={debouncedShowAlert}>Click</button>
}
Implementation (note I'm using lodash/debounce as a helper)
import debounce from 'lodash/debounce';
import { useMemo } from 'react';
export function useDebouncedCallback<T extends (...args: any) => any>(callback: T, wait?: number) {
const debouncedCallback = useMemo(() => debounce(callback, wait), [callback, wait]);
return debouncedCallback;
}
If all you need from the event object is to get the DOM input element, the solution is much simpler – just use ref. Note that this requires Underscore:
class Item extends React.Component {
constructor(props) {
super(props);
this.saveTitle = _.throttle(this.saveTitle.bind(this), 1000);
}
saveTitle(){
let val = this.inputTitle.value;
// make the ajax call
}
render() {
return <input
ref={ el => this.inputTitle = el }
type="text"
defaultValue={this.props.title}
onChange={this.saveTitle} />
}
}
There's a use-debounce package that you can use with ReactJS hooks.
From package's README:
import { useDebounce } from 'use-debounce';
export default function Input() {
const [text, setText] = useState('Hello');
const [value] = useDebounce(text, 1000);
return (
<div>
<input
defaultValue={'Hello'}
onChange={(e) => {
setText(e.target.value);
}}
/>
<p>Actual value: {text}</p>
<p>Debounce value: {value}</p>
</div>
);
}
As you can see from the example above, it is set up to update the variable value only once every second (1000 milliseconds).
Lots of good info here already, but to be succinct. This works for me...
import React, {Component} from 'react';
import _ from 'lodash';
class MyComponent extends Component{
constructor(props){
super(props);
this.handleChange = _.debounce(this.handleChange.bind(this),700);
};
If you are using redux you can do this in a very elegant way with middleware. You can define a Debounce middleware as:
var timeout;
export default store => next => action => {
const { meta = {} } = action;
if(meta.debounce){
clearTimeout(timeout);
timeout = setTimeout(() => {
next(action)
}, meta.debounce)
}else{
next(action)
}
}
You can then add debouncing to action creators, such as:
export default debouncedAction = (payload) => ({
type : 'DEBOUNCED_ACTION',
payload : payload,
meta : {debounce : 300}
}
There's actually already middleware you can get off npm to do this for you.
Using ES6 CLASS and React 15.x.x & lodash.debounce
Im using React's refs here since event losses the this bind internally.
class UserInput extends React.Component {
constructor(props) {
super(props);
this.state = {
userInput: ""
};
this.updateInput = _.debounce(this.updateInput, 500);
}
updateInput(userInput) {
this.setState({
userInput
});
//OrderActions.updateValue(userInput);//do some server stuff
}
render() {
return ( <div>
<p> User typed: {
this.state.userInput
} </p>
<input ref = "userValue" onChange = {() => this.updateInput(this.refs.userValue.value) } type = "text" / >
</div>
);
}
}
ReactDOM.render( <
UserInput / > ,
document.getElementById('root')
);
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.5/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
You can use Lodash debounce https://lodash.com/docs/4.17.5#debounce method. It is simple and effective.
import * as lodash from lodash;
const update = (input) => {
// Update the input here.
console.log(`Input ${input}`);
}
const debounceHandleUpdate = lodash.debounce((input) => update(input), 200, {maxWait: 200});
doHandleChange() {
debounceHandleUpdate(input);
}
You can also cancel the debounce method by using the below method.
this.debounceHandleUpdate.cancel();
Hope it helps you. Cheers!!
FYI
Here is another PoC implementation:
without any libraries (e.g. lodash) for debouncing
using React Hooks API
I hope it helps :)
import React, { useState, useEffect, ChangeEvent } from 'react';
export default function DebouncedSearchBox({
inputType,
handleSearch,
placeholder,
debounceInterval,
}: {
inputType?: string;
handleSearch: (q: string) => void;
placeholder: string;
debounceInterval: number;
}) {
const [query, setQuery] = useState<string>('');
const [timer, setTimer] = useState<NodeJS.Timer | undefined>();
useEffect(() => {
if (timer) {
clearTimeout(timer);
}
setTimer(setTimeout(() => {
handleSearch(query);
}, debounceInterval));
}, [query]);
const handleOnChange = (e: ChangeEvent<HTMLInputElement>): void => {
setQuery(e.target.value);
};
return (
<input
type={inputType || 'text'}
className="form-control"
placeholder={placeholder}
value={query}
onChange={handleOnChange}
/>
);
}
There is now another solution for React and React Native in late/2019:
react-debounce-component
<input>
<Debounce ms={500}>
<List/>
</Debounce>
It's a component, easy to use, tiny and widley supported
Example:
import React from 'react';
import Debounce from 'react-debounce-component';
class App extends React.Component {
constructor (props) {
super(props);
this.state = {value: 'Hello'}
}
render () {
return (
<div>
<input value={this.state.value} onChange={(event) => {this.setState({value: event.target.value})}}/>
<Debounce ms={1000}>
<div>{this.state.value}</div>
</Debounce>
</div>
);
}
}
export default App;
*I'm the creator of this component
I can't find any answers under this question mentioning the approach I am using so just want to provide an alternative solution here which I think is the best for my use case.
If you are using the popular react hooks toolkit lib called react-use, then there is a utility hook called useDebounce() that implemented denounce logic in a quite elegant way.
const [query, setQuery] = useState('');
useDebounce(
() => {
emitYourOnDebouncedSearchEvent(query);
},
2000,
[query]
);
return <input onChange={({ currentTarget }) => setQuery(currentTarget.value)} />
For details please check the lib's github page directly.
https://github.com/streamich/react-use/blob/master/docs/useDebounce.md
As for June 2021, You can simply implement xnimorz solution: https://github.com/xnimorz/use-debounce
import { useState, useEffect, useRef } from "react";
// Usage
function App() {
// State and setters for ...
// Search term
const [searchTerm, setSearchTerm] = useState("");
// API search results
const [results, setResults] = useState([]);
// Searching status (whether there is pending API request)
const [isSearching, setIsSearching] = useState(false);
// Debounce search term so that it only gives us latest value ...
// ... if searchTerm has not been updated within last 500ms.
// The goal is to only have the API call fire when user stops typing ...
// ... so that we aren't hitting our API rapidly.
const debouncedSearchTerm = useDebounce(searchTerm, 500);
// Effect for API call
useEffect(
() => {
if (debouncedSearchTerm) {
setIsSearching(true);
searchCharacters(debouncedSearchTerm).then((results) => {
setIsSearching(false);
setResults(results);
});
} else {
setResults([]);
setIsSearching(false);
}
},
[debouncedSearchTerm] // Only call effect if debounced search term changes
);
return (
<div>
<input
placeholder="Search Marvel Comics"
onChange={(e) => setSearchTerm(e.target.value)}
/>
{isSearching && <div>Searching ...</div>}
{results.map((result) => (
<div key={result.id}>
<h4>{result.title}</h4>
<img
src={`${result.thumbnail.path}/portrait_incredible.${result.thumbnail.extension}`}
/>
</div>
))}
</div>
);
}
// API search function
function searchCharacters(search) {
const apiKey = "f9dfb1e8d466d36c27850bedd2047687";
return fetch(
`https://gateway.marvel.com/v1/public/comics?apikey=${apiKey}&titleStartsWith=${search}`,
{
method: "GET",
}
)
.then((r) => r.json())
.then((r) => r.data.results)
.catch((error) => {
console.error(error);
return [];
});
}
// Hook
function useDebounce(value, delay) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}
A nice and clean solution, that doesn't require any external dependencies:
Debouncing with React Hooks
It uses a custom plus the useEffect React hooks and the setTimeout / clearTimeout method.
Just another variant with recent react and lodash.
class Filter extends Component {
static propTypes = {
text: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired
}
state = {
initialText: '',
text: ''
}
constructor (props) {
super(props)
this.setText = this.setText.bind(this)
this.onChange = _.fp.debounce(500)(this.onChange.bind(this))
}
static getDerivedStateFromProps (nextProps, prevState) {
const { text } = nextProps
if (text !== prevState.initialText) {
return { initialText: text, text }
}
return null
}
setText (text) {
this.setState({ text })
this.onChange(text)
}
onChange (text) {
this.props.onChange(text)
}
render () {
return (<input value={this.state.text} onChange={(event) => this.setText(event.target.value)} />)
}
}
Did you try?
function debounce(fn, delay) {
var timer = null;
return function() {
var context = this,
args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
};
}
var SearchBox = React.createClass({
render: function() {
return <input type="search" name="p" onChange={this.handleOnChange} />;
},
handleOnChange: function(event) {
debounce(\\ Your handleChange code , 200);
}
});
simple and effective
https://www.npmjs.com/package/use-debounce
use-debounce
import { useDebouncedCallback } from 'use-debounce';
function Input({ defaultValue }) {
const [value, setValue] = useState(defaultValue);
const debounced = useDebouncedCallback(
(value) => {
setValue(value);
},
// delay
1000
);
return (
<div>
<input defaultValue={defaultValue} onChange={(e) => debounced(e.target.value)} />
<p>Debounced value: {value}</p>
</div>
);
}
Instead of wrapping the handleOnChange in a debounce(), why not wrap the ajax call inside the callback function inside the debounce, thereby not destroying the event object. So something like this:
handleOnChange: function (event) {
debounce(
$.ajax({})
, 250);
}
Here is an example I came up with that wraps another class with a debouncer. This lends itself nicely to being made into a decorator/higher order function:
export class DebouncedThingy extends React.Component {
static ToDebounce = ['someProp', 'someProp2'];
constructor(props) {
super(props);
this.state = {};
}
// On prop maybe changed
componentWillReceiveProps = (nextProps) => {
this.debouncedSetState();
};
// Before initial render
componentWillMount = () => {
// Set state then debounce it from here on out (consider using _.throttle)
this.debouncedSetState();
this.debouncedSetState = _.debounce(this.debouncedSetState, 300);
};
debouncedSetState = () => {
this.setState(_.pick(this.props, DebouncedThingy.ToDebounce));
};
render() {
const restOfProps = _.omit(this.props, DebouncedThingy.ToDebounce);
return <Thingy {...restOfProps} {...this.state} />
}
}
Here's a snippet using #Abra's approach wrapped in a function component
(we use fabric for the UI, just replace it with a simple button)
import React, { useCallback } from "react";
import { debounce } from "lodash";
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
const debounceTimeInMS = 2000;
export const PrimaryButtonDebounced = (props) => {
const debouncedOnClick = debounce(props.onClick, debounceTimeInMS, { leading: true });
const clickHandlerDebounced = useCallback((e, value) => {
debouncedOnClick(e, value);
},[]);
const onClick = (e, value) => {
clickHandlerDebounced(e, value);
};
return (
<PrimaryButton {...props}
onClick={onClick}
/>
);
}
Met this problem today. Solved it using setTimeout and clearTimeout.
I will give an example that you could adapt:
import React, { Component } from 'react'
const DEBOUNCE_TIME = 500
class PlacesAutocomplete extends Component {
debounceTimer = null;
onChangeHandler = (event) => {
// Clear the last registered timer for the function
clearTimeout(this.debounceTimer);
// Set a new timer
this.debounceTimer = setTimeout(
// Bind the callback function to pass the current input value as arg
this.getSuggestions.bind(null, event.target.value),
DEBOUNCE_TIME
)
}
// The function that is being debounced
getSuggestions = (searchTerm) => {
console.log(searchTerm)
}
render() {
return (
<input type="text" onChange={this.onChangeHandler} />
)
}
}
export default PlacesAutocomplete
You could also refactor it in it's own function component:
import React from 'react'
function DebouncedInput({ debounceTime, callback}) {
let debounceTimer = null
return (
<input type="text" onChange={(event) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(
callback.bind(null, event.target.value),
debounceTime
)
}} />
)
}
export default DebouncedInput
And use it like:
import React, { Component } from 'react'
import DebouncedInput from '../DebouncedInput';
class PlacesAutocomplete extends Component {
debounceTimer = null;
getSuggestions = (searchTerm) => {
console.log(searchTerm)
}
render() {
return (
<DebouncedInput debounceTime={500} callback={this.getSuggestions} />
)
}
}
export default PlacesAutocomplete
This solution does not need any extra lib and it also fires things up when the user presses enter:
const debounce = (fn, delay) => {
let timer = null;
return function() {
const context = this,
args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, delay);
};
}
const [search, setSearch] = useState('');
const [searchFor, setSearchFor] = useState(search);
useEffect(() => {
console.log("Search:", searchFor);
}, [searchFor]);
const fireChange = event => {
const { keyCode } = event;
if (keyCode === 13) {
event.preventDefault();
setSearchFor(search);
}
}
const changeSearch = event => {
const { value } = event.target;
setSearch(value);
debounceSetSearchFor(value);
};
const debounceSetSearchFor = useCallback(debounce(function(value) {
setSearchFor(value);
}, 250), []);
and the input could be like:
<input value={search} onKeyDown={fireChange} onChange={changeSearch} />
Hook:
import {useState} from "react";
const useDebounce = ({defaultTimeout = 250, defaultIdentifier = 'default'} = {}) => {
const [identifiers, setIdentifiers] = useState({[defaultIdentifier]: null});
return ({fn = null, identifier = defaultIdentifier, timeout = defaultTimeout} = {}) => {
if (identifiers.hasOwnProperty(identifier)) clearTimeout(identifiers[identifier]);
setIdentifiers({...identifiers, [identifier]: setTimeout(fn, timeout)});
};
};
export default useDebounce;
And use it anywhere (in same file use identifier to prevent concurrence) like:
const debounce = useDebounce();
const handlerA = () => {
debounce({fn: () => console.log('after 2000ms of last call with identifier A'), identifier: 'A', timeout: 2000});
};
const handlerB = () => {
debounce({fn: () => console.log('after 1500ms of last call with identifier B'), identifier: 'B', timeout: 1500});
};
I was searching for a solution to the same problem and came across this thread as well as some others but they had the same problem: if you are trying to do a handleOnChange function and you need the value from an event target, you will get cannot read property value of null or some such error. In my case, I also needed to preserve the context of this inside the debounced function since I'm executing a fluxible action. Here's my solution, it works well for my use case so I'm leaving it here in case anyone comes across this thread:
// at top of file:
var myAction = require('../actions/someAction');
// inside React.createClass({...});
handleOnChange: function (event) {
var value = event.target.value;
var doAction = _.curry(this.context.executeAction, 2);
// only one parameter gets passed into the curried function,
// so the function passed as the first parameter to _.curry()
// will not be executed until the second parameter is passed
// which happens in the next function that is wrapped in _.debounce()
debouncedOnChange(doAction(myAction), value);
},
debouncedOnChange: _.debounce(function(action, value) {
action(value);
}, 300)

Testing React component built with a function

Experimenting testing for React component built with a function. Before that I was used to do :
const animalsTable = shallow(<Animals/*props*//>);
animalsTable.instance().functionToTest();
ShallowWrapper.instance() returns null for a function so now following Alex Answer I am testing the DOM directly.
Below a simplified version of my React component and some tests :
React component :
import React, {useState, useEffect} from 'react';
import ReactTable from 'react-table';
const AnimalsTable = ({animals}) => {
const [animals, setAnimals] = useState(animals);
const [messageHelper, setMessageHelper] = useState('');
//some functions
useEffect(() => {
//some functions calls
}, []);
return (
<div>
<ReactTable id="animals-table"
//some props
getTrProps= {(state, rowInfo) => {
return {
onClick: (_event) => {
handleRowSelection(rowInfo);
}
};
}}
/>
<p id="message-helper">{messageHelper}</p>
</div>
);
};
export default AnimalsTable;
Test :
//imports
describe('AnimalsTable.computeMessageHelper', () => {
it('It should display the correct message', () => {
const expectedResult = 'Select the correct animal';
const animalsTable = mount(<AnimalsTable //props/>);
const message = animalsTable.find('#message-helper').props().children;
expect(message).to.equal(expectedResult);
});
});
This one is working well.
My issue is how to test a row click on the ReactTable component to test the handleRowSelection method?
My current test is :
describe('AnimalsTable.handleRowSelection', () => {
it('When a selection occurs should change the animal state', () => {
const animalsTable = mount(<AnimalsTable //props/>);
const getTrProps = channelsSelectionTable.find('#animals-table').props().getTrProps;
//what to do from here to trigger onClick() ?
});
});
EDIT :
I think the correct way would be like that but handleRowSelection is not triggered :
const animalsTable= mount(<AnimalsTable //props />);
const rows = animalsTable.find('div.rt-tr-group');
rows.at(0).simulate('click');
I will try to add a simple codeSandBox
Here the solution I found out, I had to inspect the classes names with Chrome then use it to simulate a click on the first row of the table :
it('When an animal selection occurs, it change the checkbox state', () => {
const animalsTable = mount(<AnimalsTable //props/>);
const rows = animalsTable.find('div.rt-tr.-odd');
rows.at(0).simulate('click');
const checkBoxResult = animalsTable.find('input[type="checkbox"]')
.at(0).props().checked;
expect(checkBoxResult).to.equal(true);
});
Might not be the correct way to test it but this one is working.

How to trigger rerender on test component?

I am testing this connected component:
export class ExclusiveSelectboxesFormSectionView extends React.Component<Props> {
handleSelection = (field: Field, message: string) => {
this.props.setCard({ message, field });
};
cancelSelection = () => this.props.setCard({ message: null, field: null });
render() {
return (
<View>
{this.props.cancelTitle && (
<CheckBox
title={this.props.cancelTitle}
checked={this.props.card.field == null}
onPress={this.cancelSelection.bind(this)}
checkedIcon="dot-circle-o"
uncheckedIcon="circle-o"
/>
)}
{this.props.fields.map((field, i) => {
const props = {};
props.isSelected = this.props.card.field == field;
props.selectionHandler = this.handleSelection.bind(this);
return <ExclusiveFieldView field={field} key={i} {...props} />;
})}
</View>
);
}
}
const mapState = ({ currentFormReducer }) => {
const card = currentFormReducer.card || { message: null, field: null };
return { card };
};
const mapDispatch = { setCard };
export default connect(mapState, mapDispatch)(ExclusiveSelectboxesFormSectionView);
I'm trying to test the non-connected component using react-native-testing-library. The component works in the app, but this test is failing to find "Second field option 2" in the next-to-last assertion in the test.
// non-connected component
import { ExclusiveSelectboxesFormSectionView } from "../../src/components/ExclusiveSelectboxesFormSectionView";
function createWrapper(customProps) {
let mockCard = { message: null, field: null };
const props = {
fields,
setCard: jest.fn().mockImplementation((card: Types.Card) => {
mockCard = card;
}),
card: mockCard,
...customProps
};
wrapper = render(
<Fragment>
<ExclusiveSelectboxesFormSectionView fields={fields} {...props} />
</Fragment>
);
return wrapper;
}
describe("ExclusiveSelectboxesFormSectionView", () => {
let checkboxes;
beforeEach(() => {
wrapper = createWrapper();
checkboxes = wrapper.getAllByType(CheckBox);
expect(checkboxes.length).toBe(3);
});
fit("shows the value of the currently selected field", async () => {
await fireEvent.press(checkboxes[1]); // show options
await fireEvent.press(wrapper.getByText("Second field option 2")); // select option
const component = wrapper.getByType(ExclusiveSelectboxesFormSectionView);
expect(component.props.setCard).toHaveBeenCalled();
// options should be gone
expect(wrapper.queryByText("Second field option 1")).toBeNull();
// selected option should still be on screen
expect(wrapper.getByText("Second field option 2")).toBeDefined();
expect(checkboxes[1].props.checked).toBe(true);
});
});
I've passed in a card prop and a setCard mock function prop, in place of redux providing these.
The mock setCard function is being called, so I think the problem is that the component is not rerendering with its new props (and a newly set card prop). A log statement in the component's render function confirms this (it only prints once when the test is run).
I imagine there's something basic I'm missing about how I'm rendering the component, or wrapping it, or calling it, or something.
Can anyone spot my problem?
It looks like react-native-testing-library's update function does the trick. But it was a bit tough to figure out.
// refactored from createWrapper
function getWrapperProps() {
return {
fields,
setCard: jest.fn().mockImplementation((card: Types.Card) => {
mockCard = card;
}),
card: mockCard
};
}
function createWrapper(customProps) {
wrapper = render(
<Fragment>
<ExclusiveSelectboxesFormSectionView
{...getWrapperProps()}
{...customProps}
/>
</Fragment>
);
return wrapper;
}
function updateWrapper(customProps) {
wrapper.update(
<Fragment>
<ExclusiveSelectboxesFormSectionView
{...getWrapperProps()}
{...customProps}
/>
</Fragment>
);
checkboxes = wrapper.getAllByType(CheckBox);
}
// call updateWrapper() when you need to get the newly rendered props
it("shows the value of the currently selected field", async () => {
await fireEvent.press(checkboxes[1]);
await fireEvent.press(wrapper.getByText("Second field option 2"));
const component = wrapper.getByType(ExclusiveSelectboxesFormSectionView);
expect(component.props.setCard).toHaveBeenCalled();
updateWrapper();
// options should be gone
expect(wrapper.queryByText("Second field option 1")).toBeNull();
// selected option should still be on screen
expect(wrapper.getByText("Second field option 2")).toBeDefined();
expect(checkboxes[1].props.checked).toBe(true);
});
Now it passes.
I'd love to know of other options. I'm not clear why this had been working with an earlier implementation, although the earlier implementation used state in the component, which I'm sure is basically the answer.

Resources