I'm trying to implement debounce in a small/test React application.
It's just an application that fetch data from an API and it has a text field for an auto complete.
import React, { useEffect, useState, useMemo } from 'react';
import axios from 'axios';
const API = 'https://jsonplaceholder.typicode.com/posts';
const AutoComplete2 = () => {
const [ text, setText ] = useState("")
const [ posts, setPosts ] = useState([])
useEffect(() => {
async function fetchData() {
const data = await axios.get(API);
if(parseInt(data.status) !== 200) return;
setPosts(data.data)
}
fetchData();
}, [])
const handleTextChange = (event) => setText(event.target.value);
const handleSelectOption = (str) => setText(str);
const showOptions = useMemo(() => {
if(text === '') return;
const showPosts = [...posts].filter((ele) => ele.title.toLowerCase().includes(text.toLowerCase()));
if(showPosts.length === 1) {
setText(showPosts[0].title);
} else {
return (
<div>
{showPosts.map((obj, index) => {
return (
<div key={index} >
<span onClick={() => handleSelectOption(obj.title)} style={{cursor: 'pointer'}}>
{obj.title}
</span>
</div>
)
})}
</div>
)
}
}, [text, posts])
// addding debounce
const debounce = (fn, delay) => {
let timer;
return function() {
let context = this;
let args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args)
}, delay);
}
}
const newHandleTextChange = ((val) => debounce(handleTextChange(val), 5000));
return (
<div>
<input type="text" value={text} onChange={newHandleTextChange} />
{showOptions}
</div>
)
}
export default AutoComplete2;
The application works, but not the debounce. I add a 5 seconds wait to clearly see if it is working, but every time I change the input text, it calls the function without the delay. Does anyone know why it is happening?
Thanks
A more idiomatic approach to debouncing in React is to use a useEffect hook and store the debounced text as a different stateful variable. You can then run your filter on whatever that variable is.
import React, { useEffect, useState, useMemo } from "react";
import axios from "axios";
const API = "https://jsonplaceholder.typicode.com/posts";
const AutoComplete2 = () => {
const [text, setText] = useState("");
const [debouncedText, setDebouncedText] = useState("");
const [posts, setPosts] = useState([]);
useEffect(() => {
async function fetchData() {
const data = await axios.get(API);
if (parseInt(data.status) !== 200) return;
setPosts(data.data);
}
fetchData();
}, []);
// This will do the debouncing
// "text" will always be current
// "debouncedText" will be debounced
useEffect(() => {
const timeout = setTimeout(() => {
setDebouncedText(text);
}, 5000);
// Cleanup function clears timeout
return () => {
clearTimeout(timeout);
};
}, [text]);
const handleTextChange = (event) => setText(event.target.value);
const handleSelectOption = (str) => setText(str);
const showOptions = useMemo(() => {
if (debouncedText === "") return;
const showPosts = [...posts].filter((ele) =>
ele.title.toLowerCase().includes(debouncedText.toLowerCase())
);
if (showPosts.length === 1) {
setText(showPosts[0].title);
} else {
return (
<div>
{showPosts.map((obj, index) => {
return (
<div key={index}>
<span
onClick={() => handleSelectOption(obj.title)}
style={{ cursor: "pointer" }}
>
{obj.title}
</span>
</div>
);
})}
</div>
);
}
}, [debouncedText, posts]);
return (
<div>
<input type="text" value={text} onChange={handleTextChange} />
{showOptions}
</div>
);
};
export default AutoComplete2;
import { useEffect, useState, useRef } from "react";
import axios from "axios";
import { backend_base_url } from "../constants/external_api";
export default function DebounceControlledInput() {
const [search_category_text, setSearchCategoryText] = useState("");
let debounceSearch = useRef();
useEffect(() => {
const debounce = function (fn, interval) {
let timer;
return function (search_key) {
clearTimeout(timer);
timer = setTimeout(() => {
fn(search_key);
}, interval);
};
};
const getCategories = function (search_key) {
axios
.get(`${backend_base_url}categories/${search_key}`)
.then((response) => {
console.log("API Success");
})
.catch((error) => {});
};
debounceSearch.current = debounce(getCategories, 300);
//use for initial load
//debounceSearch.current('');
}, []);
const searchCategory = (search_key) => {
debounceSearch.current(search_key);
};
return (
<form
className="form-inline col-4"
onSubmit={(e) => {
e.preventDefault();
}}
autoComplete="off"
>
<input
type="text"
placeholder=""
id="search"
value={search_category_text}
onChange={(e) => {
searchCategory(e.target.value);
setSearchCategoryText(e.target.value);
e.preventDefault();
}}
/>
</form>
);
}
Related
I am getting an infinite loop / crash here.
I'm trying to get an onChange event fired for these radio buttons (which are built after pulling data from a query), but I think it keeps redrawing and I can't figure out why.
Any thoughts on how I can solve this?
const GetChallenge = async () => {
const slug = useParams()
const data = await shopifyApolloClient.query({ query: singleProduct(slug) })
return data
}
const Challenge = () => {
let [loaded, setLoaded] = useState(false)
let [product, setProduct] = useState([])
let [variants, setVariants] = useState([])
let [options, setOption] = useState()
let [metafields, setMetafields] = useState([])
GetChallenge().then((ret) => {
setProduct(ret.data.product)
setVariants(ret.data.product.variants.edges)
setOption(ret.data.product.variants.edges[0].node.title)
setMetafields(ret.data.product.metafields.edges)
setLoaded(true)
})
const handleOptions = (event) => {
setOption(event.target.value)
}
if (loaded === true) {
return (
<div>
{variants.map((e) => (
<label
htmlFor={e.node.title}
key={e.node.id}>
<input
type="radio"
name="options"
checked={e.node.title === options}
value={e.node.title}
onChange={handleOptions}
/>
{e.node.title}
</label>
))}
</div>
)
} else {
return (
<p>Not Loaded</p>
)
}
}
GetChallenge is triggering every render. Try useEffect with the empty array empty soas to trigger only onmount.
import React, { useState, useEffect } from 'react';
const GetChallenge = async () => {
...
useEffect(() => {
GetChallenge().then((ret) => {
setProduct(ret.data.product)
setVariants(ret.data.product.variants.edges)
setOption(ret.data.product.variants.edges[0].node.title)
setMetafields(ret.data.product.metafields.edges)
setLoaded(true)
}),[]}
...
}
Try this:
onChange={(event) => setOption(event.target.value)}
hope this will solve your problem :)
import React, { useState, useEffect } from 'react';
const GetChallenge = async () => {
const slug = useParams();
const data = await shopifyApolloClient.query({ query: singleProduct(slug) });
return data;
};
const Challenge = () => {
const [data, setData] = useState({
loaded: false,
product: [],
variants: [],
options: "",
metafields: []
});
const { loaded, variants, options } = data;
useEffect(() => {
GetChallenge().then((ret) => {
setData((prevState) => ({
...prevState,
product: ret.data.product,
variants: ret.data.product.variants.edges,
options: ret.data.product.variants.edges[0].node.title,
metafields: ret.data.product.metafields.edges,
loaded: true
}));
});
}, []);
const handleOptions = (event) => {
setData((prevState) => ({ ...prevState, options: event.target.value }));
};
if (loaded === true) {
return (
<div>
{variants.map((e) => (
<label htmlFor={e.node.title} key={e.node.id}>
<input
type="radio"
name="options"
checked={e.node.title === options}
value={e.node.title}
onChange={(event) => handleOptions(event)}
/>
{e.node.title}
</label>
))}
</div>
);
} else {
return <p>Not Loaded</p>;
}
};
I can't send message after subsequential clicks, if i click button at first time it is sending message to server, after that it is not sending messages to server.
import { useEffect, useState, useRef } from "react";
import Header from "../src/Components/Header";
import ChatHistory from "../src/Components/ChatHistory";
import ChatArea from "../src/Components/ChatArea";
function App() {
const [messages, setMessages] = useState([]);
const testValue = { messages, setMessages };
const socket = useRef(null);
const renderCount = useRef(0);
const sendMessage = (msg = "test") => {
if (socket.current) {
socket.current.send(msg);
}
addMessages(msg);
};
const addMessages = (msg) => {
setMessages((prev) => [...prev, msg]);
};
useEffect(() => {
socket.current = new WebSocket("ws://localhost:8001/ws");
socket.current.onmessage = (msg) => {
addMessages(msg);
};
}, []);
useEffect(() => {
return () => {
if (socket.current) {
socket.current.close();
}
};
}, [socket]);
console.log("i am rendering");
return (
<>
<Header />
<ChatHistory chatHistory={messages.current} />
<div>
<button onClick={sendMessage}>Send</button>
</div>
</>
);
}
export default App;
Above mentioned one is my code, While clicking send button at first time, it is triggering message to server, after another subsequential clicks it isn't triggering message to server. Help needed.
Your second useEffect actually closing the connection after the first render, it's unnecessary.
Moreover you don't really need to save your socket instance in a ref, usually you need a single instance:
const socket = new WebSocket("ws://localhost:8001/ws");
function App() {
const [messages, setMessages] = useState([]);
const addMessages = (msg) => {
setMessages((prev) => [...prev, msg]);
};
const sendMessage = (msg = "test") => {
socket.send(msg);
addMessages(msg);
};
// Setup
useEffect(() => {
socket.current.onmessage = addMessages;
}, []);
// Runs on App unmount, means on closing the application
useEffect(() => {
return () => {
socket.close();
};
}, []);
return (
<>
<Header />
<ChatHistory chatHistory={messages.current} />
<div>
<button onClick={sendMessage}>Send</button>
</div>
</>
);
}
useEffect(() => {
return () => {
if (socket.current) {
socket.current.close();
}
};
}, [socket]);
everytime the socket is changing, you close it
try to unmont in the same useEffect that is defining socket
useEffect(() => {
if (!socket.current) {
socket.current = new WebSocket("ws://localhost:8001/ws");
socket.current.onmessage = (msg) => {
addMessages(msg);
};
}
return () => {
if (socket.current) {
socket.current.close();
}
};
}, [socket]);
Note useRef is not optimal for that case, use useState instead
This is working, but i am not sure what i am did wrong.
import { useEffect, useRef, useState } from "react";
import Header from "./Components/Header";
import ChatHistory from "./Components/ChatHistory";
function App() {
const [chatHistory, setChatHistory] = useState([]);
const [isOnline, setIsOnline] = useState(false);
const [textValue, setTextValue] = useState("");
const webSocket = useRef(null);
webSocket.current = new WebSocket("ws://localhost:8001/ws");
useEffect(() => {
setTimeout(() => {
if (webSocket.current.readyState === WebSocket.OPEN) {
setIsOnline(true);
}
if (webSocket.current.readyState === WebSocket.CLOSED) {
setIsOnline(false);
setChatHistory([]);
}
}, 5);
}, [webSocket.current]);
const sendMessage = () => {
if (webSocket.current.readyState === WebSocket.OPEN) {
setChatHistory([...chatHistory, textValue]);
webSocket.current.send(textValue);
}
};
return (
<>
<div className="App">
<Header onLine={isOnline} />
<ChatHistory chatHistory={chatHistory} />
<input
type="text"
onChange={(e) => setTextValue(e.target.value)}
value={textValue}
placeholder="Type Message..."
/>
<button onClick={sendMessage}>Hit</button>
</div>
</>
);
}
export default App;
i am trying to mimic React useEffect race condition and handle that with AbortController. I can never hit the catch block ( i guess ) because the setTimeOut is called post the fetch request. My question is how can i rewrite this code to put fetch inside setTimeout and still be able to use AbortController to cancel the request?
import './App.css';
import {useState,useEffect} from 'react'
function App() {
const [country, setCountry] = useState('N.A.')
const [capital, setCapital] = useState('')
useEffect(() => {
const ctrl = new AbortController();
const load = async() =>{
try
{
//debugger
const response = await fetch(`https://restcountries.eu/rest/v2/capital/${capital}`,
{signal:ctrl.signal})
const jsonObj = await response.json()
setTimeout( ()=> {setCountry(jsonObj[0].name)} , Math.random()*10000)
}
catch(err)
{
console.log(err)
}
}
load();
return () =>{
ctrl.abort()
};
}, [capital])
return (
<div>
<button onClick={()=>setCapital("Berlin")} >Berlin</button>
<button onClick={()=>setCapital("Paris")} >Paris</button>
<button onClick={()=>setCapital("Madrid")} >Madrid</button>
<div>
{country}
</div>
</div>
);
}
export default App;
Hmm... just put that function inside setTimeout calling and don't forget to clean up the timer on unmount (Demo).
import React, { useState, useEffect } from "react";
export default function TestComponent(props) {
const [country, setCountry] = useState("N.A.");
const [capital, setCapital] = useState("");
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
const ctrl = new AbortController();
const timer = setTimeout(async () => {
try {
if (!capital) {
return;
}
const response = await fetch(
`https://restcountries.eu/rest/v2/capital/${capital}`,
{ signal: ctrl.signal }
);
// if (!isMounted) return; // can be omitted here
const jsonObj = await response.json();
isMounted && setCountry(jsonObj[0].name);
} catch (err) {
console.log(err);
isMounted && setError(err);
}
}, Math.random() * 10000);
return () => {
clearTimeout(timer);
isMounted = false;
ctrl.abort();
};
}, [capital]);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<button onClick={() => setCapital("Berlin")}>Berlin</button>
<button onClick={() => setCapital("Paris")}>Paris</button>
<button onClick={() => setCapital("Madrid")}>Madrid</button>
<div>Country: {error ? <b>{error.toString()}</b> : country}</div>
</div>
);
}
Or you can do the same with custom libs (Demo):
import React, { useState } from "react";
import { useAsyncEffect, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CPromise, CanceledError } from "c-promise2";
import cpFetch from "cp-fetch";
export default function TestComponent(props) {
const [country, setCountry] = useState("N.A.");
const [capital, setCapital] = useState("");
const [error, setError] = useState(null);
const cancel = useAsyncEffect(
function* () {
setError(null);
if (!capital) {
return;
}
yield CPromise.delay(Math.random() * 10000);
try {
const response = yield cpFetch(
`https://restcountries.eu/rest/v2/capital/${capital}`
).timeout(props.timeout);
const jsonObj = yield response.json();
setCountry(jsonObj[0].name);
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED);
console.log(err);
setError(err);
}
},
[capital]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<button onClick={() => setCapital("Berlin")}>Berlin</button>
<button onClick={() => setCapital("Paris")}>Paris</button>
<button onClick={() => setCapital("Madrid")}>Madrid</button>
<button onClick={cancel}>Cancel request</button>
<div>Country: {error ? <b>{error.toString()}</b> : country}</div>
</div>
);
}
In my application I have a initial state that start with some values.
In a process, I need to change this state for a empty array, (that is a return of my Api).
I am trying do this, but the value of the state don't change.
What can I do?
My code
import React, {useEffect, useState} from "react";
import "./style.scss";
import Services from "../../services/Services";
export default function Acoplamento({history, match}) {
const [veiculos, setVeiculos] = useState([]);
const [loading, setLoading] = useState(false);
const [type, setType] = useState(1);
const getAllVeiculos = async () => {
setLoading(true);
await Services.VeiculoServices.getAll().then(result => {
setVeiculos(result.data); // Here, i have a array with some objects
}).catch(error => {
error(error.message);
}).finally(() => setLoading(false));
return true;
}
const getOneVeiculos = async () => {
setLoading(true);
await Services.VeiculoServices.get().then(result => {
setVeiculos(result.data); // Here, my return is a empty array, but my state don't chage
}).catch(error => {
error(error.message);
}).finally(() => setLoading(false));
return true;
}
useEffect(() => {
if (type === 1) {
getAllVeiculos();
}
if (type === 2) {
getOneVeiculos();
}
}, [type]);
return (
<div>
<button onClick={() => setType(2)}>Click</button>
{veiculos.map(item => (
<div>{item.name}</div>
))}
</div>
);
}
I guess the problem arises because you did not convert the incoming data to json format. It can solve the following code problem.
import React, {useEffect, useState} from "react";
import "./style.scss";
import Services from "../../services/Services";
export default function Acoplamento({history, match}) {
const [veiculos, setVeiculos] = useState([]);
const [loading, setLoading] = useState(false);
const [type, setType] = useState(1);
const getAllVeiculos = async () => {
setLoading(true);
const response = await Services.VeiculoServices.getAll();
const result = await response.json();
if (result.data) setVeiculos(result.data);
setLoading(false);
}
const getOneVeiculos = async () => {
setLoading(true);
const response = await Services.VeiculoServices.get();
const result = await response.json();
if (result.data) setVeiculos(result.data);
setLoading(false);
}
useEffect(() => {
if (type === 1) {
getAllVeiculos();
}else if (type === 2) {
getOneVeiculos();
}
}, [type]);
return (
<div>
<button onClick={() => setType(2)}>Click</button>
{veiculos.map(item => (
<div>{item.name}</div>
))}
</div>
);
}
I'm trying to set a state, which I fetch from an API in the form of an array.
Tried this in every way possible, doesn't work.
Any ideas on how to fix this?
instance is an axios.create that creates the instance to a localhost django server which has CORS-ALLOW-CROSS-ORIGIN True
import React, { useState } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const fetchText = async () => {
const response = await instance.get(`/one/list/`);
setOne(response.data);
};
fetchText();
return (
<>
<div>Hello World.</div>
{one.forEach(o => (
<p>o.text</p>
))}
</>
);
};
export default OneList;
Do it like this,
import React, { useState } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const fetchText = async () => {
const response = await instance.get(`/one/list/`);
setOne(response.data);
};
useEffect(() => {
fetchText();
},[ any variable you want it to fetch that again ]);
return (
<>
<div>Hello World.</div>
{one.forEach(o => (
<p>o.text</p>
))}
</>
);
};
export default OneList;
This looks to be a good use case for a useEffect hook. Also, you need to await async functions within useEffect statement. The useEffect hook cannot be an async function in itself. Also, the original implementation would result in an infinite loop. The setState function would trigger a re-render which would then trigger the fetch function to fire off again. Consider the following:
import React, { useState, useEffect } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const fetchText = async () => {
const request= await instance.get(`/one/list/`);
request.then( r => setOne(r.data))
};
useEffect(() => {
(async () => {
await fetchText();
})();
}, []);
return (
<>
<div>Hello World.</div>
{one.forEach(o => (
<p>o.text</p>
))}
</>
);
};
export default OneList;
Based on you guys' advice, I came up with the below code which works fine so far.
import React, { useState, useEffect } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const [el, setEl] = useState(null);
const fetchText = async () => {
let res = await instance.get("/one/list/");
setOne(res.data);
};
useEffect(() => {
(async () => {
await fetchText();
})();
}, []);
useEffect(() => {
const handleClick = e => {
let newEl = document.createElement("input");
newEl.value = e.target.innerHTML;
newEl.id = e.target.id;
newEl.addEventListener("keypress", e => handleInput(e));
newEl.addEventListener("focusout", e => handleInput(e));
e.target.parentNode.replaceChild(newEl, e.target);
newEl.focus();
};
const handleInput = async e => {
console.log(e.type);
if (
e.target.value !== "" &&
(e.key === "Enter" || e.type === "focusout")
) {
let payload = { text: e.target.value };
try {
e.preventDefault();
let res = await instance.put(`/one/update/${e.target.id}`, payload);
let text = res.data.text;
e.target.value = text;
let newEl = document.createElement("span");
newEl.innerHTML = text;
newEl.addEventListener("click", e => handleClick(e));
newEl.id = e.target.id;
e.target.parentNode.replaceChild(newEl, e.target);
} catch (e) {
console.error(e);
}
}
};
setEl(
one.map(o => (
<p>
<span id={o.id} onClick={e => handleClick(e)}>
{o.text}
</span>
</p>
))
);
}, [one]);
return <>{el}</>;
};
export default OneList;