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>
);
}
Related
I'm trying to map over data from API, but while writing the code to display the data I got this error: TypeError: weatherData.map is not a function
I tried removing useEffect from the code and tried to add curly brackets: const [weatherData, setWeatherData] = useState([{}])
Update: Line 14 log undefined : console.log(weatherData.response)
import axios from 'axios'
import { useEffect, useState } from 'react'
import './App.css'
function App() {
const [search, setSearch] = useState("london")
const [weatherData, setWeatherData] = useState([])
const getWeatherData = async () => {
try {
const weatherData = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${search}&appid={APIKEY}`);
console.log(weatherData.response);
if (weatherData) {
setWeatherData(weatherData);
}
} catch (err) {
console.error(err);
}
}
useEffect(() => {
getWeatherData()
}, [getWeatherData])
const handleChange = (e) => {
setSearch(e.target.value)
}
return (
<div className="App">
<div className='inputContainer'>
<input className='searchInput' type="text" onChange={handleChange} />
</div>
{weatherData.map((weather) => {
return (
<div>
<h1>{weather.name}, {weather.country}</h1>
</div>
)
})}
</div>
)
}
export default App
You're having errors in fetching the data as well as rendering it.
Just change the entire App component like this :
import { useEffect, useState } from "react";
import axios from "axios";
function App() {
const [search, setSearch] = useState("London");
const [weatherData, setWeatherData] = useState([]);
const APIKEY = "pass your api key here";
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`https://api.openweathermap.org/data/2.5/weather?q=${search}&appid=${APIKEY}`
);
setWeatherData(result.data);
};
fetchData();
}, [search]);
const handleChange = (e) => {
setSearch(e.target.value);
};
return (
<div className="App">
<div className="inputContainer">
<input className="searchInput" type="text" onChange={handleChange} />
</div>
<h1>
{" "}
{weatherData.name} ,{" "}
{weatherData.sys ? <span>{weatherData.sys.country}</span> : ""}{" "}
</h1>
</div>
);
}
export default App;
this should be working fine just make sure to change : const APIKEY = "pass your api key "; to const APIKEY = "<your API key> ";
this is a demo in codesandbox
Create a promise function:
const getWeatherData = async () => {
try {
const weatherData = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${search}&appid={APIKEY}`);
console.log(weatherData.response);
if (weatherData.response.data) {
setWeatherData(weatherData.response.data);
}
} catch (err) {
console.error(err);
}
}
Then call it.
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>
);
}
well, when I want to update an item I call UseEffect and make an asynchronous call to my endpoint, but I want to solve the problem when the id doesn't exist in the db, it throws me the following error: 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.
export const AddOrUpdateItem = () => {
const {id} = useParams();
const [itemToUpdate, setItemToUpdate] = useState(null);
const history = useHistory();
useEffect(() => {
if(id) {
const fetchData = async() => {
try {
const resp = await axios.get(`${ITEMS_ENDPOINT}/${id}`);
setItemToUpdate(resp.data);
} catch (error) {
console.log(error);
history.push('/articulos'); //I think here is the problem
}
};
fetchData();
}
}, [id]);
return (
<Box mt={5}>
<Paper elevation={7}>
<Card className="card-root" variant="outlined">
<CardContent>
<h2>{id !== undefined ? 'Actualizar artÃculo' : 'Registrar artÃculo'}</h2>
<hr/>
<ItemForm
id={id}
item={itemToUpdate}
/>
</CardContent>
</Card>
</Paper>
</Box>
)
}
The minimal fix might be as follows:
useEffect(() => {
const source = axios.CancelToken.source()
if(id) {
const fetchData = async() => {
try {
const resp = await axios.get(`${ITEMS_ENDPOINT}/${id}`, {cancelToken: source.token});
setItemToUpdate(resp.data);
} catch (error) {
console.log(error);
history.push('/articulos');
}
};
fetchData();
}
return ()=> source.cancel() // <<<<<<<<<<<<<<
}, [id]);
Using a custom hook (Live demo):
import React from "react";
import { useState } from "react";
import {
useAsyncEffect,
CanceledError,
E_REASON_UNMOUNTED
} from "use-async-effect2";
import cpAxios from "cp-axios";
function TestComponent(props) {
const [text, setText] = useState("");
const cancel = useAsyncEffect(
function* () {
setText("fetching...");
try {
const json = (yield cpAxios(props.url).timeout(props.timeout)).data;
setText(`Success: ${JSON.stringify(json)}`);
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED);
setText(err.toString());
}
},
[props.url]
);
return (
<div className="component">
<div>{text}</div>
<button className="btn btn-warning" onClick={cancel}>
Cancel request
</button>
</div>
);
}
Demo with internal state usage (Live demo):
import React from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpAxios from "cp-axios";
function TestComponent(props) {
const [cancel, done, result, err] = useAsyncEffect(
function* () {
return (yield cpAxios(props.url)).data;
},
{ states: true, deps: [props.url] }
);
return (
<div className="component">
<div>
{done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
</div>
<button className="btn btn-warning" onClick={cancel} disabled={done}>
Cancel async effect
</button>
</div>
);
}
import { useState, useCallback, useRef, useEffect } from 'react';
export const useHttpClient = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState();
const activeHttpRequests = useRef([]);
const sendRequest = useCallback(
async (url, method = 'GET', body = null, headers = {}) => {
setIsLoading(true);
const httpAbortCtrl = new AbortController();
activeHttpRequests.current.push(httpAbortCtrl);
try {
const response = await fetch(url, {
method,
body,
headers,
signal: httpAbortCtrl.signal
});
const responseData = await response.json();
activeHttpRequests.current = activeHttpRequests.current.filter(
reqCtrl => reqCtrl !== httpAbortCtrl
);
if (!response.ok) {
throw new Error(responseData.message);
}
setIsLoading(false);
return responseData;
} catch (err) {
setError(err.message);
setIsLoading(false);
throw err;
}
}, []);
const clearError = () => {
setError(null);
};
useEffect(() => {
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
activeHttpRequests.current.forEach(abortCtrl => abortCtrl.abort());
};
}, []);
return { isLoading, error, sendRequest, clearError };
};
Usage:
const fetchUsers = async () => {
try {
const responseData = await sendRequest(
'http://localhost:5000/api/users?page=1'
);
setLoadedUsers(responseData);
} catch (err) {}
};
Currently this is code that i got, i want to make it simplier so i dont need to write on every fetch the cleaning up functions. In this file it used controller and theres also passed abourt on end on useeffect but times when i start to switching pages really fast and dont even give server to load content it consoling me log for unmounted error.. Is there anyone that can help me about this? Maybe theres something wrong in this code or something?
The abort controller rejects only the fetch promise, it doesn't affect any others. Moreover, you're trying to change the state in the catch block without any check whether the component was unmounted or not. You should do these checks manually. The weird array of abort controllers is unnecessary, you can use one controller for all requests.
This code is ugly, but it just illustrates the approach... (Live demo)
export default function TestComponent(props) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState();
const [text, setText] = useState("");
const controllerRef = useRef(null);
const isMountedRef = useRef(true);
const sendRequest = useCallback(
async (url, method = "GET", body = null, headers = {}) => {
setIsLoading(true);
const httpAbortCtrl =
controllerRef.current ||
(controllerRef.current = new AbortController());
try {
const response = await fetch(url, {
method,
body,
headers,
signal: httpAbortCtrl.signal
});
//if (!isMountedRef.current) return; // nice to have this here
const responseData = await response.json();
if (!response.ok) {
throw new Error(responseData.message);
}
if (!isMountedRef.current) return;
setIsLoading(false);
return responseData;
} catch (err) {
if (isMountedRef.current) {
setError(err.message);
setIsLoading(false);
}
throw err;
}
},
[]
);
useEffect(() => {
return () => {
isMountedRef.current = false;
controllerRef.current && controllerRef.current.abort();
};
}, []);
const fetchUsers = async () => {
try {
const responseData = await sendRequest(props.url);
isMountedRef.current && setText(JSON.stringify(responseData));
} catch (err) {}
};
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{error ? error.toString() : text}</div>
<button onClick={fetchUsers}>Send request</button>
</div>
);
}
You can use a custom library for fetching, which automatically cancels async code (and aborts the request) when the related component unmounting. You can use it directly in your components. So behold the magic :) The simplest demo ever :
import React, { useState } from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpAxios from "cp-axios";
export default function TestComponent(props) {
const [text, setText] = useState("");
const cancel = useAsyncEffect(
function* () {
const response = yield cpAxios(props.url);
setText(JSON.stringify(response.data));
},
[props.url]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button onClick={cancel}>Cancel request</button>
</div>
);
}
Or a more practical example with fetching error handling (Live Demo):
import React, { useState } from "react";
import { useAsyncCallback, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CanceledError } from "c-promise2";
import cpAxios from "cp-axios";
export default function TestComponent(props) {
const [text, setText] = useState("");
const [isFetching, setIsFetching] = useState(false);
const fetchUrl = useAsyncCallback(
function* (options) {
try {
setIsFetching(true);
setText("fetching...");
const response = yield cpAxios(options).timeout(props.timeout);
setText(JSON.stringify(response.data));
setIsFetching(false);
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED);
setText(err.toString());
setIsFetching(false);
}
},
[props.url]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button onClick={() => fetchUrl(props.url)} disabled={isFetching}>
Fetch data
</button>
<button onClick={() => fetchUrl.cancel()} disabled={!isFetching}>
Cancel request
</button>
</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>
);
}