I have built this custom react hook :
import { useEffect, useContext, useState } from 'react';
import { ProductContext } from '../contexts';
import axios from 'axios';
export default function useProducts(searchQuery) {
const [products, setProducts] = useContext(ProductContext);
const [isLoading, setIsloading] = useState(true);
useEffect(() => {
axios
.get(`/shoes?q=${searchQuery ? searchQuery : ''}`)
.then((res) => {
setProducts(res.data);
setIsloading(false);
})
.catch((err) => {
console.error(err);
});
}, [searchQuery, setProducts]);
return { products, isLoading };
}
It basically fetches some data based on a query string that i pass in. The query string comes from an input field :
import React, { useState } from 'react';
import { FiSearch } from 'react-icons/fi';
import { useProducts } from '../../hooks';
export default function SearchBar() {
const [query, setQuery] = useState('');
const handleChange = (e) => {
e.preventDefault();
setQuery(e.target.value);
};
useProducts(query);
return (
<div className="search-form">
<FiSearch className="search-form__icon" />
<input
type="text"
className="search-form__input"
placeholder="Search for brands or shoes..."
onChange={handleChange}
/>
</div>
);
}
The problem is it will fetch while the user is typing. I want it to fetch after the user didnt type for 500 miliseconds.
What I tried is :
setTimeout(() => {
useProducts(query);
}, 500);
But this will return an error saying :
src\components\header\SearchBar.js
Line 14:5: React Hook "useProducts" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks
Search for the keywords to learn more about each error.
You can debounce your value with an additional piece of state. Once query is changed, we set off a 500 ms timer that will set the value of debounced. However, if the effect re-runs, we clear that timer and set a new timer.
import React, { useState, useEffect } from 'react';
import { FiSearch } from 'react-icons/fi';
import { useProducts } from '../../hooks';
export default function SearchBar() {
const [query, setQuery] = useState('');
const [debounced, setDebounced] = useState('');
useEffect(() => {
const timeout = setTimeout(() => {
setDebounced(query);
}, 500);
return () => { clearTimeout(timeout) }
}, [query])
const handleChange = (e) => {
e.preventDefault();
setQuery(e.target.value);
};
useProducts(debounced);
return (
<div className="search-form">
<FiSearch className="search-form__icon" />
<input
type="text"
className="search-form__input"
placeholder="Search for brands or shoes..."
onChange={handleChange}
/>
</div>
);
}
I'd change the useProducts hook to accept a debounce time as a parameter, and have it make the axios call only once the debounce time is up:
useProducts(query, 500);
export default function useProducts(searchQuery, debounceTime = 0) {
const [products, setProducts] = useContext(ProductContext);
const [isLoading, setIsloading] = useState(true);
const [timeoutId, setTimeoutId] = useState();
useEffect(() => {
clearTimeout(timeoutId);
setTimeoutId(setTimeout(() => {
axios
.get(`/shoes?q=${searchQuery ? searchQuery : ''}`)
.then((res) => {
setProducts(res.data);
setIsloading(false);
})
.catch((err) => {
console.error(err);
});
}, debounceTime));
}, [searchQuery, setProducts]);
return { products, isLoading };
}
Related
import './App.css';
import io from 'socket.io-client'
import { useEffect, useRef, useState } from 'react'
import React from 'react';
import ReactDOM from "react-dom/client";
const socket = io.connect("http://localhost:3001");
function App() {
const [message, setMessage] = useState("");
const [state, setState] = useState([]);
const [chat, setChat] = useState([]);
const socketRef = useRef();
const sendMessage = () => {
socket.emit("send_message", { message });
};
const renderChat = () => {
return (
chat.map(msg => {
console.log(msg.data)
return (
<h3>{msg.data["message"]}</h3>
)
})
)
}
useEffect(() => {
socketRef.current = io.connect("http://localhost:3001")
socketRef.current.on("receive_message", ({ message }) => {
setChat([ ...chat, { message } ])
})
return () => socketRef.current.disconnect()
},
[ chat ]
)
return (
<div className="App">
<input placeholder="Message..." onChange={(event) => {
setMessage(event.target.value);}}
/>
<button onClick={sendMessage}>Send Message</button>
<h1>Message:</h1>
{renderChat()}
</div>
);
}
export default App;
For some reason the useEffect that needs to store information doesn't work. I have tried a few solutions to store new values in an array useState but I always get this error:
When I do it like this:
useEffect(() => {
socket.on("receive_message", message => {
setChat([...chat, {message}]);
});
}, [socket])
it works but it doesn't save the information (it always has only 1 value which is the latest input text).
You can do it like in the second approach you mentioned, using the previous State:
useEffect(() => {
socket.on("receive_message", message => {
setChat(prevState => [...prevState, {message}]);
});
}, [socket])
You try
useEffect(() => {
socket.on("receive_message", ({ message }) => {
if(!!message){
setChat(prev => [ ...prev, { message } ])
}
})
return () => socket.disconnect()
},[ socket ])
I have the below code
import React, {useState, useEffect, useCallback} from 'react';
import axios from 'axios';
const Users = () => {
const [users, setUsers] = useState([]);
const [nextPageNumber, setNextPageNumber] = useState(1);
const fetchUsers = useCallback(() => {
axios.get(`https://randomuser.me/api?page=${nextPageNumber}`).then(response => {
const updatedUsers = [...users, ...response.data.results];
setUsers(updatedUsers);
setNextPageNumber(response.data.info.page + 1);
}).catch(error => {
console.log(error)
})
}, [nextPageNumber, users])
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
const fetchNextUser = () => {
fetchUsers();
}
if(users.length === 0){
return <div>No Users available</div>
}
return(
<div>
<button onClick={fetchNextUser}>Fetch Next user</button>
{users.map(user => (
<div key={user.id}>
<div>Name: {user.name.title} {user.name.first} {user.name.last}</div>
<div>
<img src={user.picture.large} alt="Not available"/>
</div>
</div>
))}
</div>
)
}
export default Users;
This is calling the api repeatadlly. I have used the fetchUsers dependency in useEffect and useCallback hook due to eslint errors. I just want to call the api on first mount and on click of Fetch Next user button without any eslint error.
Is there any way we can achieve that?
Have a try with the below changes it will not give you the eslint error messages.
import React, {useState, useEffect, useRef, useCallback} from 'react';
import axios from 'axios';
const Users = () => {
const [users, setUsers] = useState([]);
const nextPageRef = useRef(1)
const fetchUsers = useCallback(() => {
axios.get(`https://randomuser.me/api?page=${nextPageRef.current}`).then(response => {
const updatedUsers = [...response.data.results];
setUsers(prevUsers => [...prevUsers, ...updatedUsers]);
nextPageRef.current = response.data.info.page + 1
}).catch(error => {
console.log(error)
})
}, [])
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
You could simply use
useEffect(() => {
fetchUsers();
}, []);
This will call the fetchUsers function only once.
And when button is pressed, again call the fetchUsers() function
I am trying to make work search input. I'm filtering through fetched data in useEffect in Hooks/useCountries component, listening to input in App.js and passing props for handleChange in Searchbar component. Something is missing, I can't figure out what. Here is Hooks/useCountries component
import React, { useState, useEffect } from "react";
export default function useCountries(search) {
const [data, setData] = useState([]);
const [error, setError] = useState(null);
const fetchData = () => {
fetch("https://restcountries.eu/rest/v2/all")
.then((res) => res.json())
.then((result) => setData(result))
.catch((err) => console.log("error"));
};
useEffect(() => {
const searchResult =
data &&
data
.filter((item) => item.name.toLowerCase().includes(search))
.map((element) => <div>{element.name}</div>);
}, []);
useEffect(() => {
fetchData();
}, []);
return [data, error];
}
App.js
import React, { useState } from "react";
import SearchBar from "./components/SearchBar";
import useCountries from "./Hooks/useCountries";
import MainTable from "./components/MainTable";
import "./App.scss";
export default function App() {
const [search, setSearch] = useState("");
const [data, error] = useCountries(search);
const handleChange = (e) => {
setSearch(e.target.value);
};
return (
<div className="App">
<SearchBar handleChange={handleChange} search={search} />
<MainTable countries={data} />
</div>
);
}
SearchBar component
import React, { useState } from "react";
import "./SearchBar.scss";
export default function Searchbar({ handleChange, search }) {
return (
<div className="SearchBar">
<input
className="input"
type="text"
placeholder="search country ..."
value={search}
onChange={handleChange}
/>
</div>
);
}
The useEffect method you have which performs the filtering needs to be fired each time the search term changes - currently you are only using it once when the hook is created for the first time:
useEffect(() => {
const searchResult =
data &&
data
.filter((item) => item.name.toLowerCase().includes(search))
.map((element) => <div>{element.name}</div>);
}, [search]);
Note how the search variable is now part of the useEffect dependency array.
I am trying to make work search input. I'm filtering through fetched data in useEffect in Hooks/useCountries component, listening to input in App.js and passing props for handleChange in Searchbar component. Something is missing, I can't figure out what. Here is the link of codesandbox and Hooks/useCountries component
import React, { useState, useEffect } from "react";
export default function useCountries(search) {
const [data, setData] = useState([]);
const [error, setError] = useState(null);
const fetchData = () => {
fetch("https://restcountries.eu/rest/v2/all")
.then((res) => res.json())
.then((result) => setData(result))
.catch((err) => console.log("error"));
};
useEffect(() => {
const searchResult =
data &&
data
.filter((item) => item.name.toLowerCase().includes(search))
.map((element) => <div>{element.name}</div>);
}, []);
useEffect(() => {
fetchData();
}, []);
return [data, error];
}
App.js
import React, { useState } from "react";
import SearchBar from "./components/SearchBar";
import useCountries from "./Hooks/useCountries";
import MainTable from "./components/MainTable";
import "./App.scss";
export default function App() {
const [search, setSearch] = useState("");
const [data, error] = useCountries(search);
const handleChange = (e) => {
setSearch(e.target.value);
};
return (
<div className="App">
<SearchBar handleChange={handleChange} search={search} />
<MainTable countries={data} />
</div>
);
}
SearchBar component
import React, { useState } from "react";
import "./SearchBar.scss";
export default function Searchbar({ handleChange, search }) {
return (
<div className="SearchBar">
<input
className="input"
type="text"
placeholder="search country ..."
value={search}
onChange={handleChange}
/>
</div>
);
}
So in your useCountries hook, you need to update the useEffect to trigger whenever search is changed. Otherwise, it runs when the hook is first loaded, but then never again. I'm also not exactly sure what your logic is attempting to accomplish in your current useEffect. I've posted a possible update to it that also changes your search to regex to account for the possibility that the user may not be typing in lower case. Let me know if this doesn't work for your use case and I can adapt it.
import React, { useState, useEffect } from "react";
export default function useCountries(search) {
const [data, setData] = useState([]);
const [error, setError] = useState(null);
const [searchResults, setSearchResults] = useState(null);
const fetchData = () => {
fetch("https://restcountries.eu/rest/v2/all")
.then((res) => res.json())
.then((result) => setData(result))
.catch((err) => console.log("error"));
};
useEffect(() => {
if (search) {
const searchCriteria = new RegExp(search, "i");
setSearchResults(
data
.filter((item) => searchCriteria.test(item.name))
.map((element) => <div>{element.name}</div>)
);
} else {
setSearchResults(null);
}
}, [search]);
useEffect(() => {
fetchData();
}, []);
return [data, error, searchResults];
}
And in App.js add:
const [data, error, searchResults] = useCountries(search);
Here is the fork off of your sandbox where this works:
CodeSandbox
I currently have a component that does a history.push('/') after a few seconds. But I am getting warnings
index.js:1375 Warning: Cannot update during an existing state transition (such as withinrender). Render methods should be a pure function of props and state.
and also
index.js:1375 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
I am fairly new to React, do I need do some sort of clean up?
Here is my component.
import React, {useState} from 'react'
import {UsePostOrPutFetch} from "../hooks/postHook";
import "./styles/ConfirmationChange.scss";
import moment from 'moment';
export default function ConfirmatonChange(props) {
const [shouldFetch, setShouldFetch] = useState(false);
const [data,loading,isError, errorMessage] = UsePostOrPutFetch("/send-email/", props.data.location.state.value,"POST", shouldFetch, setShouldFetch);
const [countDown, setCountDown] = useState(5)
let spinner = (
<strong className="c-spinner" role="progressbar">
Loading…
</strong>
);
const changeView = () =>
{
if (countDown < 0) {
props.data.history.push('/')
} else {
setTimeout(() => {
setCountDown(countDown - 1)
}
, 1000)
}
}
return (
<div>
<div className="o-container">
<article className="c-tile">
<div className="c-tile__content">
<div className="c-tile__body u-padding-all">
<button className = "c-btn c-btn--primary u-margin-right" onClick={props.data.history.goBack}>Edit</button>
<button className = "c-btn c-btn--secondary u-margin-right" disabled={loading} onClick={(e) => { setShouldFetch(true)}}>Send</button>
{!loading && data === 200 && !isError ? (
<div className="ConfirmationChange-success-send">
<hr hidden={!data === 200} className="c-divider" />
Email Sent succesfully
<p>You will be redirected shortly....{countDown}</p>
{changeView()}
</div>
) : (loading && !isError ? spinner :
<div className="ConfirmationChange-error-send">
<hr hidden={!isError} className="c-divider" />
{errorMessage}
</div>
)}
</div>
</div>
</article>
</div>
</div>
)
}
And here is what my data fetch component look like
import { useState, useEffect } from "react";
import { adalApiFetch } from "../config/adal-config";
const UsePostOrPutFetch = (url, sendData, methodType, shouldFetch, setShouldSend) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [errorMessage, setError] = useState("");
useEffect(() => {
const ac = new AbortController();
if (shouldFetch) {
const postOrPutData = async () => {
try {
const response = await adalApiFetch(fetch, url,
{
signal: ac.signal,
method: methodType,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(sendData)
});
const json = await response.json();
setData(await json);
setLoading(true);
} catch (err) {
setIsError(true);
setError(err.message);
} finally {
setShouldSend(false)
setLoading(false);
}
};
postOrPutData();
}
return () => { ac.abort(); };
}, [shouldFetch, sendData, url, methodType, setShouldSend]);
return [data, loading, isError, errorMessage];
};
export {UsePostOrPutFetch}
Any help would be greatly appreciated.
Check React Hooks - Check If A Component Is Mounted
The most common cause of this warning is when a user kicks off an asynchronous request, but leaves the page before it completes.
You'll need a componentIsMounted variable and useEffect and useRef hooks :
const componentIsMounted = useRef(true);
useEffect(() => {
return () => {
componentIsMounted.current = false;
};
}, []);
const changeView = () => {
if (countDown < 0) {
props.data.history.push("/");
} else {
setTimeout(() => {
if (componentIsMounted.current) { // only update the state if the component is mounted
setCountDown(countDown - 1);
}
}, 1000);
}
};
You should do the same for data fetch component
Yup, you have a timeOut that could potentially fire after the component has unmounted.
You need to add a useEffect that clears the timer onUnmount as follows
const timerRef = useRef();
useEffect(() => () => clearTimeout(timerRef.current), [])
const changeView = () => {
if (countDown < 0) {
props.data.history.push("/");
} else {
timerRef.current = setTimeout(() => {
setCountDown(countDown - 1);
}, 1000);
}
};