React Hooks Forms: defaultValues not working as expected - reactjs

I'm using hooks forms to fetch articles based on user's region. It can be changed any time, with select input, to change articles location.
import React, { useState, useEffect } from 'react';
import useForm from 'react-hook-form';
import { getRegions } from '../../services/regions';
import s from './style.module.scss';
function UpdateRegion({ putRegion, region }) {
const [regionsList, setRegionsList] = useState(null);
const [isLoading, setIsLoading] = useState(null);
const {
register, handleSubmit, watch, setValue,
} = useForm({
defaultValues: {
id: region !== null && region.id,
},
});
const { id } = watch();
const onSubmit = (data) => putRegion(regionsList.find((r) => (r.id === data.id)));
console.log('WATCH', id);
console.log('userRegionId', region !== null && region.id);
useEffect(() => {
register({ name: 'id' });
}, [register]);
const regions = async () => {
try {
setIsLoading(true);
const data = await getRegions();
setRegionsList(data);
setIsLoading(false);
} catch (error) {
setIsLoading(false);
console.log(error);
}
};
useEffect(() => {
if (regionsList === null && !isLoading) {
regions();
}
}, []);
useEffect(() => {
if (region !== null) {
setValue('id', region.id || '');
}
}, [region]);
return (
<form className={s.container}>
<select
name="id"
ref={register}
onChange={handleSubmit(onSubmit)}
className={s.container__select}
>
{regionsList !== null && (
regionsList.map((region) => (
<option
value={region.id}
key={region.id}
className={s.container__select__option}
>
{region.name}
</option>
))
)}
</select>
</form>
);
}
export default UpdateRegion;
DefaultValue should be all the time equal to region.id, and it works only if I refresh the page. If I change the page, and come back (without refreshing), my defaultValue is equal to the first option from select.
Why this is happening ?
EDIT
I resolved my issue by removing hooks forms:
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import Loading from 'Components/Loading';
import classNames from 'classnames';
import { getRegions } from '../../services/regions';
import s from './style.module.scss';
function UpdateRegion({ putRegion, region, className }) {
const [regionsList, setRegionsList] = useState(null);
const [isLoading, setIsLoading] = useState(null);
const regions = async () => {
try {
setIsLoading(true);
const data = await getRegions();
setRegionsList(data);
setIsLoading(false);
} catch (error) {
setIsLoading(false);
}
};
useEffect(() => {
if (regionsList === null && !isLoading) {
regions();
}
}, []); // eslint-disable-line
function changeRegion(e) {
putRegion(regionsList.find((r) => (r.id === e.target.value)));
}
if (region !== null) {
return (
<form className={classNames(s.container, className)}>
<select
name="id"
onChange={(e) => changeRegion(e)}
className={s.container__select}
value={region !== null && region.id}
>
{regionsList !== null && (
regionsList.map((r) => (
<option
value={r.id}
key={r.id}
className={s.container__select__option}
>
{r.name}
</option>
))
)}
</select>
</form>
);
} return <Loading small />;
}
UpdateRegion.propTypes = {
putRegion: PropTypes.func.isRequired,
region: PropTypes.object.isRequired,
className: PropTypes.string,
};
UpdateRegion.defaultProps = {
className: '',
};
export default UpdateRegion;

I think you have a wrong opinion about the defaultValue property. Let's try to explain why. First, when your hook calls for the first time your default value that has been passed from outside will be used later and it won't be changed any more. So, using the useEffect you will be able to observe and update the actual region id in your component.

Related

SearchBar Assistance-React

So I am trying to set a searchbar that has a drop down menu that gives you options on what you want to search for=and then let the user input. I have a list of inspections that I want to base that on.
For example I want to search inspections that only have issues with pests, so the user selects the pest option for search and then inputs-hive beetles
so the dropdown should have an option of searching by: pests, notes, actions taken, and so forth, my table has 8 values that I want to specific each search.
I have am using React for this and my database is in django, any suggestions on how to tackle this? The research I found is not really helpful.
My InpsectionSearch
import {React, useState} from "react";
const SearchInspections = (inspections) => {
const [searchField, setSearchField] = useState("")
const [searchResults, setSearchResults] = useState([])
function handleChange(event){
event.preventDefault()
const filteredInspections = inspections.filter((inspections) => {
return (
inspections.eggs().includes(searchField.toLowerCase()) ||
inspections.larvae().includes(searchField.toLowerCase()) ||
inspections.sealed_brood().includes(searchField.toLowerCase()) ||
inspections.covered_bees().includes(searchField.toLowerCase()) ||
inspections.nectar_honey().includes(searchField.toLowerCase()) ||
inspections.pollen().includes(searchField.toLowerCase()) ||
inspections.pest_spotted.toLowerCase().includes(searchField.toLowerCase()) ||
inspections.pest_action.toLowerCase().includes(searchField.toLowerCase()) ||
inspections.notes_concerns.toLowerCase().includes(searchField.toLowerCase())
)
})
setSearchResults(filteredInspections)
if(searchField.length > 0){
inspections(searchResults)
}
}
return (
<form onSubmit={handleChange}>
<h3>Search For Your Item</h3>
<input type="search" placeholder='Search Song' value={searchField} onChange={event => setSearchField(event.target.value)}/>
<button type='submit'>Click twice to search</button>
</form>
);
}
export default SearchInspections;
my Inspection Page where the searchbar will be
import { React, useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import axios from "axios";
import useAuth from "../../hooks/useAuth";
import DisplayInspections from "../../components/DisplayInspections/DisplayInspections";
import InspectionTracker from "../../components/InspectionTracker/InspectionTracker";
import SearchInspections from "../../components/InspectionSearch/InspectionSearch"
const InspectionPage = (props) => {
const [user, token] = useAuth();
const [inspections, setInspections] = useState([]);
const { id } = useParams();
useEffect(() => {
fetchInspections();
}, [token]);
const fetchInspections = async () => {
try {
let response = await axios.get(
`http://127.0.0.1:8000/api/inspections/all/${id}`,
{
headers: {
Authorization: "Bearer " + token,
},
}
);
setInspections(response.data);
} catch (error) {
console.log(error.response.data);
}
};
function applyFilter(category, userInput){
if(category === 'All' || userInput ===''){
console.log('working!')
fetchInspections();
}
else
{
let newInspections = inspections.filter(function(element){
if(element[category] === userInput){
return true;
}
})
setInspections(newInspections);
}
}
return (
<div className="container">
<InspectionTracker inspections={inspections} />
<SearchInspections inspections={inspections} />
<DisplayInspections
inspections={inspections}
setSelectedHive={props.setSelectedHive}
setSelectedInspection={props.setSelectedInspection}
/>
</div>
);
};
export default InspectionPage;

useEffect dosn't save data in localstorage

I have a simple app, sorta for chat purpuses. I fetch data from static file in json format. So this app shows all the messages from that file but also I want to edit the messeges, delete them and add via local storage. For that I used useEffect, but after refresh all the changes I do disappear.
This is my component:
export const WorkChat = (props) => {
const [messageValue, setMessageValue] = useState('');
const [edit, setEdit] = useState(null);
const [editmessageValue, setMessageEditValue] = useState('')
const submitMessage = () => {
const newMessage = {
id: Math.floor(Math.random() * 10000),
message: messageValue
}
props.addMessage(newMessage);
setMessageValue('')
}
const removeMsg = (id) => {
props.deleteMessage(id)
}
const goToEditMode = (message) => {
setEdit(message.id);
setMessageEditValue(message.message)
}
const saveChanges = (id) => {
const newMessagesArray = props.messages.map(m => {
if(m.id === id){
m.message = editmessageValue
}
return m
})
props.updateMessage(newMessagesArray);
setEdit(null)
}
useEffect(()=> {
let data = localStorage.getItem('work-messages');
if(data){
props.setMessages(JSON.parse(data))
}
}, []);
useEffect(()=> {
localStorage.setItem('work-messages', JSON.stringify(props.messages))
},[props.messages])
return (
<div className={s.workChatContainer}>
<input className={s.workInput} placeholder='Enter work message...' onChange={(e)=> setMessageValue(e.target.value)} value={messageValue}/>
<button className={`${s.btn} ${s.sendBtn}`} onClick={()=>submitMessage()}><SendIcon style={{fontSize: 20}}/></button>
<div>
{props.messages.map(m => (
<div key={m.id} className={s.messages}>
{edit !== m.id ? <div>
<span className={s.message}>{m.message}</span>
<button className={`${s.btn} ${s.deleteBtn}`} onClick={()=> removeMsg(m.id)}><DeleteOutlineIcon style={{fontSize: 15}}/></button>
<button className={`${s.btn} ${s.editBtn}`} onClick={()=> goToEditMode(m)}><EditIcon style={{fontSize: 15}}/></button>
</div>
:
<form>
<input className={s.editInput} value={editmessageValue} onChange={(e)=> setMessageEditValue(e.target.value)}/>
<button className={`${s.btn} ${s.saveBtn}`} onClick={()=> saveChanges(m.id)}><BeenhereIcon style={{fontSize: 15}}/></button>
</form>
}
</div>
))}
</div>
</div>
)
}
Just in case, this is my container component:
import { connect } from "react-redux"
import { setFloodMessagesAC, addFloodMessageAC, deleteFloodMessageAC, upadateMessageAC } from "../../redux/flood-reducer"
import { FloodChat } from "./FloodChat"
import { useEffect } from 'react'
import data from '../../StaticState/dataForFlood.json'
const FloodChatApiContainer = (props) => {
useEffect(()=> {
props.setFloodMessages(data)
}, [])
return <FloodChat messages={props.messages}
setFloodMessages={props.setFloodMessages}
addFloodMessage={props.addFloodMessage}
deleteFloodMessage={props.deleteFloodMessage}
upadateMessage={props.upadateMessage}
/>
}
const mapStateToProps = (state) => ({
messages: state.flood.messages
})
export const FloodChatContainer = connect(mapStateToProps, {
setFloodMessages: setFloodMessagesAC,
addFloodMessage: addFloodMessageAC,
deleteFloodMessage: deleteFloodMessageAC,
upadateMessage: upadateMessageAC
})(FloodChatApiContainer)
Why useEffect doesn't work? It seems to me like it should, but it doesnt.
I figured it out. Since I use data from static file, I need to implement functions that get/set data from/to local storage right where I import it which is container component. Once I put those useEffect functions in container component it works perfectly well.
const FloodChatApiContainer = (props) => {
useEffect(()=> {
props.setFloodMessages(data)
}, [])
useEffect(()=> {
let data = JSON.parse(localStorage.getItem('flood-messages'));
if(data){
props.setFloodMessages(data)
}
console.log('get')
}, [])
useEffect(() => {
localStorage.setItem('flood-messages', JSON.stringify(props.messages));
console.log('set')
}, [props.messages]);
return <FloodChat messages={props.messages}
setFloodMessages={props.setFloodMessages}
addFloodMessage={props.addFloodMessage}
deleteFloodMessage={props.deleteFloodMessage}
upadateMessage={props.upadateMessage}
/>
}
const mapStateToProps = (state) => ({
messages: state.flood.messages
})
export const FloodChatContainer = connect(mapStateToProps, {
setFloodMessages: setFloodMessagesAC,
addFloodMessage: addFloodMessageAC,
deleteFloodMessage: deleteFloodMessageAC,
upadateMessage: upadateMessageAC
})(FloodChatApiContainer)

Cannot access ___ before initialization reactjs useState useTracker Subscriptions Form state meteor

I have a form that takes its state from a react useState hook, that hooks default value I would like to come from a useTracker call, I am using pub sub in Meteor to do this. I get a error Cannot access '' before initialization I know it has something to do with the lead not being ready yet and returning undefined and the hook not being able to use that, at least I think so. But I am not sure how to solve that.
Here is my code thus far
import React, { useState } from "react";
import Dasboard from "./Dashboard";
import { Container } from "../styles/Main";
import { LeadsCollection } from "../../api/LeadsCollection";
import { LeadWalkin } from "../leads/LeadWalkin";
import { useTracker } from "meteor/react-meteor-data";
const Walkin = ({ params }) => {
const [email, setEmail] = useState(leads.email);
const handleSubmit = (e) => {
e.preventDefault();
if (!email) return;
Meteor.call("leads.update", email, function (error, result) {
console.log(result);
console.log(error);
});
setEmail("");
};
const { leads, isLoading } = useTracker(() => {
const noDataAvailable = { leads: [] };
if (!Meteor.user()) {
return noDataAvailable;
}
const handler = Meteor.subscribe("leads");
if (!handler.ready()) {
return { ...noDataAvailable, isLoading: true };
}
const leads = LeadsCollection.findOne({ _id: params._id });
return { leads };
});
console.log(leads);
//console.log(params._id);
const deleteLead = ({ _id }) => {
Meteor.call("leads.remove", _id);
window.location.pathname = `/walkin`;
};
return (
<Container>
<Dasboard />
<main className="split">
<div>
<h1>Edit a lead below</h1>
</div>
{isLoading ? (
<div className="loading">loading...</div>
) : (
<>
<LeadWalkin
key={params._id}
lead={leads}
onDeleteClick={deleteLead}
/>
<form className="lead-form" onSubmit={handleSubmit}>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Type to edit lead"
/>
<button type="submit">Edit Lead</button>
</form>
</>
)}
</main>
</Container>
);
};
export default Walkin;
It should work if you change the order of these two hooks, but it's probably better to break this into two components so that you can wait until your subscription is ready before you try to use leads.email as default value. It's not possible to branch out ('return loading`) in between hooks, because React doesn't like it when the number of hooks it finds in a component change in-between re-renderings.
const Walkin = ({ params }) => {
const { leads, isLoading } = useTracker(() => {
const noDataAvailable = { leads: [] };
if (!Meteor.user()) {
return noDataAvailable;
}
const handler = Meteor.subscribe("leads");
if (!handler.ready()) {
return { ...noDataAvailable, isLoading: true };
}
const leads = LeadsCollection.findOne({ _id: params._id });
return { leads };
});
if (isLoading || !leads) {
return <div>loading..</div>;
} else {
return <SubWalkin params=params leads=leads />;
}
};
const SubWalkin = ({ params, leads }) => {
const [email, setEmail] = useState(leads.email);
...
};

Testing Apollo Query with React Hook Client

I am trying to write test for this component using jest
import { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Query } from 'react-apollo';
import { updateYourDetails } from 'universal/domain/health/yourDetails/yourDetailsActions';
import Input from 'universal/components/input/input';
import InputNumber from 'universal/components/input/inputNumber/inputNumber';
import AsyncButton from 'universal/components/asyncButton/asyncButton';
import ErrorMessage from 'universal/components/errorMessage/errorMessage';
import Link from 'universal/components/link/link';
import analytics from 'universal/utils/analytics/analytics';
import { isChatAvailable } from 'universal/logic/chatLogic';
import { validators } from 'universal/utils/validation';
import { localTimezone, getWeekdays } from 'universal/utils/date';
import {
CALL_ME_BACK_LOADING_MSG,
CALL_ME_BACK_LABELS_SCHEDULE_TIME,
CALL_ME_BACK_LABELS_SELECTED_DATE,
CALL_ME_BACK_ERROR_MSG,
CALL_ME_BACK_TEST_PARENT_WEEKDAY,
CALL_ME_BACK_TEST_CHILD_WEEKDAY,
} from 'universal/constants/callMeBack';
import CallCenterAvailibility from './CallCenterAvailibility';
import SelectWrapper from './SelectWrapper';
import SelectOption from './SelectOption';
import styles from './callMeBackLightBox.css';
import { CALL_ME_BACK_QUERY } from './callMeBackQuery';
import postData from './postData';
export const CallMeForm = props => {
const initSelectedDate = getWeekdays()
.splice(0, 1)
.reduce(acc => ({ ...acc }));
const { onSubmissionComplete, className, variant } = props;
const [hasSuccessfullySubmitted, setHasSuccessfullySubmitted] = useState(false);
const [apiStatus, setApiStatus] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [cellNumber, setCallNumber] = useState(props.cellNumber || '');
const [customerFirstName, setCustomerFirstName] = useState(props.customerFirstName || '');
const [number, setNumber] = useState(props.Number || '');
const [selectedDate, setSelectedDate] = useState(initSelectedDate || '');
const [scheduledTime, setScheduledTime] = useState('');
const weekdays = getWeekdays() || [];
const timezone = localTimezone || '';
const requestReceived = apiStatus === 'CALLBACK_ALREADY_EXIST';
const cellNumberInput = useRef(null);
const customerFirstNameInput = useRef(null);
const getQuery = () => (
<Query query={CALL_ME_BACK_QUERY} variables={{ weekday: selectedDate.weekday }}>
{({ data, error, loading }) => {
if (loading)
return (
<SelectWrapper disabled labelTitle={CALL_ME_BACK_LABELS_SCHEDULE_TIME} name="scheduledTime">
<SelectOption label={CALL_ME_BACK_LOADING_MSG} />
</SelectWrapper>
);
if (error) return <ErrorMessage hasError errorMessage={<p>{CALL_ME_BACK_ERROR_MSG}</p>} />;
return (
<CallCenterAvailibility
selectedDate={selectedDate}
callCenterBusinessHour={data.callCenterBusinessHour}
onChange={val => setScheduledTime(val)}
/>
);
}}
</Query>
);
const getPostSubmitMessage = (firstName: string, type: string) => {
const messages = {
callCentreClosed: `a`,
requestReceived: `b`,
default: `c`,
};
return `Thanks ${firstName}, ${messages[type] || messages.default}`;
};
const validate = () => {
const inputs = [customerFirstNameInput, cellNumberInput];
const firstInvalidIndex = inputs.map(input => input.current.validate()).indexOf(false);
const isValid = firstInvalidIndex === -1;
return isValid;
};
const onSubmitForm = event => {
event.preventDefault();
onSubmit();
};
const onSubmit = async () => {
if (variant === '0' && !validate()) {
return;
}
analytics.track(analytics.events.callMeBack.callMeBackSubmit, {
trackingSource: 'Call Me Form',
});
setIsLoading(true);
const srDescription = '';
const response = await postData({
cellNumber,
customerFirstName,
number,
scheduledTime,
timezone,
srDescription,
});
const { status } = response;
const updatedSubmissionFlag = status === 'CALLBACK_ALREADY_EXIST' || status === 'CALLBACK_ADDED_SUCCESSFULLY';
// NOTE: add a slight delay for better UX
setTimeout(() => {
setApiStatus(apiStatus);
setIsLoading(false);
setHasSuccessfullySubmitted(updatedSubmissionFlag);
}, 400);
// Update Redux store
updateYourDetails({
mobile: cellNumber,
firstName: customerFirstName,
});
if (onSubmissionComplete) {
onSubmissionComplete();
}
};
if (hasSuccessfullySubmitted) {
return (
<p aria-live="polite" role="status">
{getPostSubmitMessage(
customerFirstName,
(!requestReceived && !isChatAvailable() && 'callCentreClosed') || (requestReceived && 'requestReceived')
)}
</p>
);
}
return (
<form onSubmit={onSubmitForm} className={className}>
{variant !== '1' && (
<>
<label htmlFor="customerFirstName" className={styles.inputLabel}>
First name
</label>
<Input
className={styles.input}
initialValue={customerFirstName}
isMandatory
maxLength={20}
name="customerFirstName"
onChange={val => setCustomerFirstName(val)}
ref={customerFirstNameInput}
value={customerFirstName}
{...validators.plainCharacters}
/>
</>
)}
{variant !== '1' && (
<>
<label htmlFor="cellNumber" className={styles.inputLabel}>
Mobile number
</label>
<Input
className={styles.input}
initialValue={cellNumber}
isMandatory
maxLength={10}
name="cellNumber"
onChange={val => setCallNumber(val)}
ref={cellNumberInput}
type="tel"
value={cellNumber}
{...validators.tel}
/>
</>
)}
{variant !== '1' && (
<>
{' '}
<label htmlFor="number" className={styles.inputLabel}>
Qantas Frequent Flyer number (optional)
</label>
<InputNumber
className={styles.input}
disabled={Boolean(props.number)}
initialValue={number}
name="number"
onChange={val => setNumber(val)}
value={number}
/>
</>
)}
{weekdays && (
<>
<SelectWrapper
testId={`${CALL_ME_BACK_TEST_PARENT_WEEKDAY}`}
labelTitle={CALL_ME_BACK_LABELS_SELECTED_DATE}
name="selectedDate"
onChange={val =>
setSelectedDate({
...weekdays.filter(({ value }) => value === val).reduce(acc => ({ ...acc })),
})
}
tabIndex={0}
>
{weekdays.map(({ value, label }, i) => (
<SelectOption
testId={`${CALL_ME_BACK_TEST_CHILD_WEEKDAY}-${i}`}
key={value}
label={label}
value={value}
/>
))}
</SelectWrapper>
{getQuery()}
</>
)}
<AsyncButton className={styles.submitButton} onClick={onSubmit} isLoading={isLoading}>
Call me
</AsyncButton>
<ErrorMessage
hasError={(apiStatus >= 400 && apiStatus < 600) || apiStatus === 'Failed to fetch'}
errorMessage={
<p>
There was an error submitting your request to call you back. Please try again or call us at{' '}
<Link href="tel:134960">13 49 60</Link>.
</p>
}
/>
</form>
);
};
CallMeForm.propTypes = {
cellNumber: PropTypes.string,
customerFirstName: PropTypes.string,
number: PropTypes.string,
onSubmissionComplete: PropTypes.func,
className: PropTypes.string,
variant: PropTypes.string,
};
const mapStateToProps = state => {
const { frequentFlyer, yourDetails } = state;
return {
cellNumber: yourDetails.mobile,
customerFirstName: yourDetails.firstName,
number: frequentFlyer.memberNumber,
};
};
export default connect(mapStateToProps)(CallMeForm);
My test file is as below
import { render, cleanup } from '#testing-library/react';
import { MockedProvider } from 'react-apollo/test-utils';
import { shallow } from 'enzyme';
import MockDate from 'mockdate';
import { isChatAvailable } from 'universal/logic/chatLogic';
import { CALL_ME_BACK_QUERY } from './callMeBackQuery';
import { CallMeForm } from './CallMeForm';
import postData from './postData';
jest.mock('universal/components/input/input', () => 'Input');
jest.mock('universal/components/asyncButton/asyncButton', () => 'AsyncButton');
jest.mock('universal/components/errorMessage/errorMessage', () => 'ErrorMessage');
jest.mock('universal/logic/chatLogic');
jest.mock('./postData');
describe('CallMeForm', () => {
let output;
beforeEach(() => {
jest.resetModules();
jest.resetAllMocks();
const mockQueryData = [
{
client:{},
request: {
query: CALL_ME_BACK_QUERY,
variables: { weekday: '' },
},
result: {
data: {
callCenterBusinessHour: {
timeStartHour: 9,
timeStartMinute: 0,
timeEndHour: 5,
timeEndMinute: 0,
closed: false,
},
},
},
},
];
const { container } = render(<MockedProvider mocks={mockQueryData} addTypename={false}><CallMeForm /></MockedProvider>);
output = container;
});
afterEach(cleanup);
it('renders correctly', () => {
expect(output).toMatchSnapshot();
});
});
I keep getting error: TypeError: this.state.client.stop is not a function
I also removed <MockedProvider> wrapper and I got another error Invariant Violation: Could not find "client" in the context or passed in as a prop. Wrap the root component in an , or pass an ApolloClient instance in
via props.
Does anyone know why I get this error and how to fix this?
I have not the solution, but I've got some information.
First of all, I'm having the same error here, rendering with #testing-library/react.
I then tried to render with ReactDOM, like that:
// inside the it() call with async function
const container = document.createElement("div");
ReactDOM.render(
< MockedProvider {...props}>
<MyComponent />
</MockedProvider>,
container
);
await wait(0);
expect(container).toMatchSnapshot();
And also tried to render with Enzyme, like that:
// inside the it() call, with async function too
const wrapper = mount(
<MockedProvider {...props}>
<MyComponent />
</MemoryRouter>
);
await wait(0);
expect(wrapper.html()).toMatchSnapshot();
Both ReactDOM and Enzyme approaches worked fine.
About the error we're getting, I think maybe it's something related with #testing-library/react =/
I didn't tried to render with react-test-renderer, maybe it works too.
Well, that's what I get... maybe it helps you somehow.
Ps.: About waait: https://www.apollographql.com/docs/react/development-testing/testing/#testing-final-state
EDIT 5 Feb 2020:
Based on https://github.com/apollographql/react-apollo/pull/2165#issuecomment-478865830, I found that solution (it looks ugly but works ¯\_(ツ)_/¯):
<MockedProvider {...props}>
<ApolloConsumer>
{client => {
client.stop = jest.fn();
return <MyComponent />;
}}
</ApolloConsumer>
</MockedProvider>
I had the same problem and was able to solve it. I had a missing peer dependency.
Your package.json is not shown so I am not sure if your problem is the same as mine but I was able to resolve the problem by installing "apollo-client".
I am using AWS Appsync for my client and hence did not have apollo-client installed.

How to start search only when user stops typing?

I need to perform a Search when user stops typing.I know I am supposed to use setTimeout() . But with Reactjs I cant find how it works. Can someone please tell me how to invoke a method (that will handle Search) when the user stops typing for a few seconds (suppose 5).I cant figure out where to write the code to check that the user has stopped typing.
import React, {Component, PropTypes} from 'react';
export default class SearchBox extends Component {
state={
name:" ",
}
changeName = (event) => {
this.setState({name: event.target.value});
}
sendToParent = () => {
this.props.searching(this.state.name);
}
render() {
return (
<div>
<input type="text" placeholder='Enter name you wish to Search.' onChange={this.changeName} />
</div>
);
}
}
I want to invoke the sendToParent method when the user stops typing.
Implement using useEffect hook:
function Search() {
const [searchTerm, setSearchTerm] = useState('')
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
console.log(searchTerm)
// Send Axios request here
}, 3000)
return () => clearTimeout(delayDebounceFn)
}, [searchTerm])
return (
<input
autoFocus
type='text'
autoComplete='off'
className='live-search-field'
placeholder='Search here...'
onChange={(e) => setSearchTerm(e.target.value)}
/>
)
}
You can use setTimeout with respect to your code as follows,
state = {
name: '',
typing: false,
typingTimeout: 0
}
changeName = (event) => {
const self = this;
if (self.state.typingTimeout) {
clearTimeout(self.state.typingTimeout);
}
self.setState({
name: event.target.value,
typing: false,
typingTimeout: setTimeout(function () {
self.sendToParent(self.state.name);
}, 5000)
});
}
Also, you need to bind changeName handler function in constructor.
constructor(props) {
super(props);
this.changeName = this.changeName.bind(this);
}
Another way that worked with me:
class Search extends Component {
constructor(props){
super(props);
this.timeout = 0;
}
doSearch(evt){
var searchText = evt.target.value; // this is the search text
if(this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
//search function
}, 300);
}
render() {
return (
<div className="form-group has-feedback">
<label className="control-label">Any text</label>
<input ref="searchInput" type="text" onChange={evt => this.doSearch(evt)} />
</div>
);
}
}
This library (use-debounce) is nice and simple.
Setup
yarn add use-debounce
or
npm i use-debounce --save
Usage sample from documentation
import React, { useState } from 'react';
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>
);
}
Things that I liked at this moment, things could be different in
future!:
Easy to setup & use
Less Boilerplate code
Modest ratings (~1K) and usage (npm - 200K downloads/Week)
Supports timeout, MaxWait and other features
I used the debounce function of lodash
onChangeSearchInput = (evt)=> {
this.debouncedSearch(evt.target.value);
};
debouncedSearch = debounce(function (query) {
this.setState({query});
}, 1000);
Somewhere in my render method i have this input field
<input
type='text'
onChange={this.onChangeSearchInput}
className='uk-input'
placeholder={'search by name or email...'}
/>
I have use this custom hook and it's work perfectly no issue still.
export function useSearchDebounce(delay = 350) {
const [search, setSearch] = useState(null);
const [searchQuery, setSearchQuery] = useState(null);
useEffect(() => {
const delayFn = setTimeout(() => setSearch(searchQuery), delay);
return () => clearTimeout(delayFn);
}, [searchQuery, delay]);
return [search, setSearchQuery];
}
Use in any place like
const [search, setSearch] = useSearchDebounce();
<input onChange={(e) => setSearch(e.target.value)}/>
I think we can do it in a more simpler and cleaner manner, without abrupting the state parameter which calls the complete component life cycle like this:
constructor(props) {
super(props);
//Timer
this.typingTimeout = null;
//Event
this.onFieldChange = this.onFieldChange.bind(this);
//State
this.state = { searchValue: '' };
}
/**
* Called on the change of the textbox.
* #param {[Object]} event [Event object.]
*/
onFieldChange(event) {
// Clears the previously set timer.
clearTimeout(this.typingTimeout);
// Reset the timer, to make the http call after 475MS (this.callSearch is a method which will call the search API. Don't forget to bind it in constructor.)
this.typingTimeout = setTimeout(this.callSearch, 475);
// Setting value of the search box to a state.
this.setState({ [event.target.name]: event.target.value });
}
<div className="block-header">
<input
type="text"
name="searchValue"
value={this.state.searchValue}
placeholder="User Name or Email"
onChange={this.onFieldChange}
/>
</div>
you can use react hooks useEffect with the use of setTimeOut function since it always return the timer id and you could easily clear the timer with that id as follows
export const Search = () => {
const [term, setTerm] = useState();
const [results, setResult] = useState([]);
useEffect(() => {
const searchWiki = async () => {
const { data } = await axios.get('https://en.wikipedia.org/w/api.php', {
params: {
srsearch: term,
},
});
setResult(data.query.search);
};
const timerId = setTimeout(() => {
searchWiki();
// make a request after 1 second since there's no typing
}, 1000);
return () => {
clearTimeout(timerId);
};
}, [term]);
How about a custom hook?
import {useEffect, useRef, useState} from "react";
export default function useSearchInputState(searchHandler) {
// to prevent calling the handler on component mount
const didMountRef = useRef(false);
const [searchValue, setSearchValue] = useState(null);
useEffect(() => {
let delayDebounceFn;
if (didMountRef.current) {
delayDebounceFn = setTimeout(searchHandler, 600)
} else {
didMountRef.current = true;
}
return () => clearTimeout(delayDebounceFn);
}, [searchValue]); // eslint-disable-line react-hooks/exhaustive-deps
return [searchValue, setSearchValue];
}
Usage:
function MyComponent(props) {
const [searchValue, setSearchValue] = useSearchInputState(() => {
resetData(searchValue ?? null, selectedFilterPos); // replace with your code
});
return (
<input className="Search"
onChange={e => setSearchValue(e?.target?.value ?? null)}
/>
);
}
you can just use the debounce from lodash or simulate using setTimeout.
import React, {Component, PropTypes} from 'react';
export default class SearchBox extends Component {
constructor(props){
super(props);
this.state={ name:" "}
this.timeout = null;
}
changeName = (event) => {
clearTimeout(timeout);
if(timeout){
setTimeout((event)=> this.setState({name: event.target.value}), 200)
}
}
sendToParent = () => {
this.props.searching(this.state.name);
}
render() {
return (
<div>
<input type="text" placeholder='Enter name you wish to Search.' onChange={this.changeName} />
</div>
);
}
}
I made my own custom component like this.
import React, { useState, useEffect } from 'react'
const InputDebounce = props => {
const { onChange, ...otherProps } = props
const [inputTimeout, setInputTimeout] = useState(null)
useEffect(() => () => clearTimeout(inputTimeout), [inputTimeout])
const inputOnChange = value => {
if (inputTimeout) clearTimeout(inputTimeout)
setInputTimeout(
setTimeout(() => {
if (onChange) onChange(value)
}, 1000)
)
}
return (
<input
{...otherProps}
onChange={e => inputOnChange(e.target.value)}
/>
)
}
export default InputDebounce
And using anywhere like this.
import React from 'react'
import ReactDOM from 'react-dom'
import InputDebounce from './InputDebounce'
const App = () => {
const usernameOnChange = value => {
console.log(value)
}
return (
<div>
<InputDebounce
type='text'
name='username'
placeholder='Username'
onChange={usernameOnChange}
/>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
For React hooks:
First we'll define a component
import React, { useEffect, useState } from "react";
const SearchInputText = ({ value, name, placeholder, onChange }) => {
// state for keepign search text
const [searchText, setSearchText] = useState(value);
// state for keeping the timeout
const [searchTextTimeout, setSearchTextTimeout] = useState(null);
// handler for form submit (pressing enter without waiting for setimeout to trigger)
const handleSubmit = (e) => {
e.preventDefault();
// clear timeout as it'll that would be triggered
if (searchTextTimeout) {
clearTimeout(searchTextTimeout);
}
onChange(searchText);
};
// onChange handler
const handleOnChange = (e) => {
// cancelling previous timeouts
if (searchTextTimeout) {
clearTimeout(searchTextTimeout);
}
// first update the input text as user type
setSearchText(e.target.value);
// initialize a setimeout by wrapping in our searchTextTimeout so that we can clear it out using clearTimeout
setSearchTextTimeout(
setTimeout(() => {
onChange(searchText);
// timeout is 2500ms, change it to less or more.
}, 2500),
);
};
// making sure that we clear the timeout if/when the component unmount
useEffect(() => {
return () => clearTimeout(searchTextTimeout);
}, [searchTextTimeout]);
return (
<form onSubmit={handleSubmit}>
<input
name={name}
placeholder={placeholder}
type="text"
value={searchText}
onChange={handleOnChange}
/>
</form>
);
};
export default SearchInputText;
Usage:
const Parent = () => {
const handleChange = (e) => {
// your implementation here
};
return (
<div>
<SortSearchInput name="search" placeholder="Enter Search" onChange={handleChange} />
</div>
);
};
The code below works well for me :
const [filter, setFilter] = useState()
useEffect(() => {
const search = setTimeout(() => {
getList()
//Your search query and it will run the function after 3secs from user stops typing
}, 3000);
return () => clearTimeout(search)
}, [filter])
and add HTML like this:
<input type="text" onInput={(e) => setFilter(e.target.value)} value={filter} />
Here is an approach using functional components and the useRef hook.
import React, { useRef, useEffect } from "react";
function Search() {
const [searchTerm, setSearchTerm] = React.useState("");
const inputRef = useRef<any>()
useEffect(() => {
let timer: NodeJS.Timeout | null = null
const sendData = () => {
// If the user keeps on typing then the timeout is cleared and restarted
if(timer) clearTimeout(timer)
timer = setTimeout(() => {
setSearchTerm(inputRef.current.value)
}, 3000)
}
const element = inputRef.current;
// Set listener and start timeout
element.addEventListener('keyup', sendData);
return () => {
// Remove listener wwhen unmounting
element.removeEventListener('keyup', sendData);
};
}, []);
return (
<div>
<input
ref={inputRef}
autoFocus
type="text"
autoComplete="off"
className="live-search-field"
placeholder="Search here..."
/>
<p>searchTerm: {searchTerm}</p>
</div>
);
}
export default Search;
This approach avoids unnecessary re-renders and utilizes event listeners to handle the search submission when user stops typing.
Here's a working component template with some useful parameters to get your started.
import React, { Component } from 'react'
const initialState = { results: [], value: '' }
export default class SearchBox extends Component {
state = initialState
timeout = null
search_url = "https://example.com/search?q="
min_query_length = 2
timeout_duration = 300
handleSearchChange = (e) => {
let value = e.target.value
clearTimeout(this.timeout);
if (value.length < 1) {
return this.setState(initialState)
} else {
this.setState({ value })
if (value.length>=this.min_query_length) {
this.timeout = setTimeout(this.search, this.timeout_duration);
}
}
}
search = () => {
// assuming your results are returned as JSON
fetch(`${this.search_url}${this.state.value}`)
.then(res => res.json())
.then(data => {
this.setState({
results: data,
})
})
}
render() {
return (
<input
onChange={this.handleSearchChange}
/>
)
}
}
using react hooks, modified from #anoNewb's answer. With additions:
prevent multiple triggers when there's still timer running
add on Form Submit event
codesandbox
import React, { useState, useEffect } from "react";
export default function App() {
const [search, setSearch] = useState("");
const [searchTimeout, setSearchTimeout] = useState(null);
useEffect(() => {
if (searchTimeout) {
clearTimeout(searchTimeout);
}
setSearchTimeout(
setTimeout(() => {
loadUsers();
}, 1000),
);
return () => clearTimeout(searchTimeout);
}, [search]);
const loadUsers = () => {
console.log("axios call with query: ", search);
};
return (
<div className="App">
<form
onSubmit={(e) => {
e.preventDefault();
if (searchTimeout) {
clearTimeout(searchTimeout);
}
loadUsers();
}}
>
<input
onChange={(e) => {
setSearch(e.target.value);
}}
/>
</form>
</div>
);
}
The code below works for me.
const[isReady, setReady] = useState(true);
const onSearchSet =(event:React.ChangeEvent<HTMLInputElement>) => {
setCriteria(event.target.value);
if(isReady) {
setReady(false);
const delayDebounceFn = setTimeout(() => {
// Send Axios request here
props.returnCall(props.RDropID, sortCriteria, event.target.value);
setReady(true);
}, 1000)
}
};
Can I use this code with Saga? It will help send the latest request. The time on the set time out can be changed. In my case, I used 600ms.
const dispatch = useDispatch();
const [searchText, setSearchText] = useState('');
useEffect(() => {
const sendSearchRequest = setTimeout(() => {
if (searchText && searchText.length > 2) {
dispatch(sendRequestToSaga(searchText));
}
}, 600);
return () => clearTimeout(sendSearchRequest);
}, [searchText]);
This is much easier now with useEffect and does not need any library
import React, { useEffect, useState } from 'react'
import ReactDOM from 'react-dom'
const FuncDemo = () => {
const [searchStr, setSearchStr] = useState('')
useEffect(() => {
const makeApiCall = async () => {
try {
// your axios call
} catch (e) {
}
}
const triggerCall = setTimeout(() => {
makeApiCall()
}, 500)
return () => clearTimeout(triggerCall)
}, [searchStr])
return (
<input
name='search'
onChange={e => setSearchString(e.target.value)}
/>
)
}
ReactDOM.render(<FuncDemo/>, document.getElementById('root'))
function debounce(func, timeout = 300){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
function search(){
console.log('search');
}
const processChange = debounce(() => search());
It can be used in input
<input type="text" onkeyup="processChange()" />
User lodash javascript library and use [_.debounce][1]
changeName: _.debounce(function (val) {
console.log(val)
}, 1000)
Problem of Typeahead library https://twitter.github.io/typeahead.js/
Since the case here is simple, I can use a quick and dirty solution:
onChange: (event) ->
if #_timeoutTask?
clearTimeout #_timeoutTask
#_timeoutTask = setTimeout (=>
#sendToParent event.target.value
clearTimeout #_timeoutTask
), 5000
In this way, the task will be triggered 5s after input event. If new event happens, the old task will be cancelled and a new task is scheduled, then it's another 5s to wait.
The difference in React is the where to store the computation state like _timeoutTask. The file scope, the component state, or the component instance.
Since _timeoutTask is component level, it should be be store globally. And it does not affect rendering, so not in component state too. So I suggest attaching it to component instance directly.

Resources