Testing Apollo Query with React Hook Client - reactjs

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.

Related

How to use useSearchParams Hook with React Router v6

I am trying to implement a search parameter functionality to my React image search app. And, I have learned that I need to (can) use the useSearchParams Hook, but I am not sure how to make these changes.
So, basically I want the URL to be something like localhost:3000/input&page=1, meaning that whatever comes after the slash is going to be the input value and key/value pair for page numbers.
As you can see in the app.js, I have these 3 main Routes and the Home Route (renders Main.js) is the one I am mainly working on. Also, Main.js renders Header.js (renders form and others).
I am thinking that I should create a new Route in the app.js but I am not sure what to do.
import './App.css';
import Home from './components/pages/Home';
import Favorites from './components/pages/Favorites';
import Error from './components/pages/Error';
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { SkeletonTheme } from 'react-loading-skeleton';
import { useDarkMode } from './components/Navbar';
function App() {
const darkMode = useDarkMode(state => state.darkMode)
let style
if (darkMode === 'light') {
style = 'wrapper'
} else {
style = 'wrapper-dark'
}
return (
<div className={style}>
<SkeletonTheme baseColor="#808080" highlightColor="#b1b1b1">
<BrowserRouter>
<Routes>
<Route path='/' element={<Home />} />
<Route path='favorites' element={<Favorites />} />
<Route path='*' element={<Error />} />
</Routes>
</BrowserRouter>
</SkeletonTheme>
</div>
);
}
export default App;
import React from 'react'
import Header from './Header'
import Image from './Image'
import { useState, useEffect, useRef } from 'react'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faTriangleExclamation } from '#fortawesome/free-solid-svg-icons'
// import InfiniteScroll from 'react-infinite-scroll-component'
function Main() {
const [input, setInput] = useState('')
const [allImages, setAllImages] = useState([])
// const [totalResults, setTotalResults] = useState(null)
const [isVisible, setIsVisible] = useState(false)
const [error, setError] = useState(null)
const [showError, setShowError] = useState(false)
const [fadeOut, setFadeOut] = useState(false)
const [page, setPage] = useState(1)
const paginationRef = useRef(false)
// get
useEffect(() => {
if (localStorage.getItem('input')) {
setInput(JSON.parse(localStorage.getItem('input')))
}
if (localStorage.getItem('allImages')) {
setAllImages(JSON.parse(localStorage.getItem('allImages')))
// setTotalResults(JSON.parse(localStorage.getItem('totalResults')))
setIsVisible(JSON.parse(localStorage.getItem('isVisible')))
setPage(JSON.parse(localStorage.getItem('page')))
paginationRef.current = true
}
}, [])
// set
//* dryer?
useEffect(() => {
localStorage.setItem('input', JSON.stringify(input))
}, [input])
useEffect(() => {
localStorage.setItem('allImages', JSON.stringify(allImages))
}, [allImages])
// useEffect(() => {
// localStorage.setItem('totalResults', JSON.stringify(totalResults))
// }, [totalResults])
useEffect(() => {
localStorage.setItem('isVisible', JSON.stringify(isVisible))
}, [isVisible])
function handleChange(event) {
setInput(event.target.value)
}
// display nothing by default
// display image-list when user press search button
// function handleSubmit(event) {
// event.preventDefault()
// // interpolate input state and .env variable to API
// fetch(`https://api.unsplash.com/search/photos?query=${input}&client_id=${process.env.REACT_APP_UNSPLASH_API_KEY}`)
// .then(res => res.json())
// .then(data => setAllImages(data.results))
// }
async function fetchImages() {
try {
const res = await fetch(`https://api.unsplash.com/search/photos?&page=${page}&per_page=30&query=${input}&client_id=${process.env.REACT_APP_UNSPLASH_API_KEY}`)
const data = await res.json()
if (data.total !== 0) {
setAllImages(data.results)
// setTotalResults(data.total)
setIsVisible(true)
}
} catch(error) {
setError(error)
}
}
const handleSubmit = async (event) => {
event.preventDefault();
fetchImages()
setPage(1)
paginationRef.current = true
}
// error
useEffect(() => {
if (error) {
setShowError(true)
setTimeout(() => {
setFadeOut(true)
setTimeout(() => {
setShowError(false)
}, 1000)
}, 5000)
}
}, [error])
// total results
// let results
// if (totalResults >= 10000) {
// results = 'Total Results: ' + totalResults + '+'
// } else if (totalResults > 0) {
// results = 'Total Results: ' + totalResults
// } else if (totalResults === 0) {
// results = 'Nothing Found'
// }
// pagination
useEffect(() => {
if (paginationRef.current) {
fetchImages()
}
localStorage.setItem('page', JSON.stringify(page))
}, [page])
function handlePrev() {
setPage(prevState => prevState - 1)
fetchImages()
}
function handleNext() {
setPage(prevState => prevState + 1)
fetchImages()
}
return (
<main>
<Header
input={input}
handleChange={handleChange}
handleSubmit={handleSubmit}
/>
{showError && <div className={`network-error ${fadeOut ? 'fade-out' : ''}`}>
<i><FontAwesomeIcon icon={faTriangleExclamation} /></i>
<div className='network-error--message'>
<h5>Network Error</h5>
<p>Please check your Internet connection and try again</p>
</div>
</div>}
{/* <p className='main--results'>{results}</p> */}
<div className='main--image-list mt-5 pb-5'>
{allImages.map(el => (
<Image
key={el.id}
// do need spread operator below for img's src to work in Image.js
{...el}
el={el}
/>
))}
</div>
{isVisible && <div className='main--pagination'>
<button disabled={page === 1} onClick={handlePrev}>
Prev
</button>
<h5 className='main--pagination--h5'>{page}</h5>
<button onClick={handleNext}>
Next
</button>
</div>}
</main>
)
}
export default Main
import React from 'react'
import Navbar from './Navbar'
function Header(props) {
return (
<div className='header'>
<Navbar />
<h2 className='header--heading text-center text-light'>Find Images</h2>
<div className='header--form'>
<form onSubmit={props.handleSubmit}>
<input
className='header--form--input'
autoComplete='off'
type='text'
placeholder='Search'
onChange={props.handleChange}
name='input'
value={props.input}
/>
</form>
</div>
</div>
)
}
export default Header
If you are just wanting to initialize the page state to the page queryParam the the following could work. If uses the useSearchParams to access the queryString and return a constructed URLSearchParams object which can then access individual query params. Pass the "page" query param as the initial page state value.
const [searchParams] = useSearchParams();
const [page, setPage] = useState(Number(searchParams.get("page")) || 1);
In all likelihood though you'll not want competing "sources of truth" for what the current page is. If you want the URL queryString to be the source of truth then remove the page state and just read/update the "page` query parameter directly.
Example:
function Main() {
const [searchParams, setSearchParams] = useSearchParams();
...
const page = Number(searchParams.get("page"));
// get
useEffect(() => {
...
if (localStorage.getItem('allImages')) {
...
setSearchParams(params => {
params.set("page", JSON.parse(localStorage.getItem('page')) || 1);
return params;
});
...
}
}, []);
...
const handleSubmit = async (event) => {
event.preventDefault();
...
setSearchParams(params => {
params.set("page", 1);
return params;
});
...
}
...
// pagination
useEffect(() => {
if (paginationRef.current) {
fetchImages();
}
localStorage.setItem('page', JSON.stringify(page));
}, [page])
function handlePrev() {
setSearchParams(params => {
params.set("page", Math.max(1, page - 1));
return params;
});
...
}
function handleNext() {
setSearchParams(params => {
params.set("page", page + 1);
return params;
});
...
}
return (
...
);
}

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)

Call a function from a class in a different file - React

I'm basically trying to call a function (getValue) from a class (Time) in a different file, but there is some issues.
Here is the code for the two files:
Time.js
export default class Time extends Component {
constructor(props) {
super(props);
this.state = {
input: '',
input2: '',
checked: false
}
this.getValue = this.getValue.bind(this);
}
hrChange = e => {
this.setState({input: e.target.value}, function () {this.getValue()})
}
minChange = e => {
this.setState({input2: e.target.value}, function () {this.getValue()})
}
amPm = () => {
this.setState({checked: !this.state.checked}, function () {this.getValue()})
}
getValue = () => {
const list = [
this.state.input,
this.state.input2,
this.state.checked
]
return (list)
}
render() {
return(
<text>some stuff</text>
)
}
}
NewStorage.js
function NewStorage() {
const time = () => {
var obj = new Time();
var list = obj.getValue()
const
hrInput = list[0],
minInput = list[1],
pm = list[2]
return(
console.log(hrInput, minInput, pm, list)
)
return(
time()
)
}
export default NewLocalStorage;
The main issue isn't that I can't call the function, it is that when I call the function, the values of input, input2, and checked are all the original value ('', '', false), not the updated versions (ex: '11', '30', true).
I'm not sure on how to solve this issue.
Your inclusion of the react-hooks tag suggest your hunch that hooks are applicable to solving your problem. I would agree -
const { useState, useEffect } = React
function Time ({ hour, minute, onChange }) {
const [h,setHour] = useState(hour)
const [m,setMinute] = useState(minute)
useEffect(_ => onChange({ hour: h, minute: m }), [h, m])
return <div>
<input value={h} onChange={event => setHour(event.target.value)} />
<input value={m} onChange={event => setMinute(event.target.value)} />
</div>
}
ReactDOM.render(<Time onChange={console.log} />, document.querySelector("main"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<main></main>
In a more sophisticated example, we can use the Time component's onChange callback to update nested state in a parent component, MyForm -
const { useState, useEffect, useCallback } = React
function Time ({ hour = 0, minute = 0, onChange }) {
const [h,setHour] = useState(hour)
const [m,setMinute] = useState(minute)
useEffect(_ => onChange({ hour: h, minute: m }), [h, m, onChange])
return <div>
<input value={h} onChange={event => setHour(event.target.value)} />
<input value={m} onChange={event => setMinute(event.target.value)} />
</div>
}
function MyForm () {
const [data, setData] = useState({ time: { hour: 5, minute: 30 }, foo: "bar" })
const onTimeChange = useCallback(t => setData({ ...data, time: t }), [])
return <form>
<Time hour={data.time.hour} minute={data.time.minute} onChange={onTimeChange} />
<pre>{JSON.stringify(data, null, 2)}</pre>
</form>
}
ReactDOM.render(<MyForm />, document.querySelector("main"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<main></main>
Instead of trying to create a class and call the function in another file, why not use React functional components and hooks?
Try something like this:
const Clock = () => {
const [hour, setHour] = useState();
const [min, setMin] = useState();
const [am, setAm] = useState(true);
useEffect(() => {
// Get your clock to work in here...
}, [hour, min, am]);
return (
<div>
{//This will post your clock here, and if you need the values, you
can set/use them individually as needed.}
{hour}:{min} {am ? 'am' : 'pm'}
{//The ternary statement will modify this portion for you in code.}
</div>
);
}
If you want to use the values globally, you may want to try using the React hook useContext(). This will allow you to access those specific values anywhere you want, but requires a bit more setup.
Context, if you don't know will turn your react app into Redux, without using Redux. Below is an example of what you need to do.
import { createContext } from "react";
export const QuizContext = createContext();
then you add the context to your App.js:
import { useState } from 'react';
import './App.css';
import MainMenu from './Components/MainMenu';
import Quiz from './Components/Quiz';
import EndScreen from './Components/EndScreen';
import { QuizContext } from './Helpers/Context';
function App() {
const [gameState, setGameState] = useState('Menu');
const [score, setScore] = useState(0);
return (
<div className="App">
<h1>Quiz App</h1>
<QuizContext.Provider value={{gameState, setGameState, score, setScore}}>
{gameState === 'Menu' && <MainMenu/>}
{gameState === 'Quiz' && <Quiz/>}
{gameState === 'EndScreen' && <EndScreen/>}
</QuizContext.Provider>
</div>
);
}
Then you can access the context from individual components as long as they are children of App.
Example:
import React, { useContext, useState } from 'react';
import { QuizContext } from '../Helpers/Context';
import {Questions} from '../Helpers/QuestionBank'
const Quiz = () => {
const [currentQuestion, setCurrentQuestion] = useState(0)
const [optionChosen, setOptionChosen] = useState('');
const {setGameState, score, setScore} = useContext(QuizContext);
const nextQuestion = () => {
Questions[currentQuestion].answer === optionChosen ? setScore(score + 1) : console.log(score);
setCurrentQuestion(currentQuestion + 1);
}
const finishQuiz = () => {
Questions[currentQuestion].answer === optionChosen ? setScore(score + 1) : console.log(score);
setGameState('EndScreen');
}
return (
<div className="Quiz">
<h1>{Questions[currentQuestion].prompt}</h1>
<div className="options">
<button onClick={() => setOptionChosen('optionA')}>{Questions[currentQuestion].optionA}</button>
<button onClick={() => setOptionChosen('optionB')}>{Questions[currentQuestion].optionB}</button>
<button onClick={() => setOptionChosen('optionC')}>{Questions[currentQuestion].optionC}</button>
<button onClick={() => setOptionChosen('optionD')}>{Questions[currentQuestion].optionD}</button>
</div>
{currentQuestion === Questions.length -1 ? <button onClick={finishQuiz}>Finish Quiz</button> : <button onClick={nextQuestion}>Next Question</button>}
</div>
)
}
export default Quiz
I learned this method from a Tutorial from PedroTech on YouTube. I followed along to create this. I wanted to make sure I didn't take credit for his work.

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);
...
};

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