onClickHandler sometimes work, sometimes not - React - reactjs

The onClickHandler in the following code, in this component, 'SearchResult', sometimes work and sometimes not.
I can't figure out any logic that can explain why it works when it works, and why it's not working, when it's not working.
I've put a debugger inside the onClickHandler, at the beginning of it, and when it's not working, it doesn't get to the debugger at all - what indicates that the function sometimes isn't even called, and I can't figure out why.
Furthermore, I've tried to move all the code in function to the onClick, inline, but then, it's not working at all.
In addition, I've tried to use a function declaration instead of an arrow function, and it still behaves the same - sometimes it works, and sometimes it's not...
This is the site, you can see the behavior for yourself, in the search box.
This is the GitHub repository
Here you can see a video demonstrating how it's not working, except for one time it did work
Please help.
The problematic component:
import { useDispatch } from 'react-redux'
import { Col } from 'react-bootstrap'
import { getWeatherRequest } from '../redux/weather/weatherActions'
import { GENERAL_RESET } from '../redux/general/generalConstants'
const SearchResult = ({ Key, LocalizedName, setText }) => {
const dispatch = useDispatch()
const onClickHandler = () => {
dispatch({ type: GENERAL_RESET })
dispatch(
getWeatherRequest({
location: Key,
cityName: LocalizedName,
})
)
setText('')
}
return (
<Col className='suggestion' onClick={onClickHandler}>
{LocalizedName}
</Col>
)
}
export default SearchResult
This is the parent component:
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Form } from 'react-bootstrap'
import { getAutoCompleteResultsRequest } from '../redux/autoComplete/autoCompleteActions'
import { AUTO_COMPLETE_RESET } from '../redux/autoComplete/autoCompleteConstants'
import SearchResult from './SearchResult'
const SearchBox = () => {
const [text, setText] = useState('')
const dispatch = useDispatch()
const autoComplete = useSelector((state) => state.autoComplete)
const { results } = autoComplete
const onChangeHandler = (e) => {
if (e.target.value === '') {
dispatch({ type: AUTO_COMPLETE_RESET })
setText('')
}
setText(e.target.value)
dispatch(getAutoCompleteResultsRequest(e.target.value))
}
const onBlurHandler = () => {
setTimeout(() => {
dispatch({ type: AUTO_COMPLETE_RESET })
setText('')
}, 100)
}
return (
<div className='search-box'>
<Form inline>
<div className='input-group search-md search-sm'>
<input
type='search'
name='q'
value={text}
onChange={onChangeHandler}
onBlur={onBlurHandler}
placeholder='Search Location...'
className='mr-sm-2 ml-sm-3 form-control'
/>
</div>
</Form>
<div className='search-results'>
{results &&
results.map((result) => {
return (
<SearchResult key={result.Key} {...result} setText={setText} />
)
})}
</div>
</div>
)
}
export default SearchBox

I played a bit with your code and it looks like a possible solution may be the following addition in the SearchResult.js:
const onClickHandler = (e) => {
e.preventDefault();
...
After some tests
Please remove the onBlurHandler. It seams to fire ahaed of the onClickHandler of the result.

Can you put console.log(e.target.value) inside the onChangeHandler,
press again search results and make sure that one of it doesn't working and show us the console.
In searchResult component print to the console LocalizedName as well

Related

React Query On Success does not trigger

I'm currently having a problem that I can't explain. I work with React, typescript and react query.
The code on which I work is a double modal and when clicking on the refuse button of the second one launches a call via react query then we execute an onSuccess.
If I move the hook call into the first modal the onSuccess fires.
If I move the OnSuccess from the second modal into the hook it works too.
But I don't understand why in this case it doesn't work...
Does anyone have an idea/explanation please?
Thanks in advance.
Here is the code below of the first modal
import React from 'react'
import Button from '../Button'
import SecondModal from '../SecondModal'
interface FirstModalProps {
isOpen: boolean
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>
id?: string
}
const FirstModal = ({ id, isOpen, setIsOpen }:
FirstModalProps) => {
const [openRefuse, setOpenRefuse] = React.useState<boolean>(false)
return (
<>
<SecondModal isOpen={openRefuse} setIsOpen={setOpenRefuse} id={id} />
<Button
onClick={() => {
setIsOpen(false)
setOpenRefuse(true)
}}
text="refuse"
/>
</>
)}
export default FirstModal
Then the code of the second modal
import React from 'react'
import ConfirmModal from '../../../../../shared/styles/modal/confirm'
import useUpdate from '../../hooks/use-update'
interface SecondModalProps {
isOpen: boolean
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>
id?: string
}
const SecondModal = ({ isOpen, setIsOpen, id }: SecondModalProps) => {
const { mutate: update } = useUpdate()
const updateT = () => {
update(
{
id
},
{
onSuccess: () => {
console.log('OnSuccess trigger')
}
}
)
}
return (
<ConfirmModal
close={() => {
setIsOpen(false)
}}
isOpen={isOpen}
validate={updateT}
/>
)}
export default SecondModal
Then the hook
import { useMutation } from 'react-query'
interface hookProps {
id?: string
}
const useUpdate = () => {
const query = async ({ id }: hookProps) => {
if (!id) return
return (await myApi.updateTr(id))()
}
return useMutation(query, {
onError: () => {
console.log('error')
}
})}
export default useUpdate
callbacks passed to the .mutate function only execute if the component is still mounted when the request completes. On the contrary, callbacks on useMutation are always executed. This is documented here, and I've also written about it in my blog here.

How to stop rendering every keystroke input field onChange in React

Is there a better way to stop rendering every keystroke input field onChange in React... I noted that if I changed the value to onBlur() on input field, however it doesn't dispatch AddReservation function the second part to clear the input field (setReservationCardInput('')).
Or I cannot stop rendering onChange due to setReservationCardInput update reservationCardInput with useState() function?
My application is below, appreciate your feedback, thank you!
import React, {useState} from 'react'
import {useSelector, useDispatch} from 'react-redux'
import ReservationCard from '../../components/ReservationCard'
import {addReservation} from '../reservation/reservationsSlice'
const ReservationsList = () => {
const reservations = useSelector(state => state.reservations.value)
const [reservationCardInput, setReservationCardInput] = useState('')
const dispatch = useDispatch()
const inputOnChange = (e) => {
setReservationCardInput(e.target.value)
}
console.log('reservations:', reservationCardInput)
const AddReservation =() => {
if(!reservationCardInput) return
dispatch(addReservation(reservationCardInput))
setReservationCardInput('')
}
return (
<div className="reservation-cards-container">
{
reservations.map((name, index) => {
return (
<ReservationCard name={name} key={index}/>
)
})
}
<div className="reservation-input-container">
<input value={reservationCardInput} onChange={inputOnChange}/>
<button onClick={AddReservation}>Add Customer</button>
</div>
</div>
)
}
export default ReservationsList

Test failing in React Testing Library / Jest despite correct DOM behavior

I'm pretty new to Jest and testing, so I'm making an app using React, React Testing Library, and Jest to improve my skills.
One of my tests is failing, and I can't figure out why. Here is the code from my test:
import { render, screen, waitFor } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
// using UrlShortener since the state has been lifted up for UrlList
import UrlShortener from '../../pages/UrlShortener/UrlShortener'
...
test('URL list displays valid URL from input bar', async () => {
const passingText = 'http://www.google.com';
const testText = 'test4';
render(<UrlShortener />);
const urlInput = screen.getByPlaceholderText('Enter URL here...');
const nameInput = screen.getByPlaceholderText('Name your URL...');
const submitBtn = screen.getByRole('button', { name: 'Shorten!' });
userEvent.type(urlInput, passingText);
userEvent.type(nameInput, testText);
userEvent.click(submitBtn);
const listButton = screen.getByText('Link History');
userEvent.click(listButton);
const list = await screen.findAllByText(/visits/i);
await waitFor(() => expect(list).toHaveLength(4));
});
The thing that's confusing me is that I can see that the list is 4 elements long in the log from the failing test, but for some reason it's not getting picked up in the expect() function. Here's what the log is giving me (it clearly shows 4 elements in the list):
expect(received).toHaveLength(expected)
Expected length: 4
Received length: 3
Received array: [<p>Visits: 2</p>, <p>Visits: 1</p>, <p>Visits: 5</p>]
...
<div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/123"
>
test1
</a>
</p>
<p>
Visits:
2
</p>
</div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/456"
>
test2
</a>
</p>
<p>
Visits:
1
</p>
</div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/789"
>
test3
</a>
</p>
<p>
Visits:
5
</p>
</div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/shorten/123"
>
test4
</a>
</p>
<p>
Visits:
9
</p>
</div>
</div>
How is it possible that the DOM is behaving as expected in the log, but is failing in the actual test?
Update:
I'm adding more information so it's obvious what I'm doing. Basically, I've lifted state up from a child component (UrlList) to the parent (UrlShortener) so that I could pass a state updater function down to a sibling (UrlBar). The UrlShortener makes an axios call to the backend, then passes down a list of URLs to the UrlList component. When you click the submit button in the UrlBar component, it re-runs the axios call and updates the list with the new URL added.
Parent component:
import { useEffect, useState } from 'react';
import { SectionPage, BackButton, PageTitle } from './style';
import axios from 'axios';
import UrlBar from '../../components/UrlBar/UrlBar';
import UrlList from '../../components/UrlList/UrlList';
import { Url } from '../../types/types';
const UrlShortener = () => {
const [urls, setUrls] = useState<Url[] | []>([]);
const getUrls = () => {
axios
.get('https://fullstack-demos.herokuapp.com/shorten/urls/all')
.then((res) => setUrls(res.data));
};
useEffect(() => {
getUrls();
}, []);
return (
<SectionPage>
<BackButton href='/'>Go Back</BackButton>
<PageTitle>URL Shortener</PageTitle>
<UrlBar getUrls={getUrls} />
<UrlList urls={urls} />
</SectionPage>
);
};
export default UrlShortener;
Children:
import React, { useState } from 'react';
import {
ComponentWrapper,
Subtitle,
Triangle,
LinksContainer,
LinkGroup,
} from './style';
import { Url } from '../../types/types';
interface IProps {
urls: Url[] | [];
}
const UrlList: React.FC<IProps> = ({ urls }) => {
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen((prevState) => !prevState);
};
return (
<ComponentWrapper>
<Subtitle onClick={handleClick}>
Link History <Triangle>{open ? '▼' : '▲'}</Triangle>
</Subtitle>
<LinksContainer>
<div>
{open &&
urls.map(({ urlId, shortUrl, urlName, visits }: Url) => (
<LinkGroup key={urlId}>
<p>
<a href={shortUrl}>{urlName}</a>
</p>
<p>Visits: {visits}</p>
</LinkGroup>
))}
</div>
</LinksContainer>
</ComponentWrapper>
);
};
export default UrlList;
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { UrlInput, NameInput, UrlButton } from './style';
import { validateUrl } from '../../utils/utils';
interface IProps {
getUrls: () => void;
}
const UrlBar: React.FC<IProps> = ({ getUrls }) => {
const [urlInput, setUrlInput] = useState('');
const [nameInput, setNameInput] = useState('');
const [error, setError] = useState<boolean | string>(false);
useEffect(() => {
// Cleanup fixes React testing error: "Can't perform a React state update on an unmounted component"
return () => {
setUrlInput('');
};
}, []);
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUrlInput(e.target.value);
};
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNameInput(e.target.value);
};
const handleSubmit = async (e: React.SyntheticEvent) => {
e.preventDefault();
if (!nameInput) {
setError('Please name your URL');
} else if (!validateUrl(urlInput)) {
setError('Invalid Input');
} else {
setError(false);
await axios.post('https://fullstack-demos.herokuapp.com/shorten', {
longUrl: urlInput,
urlName: nameInput,
});
setUrlInput('');
setNameInput('');
getUrls();
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<NameInput
type='text'
name='nameInput'
id='nameInput'
placeholder='Name your URL...'
maxLength={20}
onChange={handleNameChange}
value={nameInput}
/>
<UrlInput
type='text'
name='urlInput'
id='urlInput'
placeholder='Enter URL here...'
onChange={handleUrlChange}
value={urlInput}
/>
<UrlButton name='button' type='submit'>
Shorten!
</UrlButton>
{error && <label htmlFor='urlInput'>{error}</label>}
</form>
</div>
);
};
export default UrlBar;
So after fighting to get my tests to pass for another component, I finally figured out how to get this one to pass. Apparently I just needed to add a few more waitFor() and await statements to catch some of the async stuff happening in my component. I'd be lying if I said I understand why this fixes my problem, but now I know that if my tests are failing even though I can see the right results in the JEST DOM, it probably has to do with missing waitFor / awaits.
test('URL list displays valid URL from input bar', async () => {
const passingText = 'http://www.google.com';
const testText = 'test4';
render(<UrlShortener />);
const urlInput = screen.getByPlaceholderText('Enter URL here...');
const nameInput = screen.getByPlaceholderText('Name your URL...');
const submitBtn = screen.getByRole('button', { name: 'Shorten!' });
userEvent.type(urlInput, passingText);
userEvent.type(nameInput, testText);
await waitFor(() => userEvent.click(submitBtn));
const listButton = await screen.findByText('Link History');
await waitFor(() => userEvent.click(listButton));
const list = await screen.findAllByText(/visits/i);
await waitFor(() => expect(list).toHaveLength(4));
});
});

Jest/React-testing-library: .find is not a function in the tested component

I have been doing integration tests of my project's front-end pages with jest and react-testing-library, and have come accross an error I don't fully understand.
The page I am testing renders a formik form that the users inputs data for an commercial institution:
import React from 'react';
import {
FormGroup, Label, Row,
} from 'reactstrap';
import {
Field, getIn, FieldArray, useFormikContext,
} from 'formik';
import { useRecoilValue } from 'recoil';
import Select from 'react-select';
import {
institutionTypeAtom,
} from 'recoil/institution';
import { Colxx } from 'components/common/CustomBootstrap';
import IntlMessages from 'helpers/IntlMessages';
import { FormikStep } from 'components/Stepper/Step';
const InstitutionStep1 = ({ label, isView, validationSchema }) => {
const {
values, errors, touched, setFieldValue,
} = useFormikContext();
const types = useRecoilValue(institutionTypeAtom);
const typeFind = (values) => {
types.find((t) => t.institution_type_id === values.institution_type_id);
return values.institution_type_id;
};
return (
<FormikStep>
<Colxx xxs="4">
<FormGroup>
<Label>
<IntlMessages id="institution.type" />
</Label>
<Select
placeholder="Selecione o tipo"
value={types.find((t) => t.institution_type_id === values.institution_type_id)}
isDisabled={isView}
onChange={(selectedType) => {
setFieldValue('institution_type_id', selectedType.institution_type_id);
}}
options={types}
className="react-select"
classNamePrefix="react-select"
name="institution_type_id"
aria-label="institution_type_id"
getOptionLabel={(option) => option.institution_type}
getOptionValue={(option) => option.institution_type_id}
/>
{getIn(errors, 'institution_type_id') && getIn(touched, 'institution_type_id') && (
<div className="invalid-feedback d-block">
{getIn(errors, 'institution_type_id')}
</div>
)}
</FormGroup>
</Colxx>
</Row>
</FormikStep>
);
};
export default InstitutionStep1;
and one of the input fields is a select where he can select a type for the institution (private, public, foreign...). I am also using a stepper, and formikStep is the final component from that stepper that receives the initialValues:
export const institutionInitialValues: InitialValues<BaseInstitutionType> = (institution = {}) => ({
institution_type_id: institution.institution_type_id ? institution.institution_type_id : '',
deleted: !!institution.deleted,
});
And I have written my test to get the users select from the rendered page, but I am also very new to the testing area so I am referencing other tests that were on this project already:
import React from 'react';
import userEvent from '#testing-library/user-event';
import {
cleanup, screen, act, render, waitFor, renderWithRecoilSnapshot,
} from 'testWrapper';
import * as institution from 'services/adminModules/institution';
import {
generateMockInstitution, mockAxiosResponse,
} from 'helpers/testMocks';
import { institutionTypeAtom } from 'recoil/institution';
import InstitutionForm from '../../views/app/admin/InstitutionsPage/InstitutionForm';
let createInstitution: jest.SpyInstance;
const mockInstitution = generateMockInstitution();
const mockSubmit = jest.fn();
const mockClose = jest.fn();
const renderComponent = () => renderWithRecoilSnapshot(<InstitutionForm
handleClose={mockClose}
handleSubmit={mockSubmit}
/>,
({ set }) => {
set(institutionTypeAtom, 1);
});
describe('InstitutionFormPage', () => {
afterEach(cleanup);
beforeEach(() => {
createInstitution = jest.spyOn(institution, 'createInstitution').mockImplementation(async () => mockAxiosResponse());
jest.spyOn(institution, 'getInstitution').mockImplementation(async () => mockAxiosResponse());
});
it('should submit institution data', async () => {
renderComponent();
await waitFor(() => expect(screen.queryByText('loading')).not.toBeInTheDocument());
screen.logTestingPlaygroundURL();
const institution_type_id = screen.getByRole('select', { name: 'institution_type_id' });
expect(institution_type_id).toBeInTheDocument();
await act(async () => {
await userEvent.selectOptions(institution_type_id, mockInstitution.institution_type_id);
});
const button = screen.getByRole('button', { name: /salvar/i });
expect(button).toBeInTheDocument();
await act(async () => {
await userEvent.click(button);
});
await waitFor(() => expect(mockSubmit).toHaveBeenCalledWith(mockInstitution));
});
});
Seemingly, the test is correct, but I am getting an error from when I run it's test that says that a .find I am using, in the component, is not a function.
So my main question would be, does this mean the test is correct and is appropriately catching a code error? Or is it wrong and it's not able to proceed? Also, the code it complains about works fine, and I was taught that an integration test tests api requests, so is there a reason why the test would complain about it, but the code would work fine?

Ternary condition on onclick event

I am trying to create a to-do application in React. Code I have so far adds to-do items to the to-do list. When I click on the edit icon I had put turnery condition for the done icon but it's not working. Can someone explain what is wrong with my code?
App.js
import './App.css';
import React, { useState } from 'react';
import TodoList from './TodoList';
import { v4 as uuidv4 } from 'uuid';
function App() {
// const [input, setInput] = useState('');
const [todos, setTodo] = useState([]);
const input = React.useRef();
const addTodo = (e) => {
e.preventDefault();
const id = uuidv4();
setTodo([...todos, { id: id, text: input.current.value }])
input.current.value='';
}
const deleteTodo = (id) => {
setTodo(todos.filter(todo => todo.id !== id));
}
const editTodo = (id) => {
}
return (
<div className="App">
<form>
<input type="text" ref={input}/>
<button type="submit" onClick={addTodo}>Enter</button>
</form>
<TodoList todos={todos} deleteTodo={deleteTodo} editTodo={editTodo}/>
</div>
);
}
export default App;
TodoItem.js
import React from 'react'
import DeleteIcon from '#material-ui/icons/Delete';
import EditIcon from '#material-ui/icons/Edit';
import CheckBoxOutlineBlankIcon from '#material-ui/icons/CheckBoxOutlineBlank';
import DoneIcon from '#material-ui/icons/Done';
const TodoItem = ({todo, deleteTodo, editTodo}) => {
return (
<>
<div>
<CheckBoxOutlineBlankIcon/>
<input type="text" value={todo.text} readOnly={true}/>
</div>
<div>
{ <EditIcon/> ? <EditIcon onClick={editTodo}/> : <DoneIcon/> }
<DeleteIcon onClick={deleteTodo}/>
</div>
</>
)
}
export default TodoItem
There are a few problems with your code. One, also pointed out by Abu Sufian is that your ternary operator will always trigger whatever is immediately after ?, because <EditIcon/> is just a component and will always be true.
But more fundamentally, to do what you want, you will need to add another properly to your todo list, say, status. So when a task goes in for the first time, it will be in pending status, then once you click your Edit icon, it will change to done. And that's how we will toggle that icon with a ternary operator.
So I would change your addTodo function to
const addTodo = (e) => {
e.preventDefault();
const id = uuidv4();
setTodo([
...todos,
{ id: id, text: input.current.value, status: "pending" }
]);
input.current.value = "";
};
Then I would change your editTodo to:
const editTodo = (id) => {
console.log(id);
setTodo(
todos.map((todo) => {
if (todo.id === id) todo.status = "done";
return todo;
})
);
};
And finally, I would change your ternary part to:
{todo.status === "pending" ? (
<EditIcon onClick={() => editTodo(todo.id)} />
) : (
<DoneIcon />
)}
Here is a complete Sandbox for you. Sorry I don't have your CSS so I can't make it look super pretty.
Maybe you are looking for something like this.
{ !todo.done ? <EditIcon onClick={editTodo}/> : <DoneIcon/> }
I believe checking whether a todo item is done or not should happen with a property of todo object itself.
In a ternary you need to start with a condition.
condition ? do something when true : do something when false
So you have to have a condition in the first place. In your case EditIcon is not a condition.
If you are looking for a way to mark a todo as completed so you need to do more things.
const markAsCompleted = id => {
const todo = todos.find(todo => todo.id !== id);
setTodo([...todos, {...todo, done: true }]);
}
Then you can decide based on whether a todo is done or not.

Resources