I am trying to fetch data from API - it usually works but sometimes I get
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.
My useEffect
useEffect(() => {
let abortController = new AbortController();
getAccountInformation(abortController);
return () => {
abortController.abort();
};
}, [isFocused]);
The function
async function getAccountInformation(abortController) {
try {
const token = await AsyncStorage.getItem("token");
const client_id = await AsyncStorage.getItem("client_id");
await fetch(API_ADD + "/getClientInformation/" + client_id, {
signal: abortController.signal,
method: "GET",
headers: {
Authorization: "Bearer " + token,
"Content-Type": "application/json",
},
})
.then((response) => response.text())
.then((responseJson) => {
const safeResponse = responseJson.length
? JSON.parse(responseJson)
: {};
setClientInformation(safeResponse);
getBookingsByClientId(abortController);
}).catch(err=>{
if(err.name==='AbortError')
{
console.log("Fetch abort - caught an error");
}
else
{
console.log(err.message);
Alert.alert(err.message);
}
})
.done();
} catch (e) {
console.log(e);
}
}
Unfortunately I couldn't find any solution for this - only solutions for a function inside useEffect
AbortController can reject only the first 'fetch' promise. Just check signal.aborted before changing the state.
async function getAccountInformation(abortController) {
try {
const token = await AsyncStorage.getItem("token");
const client_id = await AsyncStorage.getItem("client_id");
const {signal}= abortController;
await fetch(API_ADD + "/getClientInformation/" + client_id, {
signal,
method: "GET",
headers: {
Authorization: "Bearer " + token,
"Content-Type": "application/json",
},
})
.then((response) => response.text())
.then((responseJson) => {
const safeResponse = responseJson.length
? JSON.parse(responseJson)
: {};
if(!signal.aborted){ // do not change the state if controller has been aborted
setClientInformation(safeResponse);
getBookingsByClientId(abortController);
}
}).catch(err=>{
if(err.name==='AbortError')
{
console.log("Fetch abort - caught an error");
}
else
{
console.log(err.message);
Alert.alert(err.message);
}
})
.done();
} catch (e) {
console.log(e);
}
}
Ideally, you should interrupt the function as soon as possible after unmounting the component to avoid doing unnecessary work. But in a minimal solution, you just need to avoid state changes.
Or you can check out this demo with custom hooks usage, that takes care of cancellation&request aborting on unmounting (or if requested by user) automatically:
import React, { useState } from "react";
import {
useAsyncCallback,
CanceledError,
E_REASON_UNMOUNTED
} from "use-async-effect2";
import cpFetch from "cp-fetch";
export default function TestComponent(props) {
const [text, setText] = useState("");
const fetchCharacter = useAsyncCallback(
function* (id) {
const response = yield cpFetch(
`https://rickandmortyapi.com/api/character/${id}`
);
return yield response.json();
}
);
const fetchUrl = useAsyncCallback(
function* () {
this.timeout(props.timeout);
try {
setText("fetching...");
const response = yield cpFetch(props.url);
const json = yield response.json();
const character = yield fetchCharacter(Math.round(Math.random() * 100));
setText(
JSON.stringify(
{
firstResponse: json,
character
},
null,
2
)
);
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED);
setText(err.toString());
}
},
[props.url, props.timeout]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button className="btn btn-success" onClick={() => fetchUrl(props.url)}>
Fetch data
</button>
<button className="btn btn-warning" onClick={() => fetchUrl.cancel()}>
Cancel request
</button>
</div>
);
}
Related
I am creating a react app with full crud functionality. It allows users to create job postings and i wanted to click on a specific job to view more details.
I am having trouble as everytime i try to click a "job" it says that ID is undefined specifically:
show function called with id: undefined
SyntaxError: Unexpected end of JSON input
My app currently displays the list of all jobs and creates.
I already confirmed the following:
Made sure the backend server is running and listening on port 3001.
Verified that the endpoint i am trying to fetch actually exists. Tried on postman
Made sure that my frontend code is using the correct URL to make requests to the backend.
I am using hooks and functions.
DetailsPage.js
import styles from './DetailsPage.module.css';
import React, { useState, useEffect } from 'react';
import jobsService from '../../utils/jobsService';
export default function DetailPage(props) {
const [job, setJob] = useState({});
const [isLoading, setIsLoading] = useState(true);
const { id } = props.match?.params || {};
useEffect(() => {
const fetchData = async () => {
try {
const { data } = await jobsService.show(id);
setJob(data);
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [id]);
return (
<>
{isLoading ? (
<div>Loading...</div>
) : (
<div className={styles.list}>
<div className={styles.Grid}>
<h3>{job.title}</h3>
<p>{job.description}</p>
</div>
</div>
)}
</>
);
}
jobsService.js
async function getAll(){
const response = await fetch('http://localhost:3001/api/jobs')
const data = await response.json()
return data
}
async function create(item) {
try {
const response = await fetch('http://localhost:3001/api/jobs/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(item)
});
return await response.json();
} catch (error) {
console.error(error);
}
}
async function show(id) {
console.log("show function called with id:", id);
return fetch(`http://localhost:3001/api/jobs/${id}`, {
method: 'GET',
headers: {
'Content-type': 'application/json',
},
})
.then(res => res.json());
}
export default {
getAll,
create,
show,
}
destruct id form empty object ?
instead this
const { id } = props.match?.params || {};
test this
const { id } = props.match?.params || {id: 0};
or use ternary operator in function calling
or
async function show(id = 0) {
console.log("show function called with id:", id);
return fetch(`http://localhost:3001/api/jobs/${id}`, {
method: 'GET',
headers: {
'Content-type': 'application/json',
},
})
.then(res => res.json());
}
Im writing an application using react and django rest. I am trying to update a post and then redirect back to the home screen, but sometimes the redirect happens before the put request.
As there is a Get request on the home page, that then gets called first and i do not see the updated values unless i refresh the page? Any suggestions?
Here is the page with the put request (updateNote())
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { ReactComponent as ArrowLeft } from "../assets/arrow-left.svg";
const NotePage = ({ match, history }) => {
let noteId = match.params.id;
let [note, setNote] = useState(null);
useEffect(() => {
getNote();
}, [noteId]);
let getNote = async () => {
let response = await fetch(`/api/get-note/${noteId}/`);
let data = await response.json();
setNote(data);
};
let updateNote = async () => {
fetch(`/api/get-note/${noteId}/update/`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(note),
});
};
let deleteNote = async () => {
fetch(`/api/get-note/${noteId}/delete/`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
history.push("/");
};
let handleSubmit = () => {
updateNote().then(history.push("/"));
};
let handleChange = (value) => {
setNote((note) => ({ ...note, body: value }));
console.log("Handle Change:", note);
};
return (
<div className="note">
<div className="note-header">
<h3>
<ArrowLeft onClick={handleSubmit} />
</h3>
<button onClick={deleteNote}>Delete</button>
</div>
<textarea
onChange={(e) => {
handleChange(e.target.value);
}}
value={note?.body}
></textarea>
</div>
);
};
export default NotePage;
Then here is the page it redirects to
import React, { useState, useEffect } from "react";
import ListItem from "../components/ListItem";
const NotesListPage = () => {
let [notes, setNotes] = useState([]);
useEffect(() => {
getNotes();
}, []);
let getNotes = async () => {
let response = await fetch("/api/get-notes/");
let data = await response.json();
setNotes(data);
};
return (
<div className="notes">
<div className="notes-header">
<h2 className="notes-title">☶ Notes</h2>
<p className="notes-count">{notes.length}</p>
</div>
<div className="notes-list">
{notes.map((note, index) => (
<ListItem key={index} note={note} />
))}
</div>
</div>
);
};
export default NotesListPage;
I want to make sure that history.push("/") doesnt get executed unitll the fetch request has returned a response
I suggest using the promise method and using '.then' or await just like that :
let updateNote = async () => {
let temp =await fetch(`/api/get-note/${noteId}/update/`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(note),
});
if(temp)
history.push("/")
};
If you want to navigate after the fetch request has resolved then the code needs to wait for them to settle. Don't forget to catch and/or handle any errors and rejected Promises appropriately.
Example:
const updateNote = async () => {
// return Promise to chain from
return fetch(`/api/get-note/${noteId}/update/`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(note),
});
};
const deleteNote = async () => {
try {
// wait for Promise to resolve
await fetch(`/api/get-note/${noteId}/delete/`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
history.push("/");
} catch(error) {
// log error, etc...
}
};
const handleSubmit = () => {
// pass a callback in .then
updateNote()
.then(() => history.push("/"))
.catch(error => {
// log error, etc...
});
};
I am new to react and I am building a page component that returns data from an API call and passes the values to my return statement. My page continues to return as blank because the page loads before the variables are returned from the API. I am wondering, how can I wait to render the page until my API has returned a response? The two variables are initialized as so and are not updated until the API response
var userData
const [customer_email, setEmail] = useState();
const [newuserid, setUserId] = useState();
useEffect(() => {
userData = Cookies.get("user-data");
if (userData) {
console.log("Userdata !== null");
try {
userData = JSON.parse(userData);
} catch (e) {
console.log(e);
}
setEmail(userData.email);
setUserId(userData.userID);
}
}, []);
function getCustomer() {
const options = {
method: "GET",
headers: {
Accept: "application/json",
"x-guid": "......",
"x-api-key": ".....",
},
};
if (customer_email != "" && customer_email != undefined) {
try {
console.log("email inside fetch =", customer_email);
fetch(
`https://exampleapi/customers?customer_email=${customer_email}&customer_id=${newuserid}`,
options
)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.log(err));
} catch (e) {
console.log(e);
}
}
}
if (customer_email) {
console.log("get customer");
getCustomer();
}
The component return statement:
return (
<>
{customer_email && (
<section>
<div
id="identification"
data-authenticated="true"
data-email={customer_email}
data-id={newuserid}
style={{ display: "none" }}
></div>
</section>
<div>
........{irrelevant html here}
</div>
)}
);
Note---- This is not a class, it is a function component
You might want to do something like this where you have a loading state that you set to true when the response from the API has been resolved, which will re-render the component.
Note: This code will not work if you copy and paste. It's just a representation of the pattern you should use.
var userData
const [customer_email, setEmail] = useState();
const [newuserid, setUserId] = useState();
cost [hasLoaded, setHasLoaded] = useState(false);
function getCustomer() {
const options = {
method: "GET",
headers: {
Accept: "application/json",
"x-guid": "......",
"x-api-key": ".....",
},
};
if (customer_email != "" && customer_email != undefined) {
try {
console.log("email inside fetch =", customer_email);
fetch(
`https://exampleapi/customers?customer_email=${customer_email}&customer_id=${newuserid}`,
options
)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.log(err));
} catch (e) {
console.log(e);
}
}
}
useEffect(() => {
userData = Cookies.get("user-data");
if (userData) {
console.log("Userdata !== null");
try {
userData = JSON.parse(userData);
} catch (e) {
console.log(e);
}
setEmail(userData.email);
setUserId(userData.userID);
}
}, []);
useEffect(async () => {
if (!customer_email) return;
console.log("getting customer")
const customerData await getCustomer()
if (customerData) setHasLoaded(true)
}, [])
return(
<>
{hasLoaded && <div>.....</div>}
</>
)
I had this working with react app but I am porting the project to next.js and it doesn't work for some reason. I am using SWR
Custom hook:
import axios from "axios";
import useSwr from "swr";
export const useHttp = (url, token) => {
const fetcher = (url, token) =>
axios
.get(url, { headers: { Authorization: "Bearer " + token } })
.then((res) => res.data);
const { data, error } = useSwr(token ? [url, token] : null, fetcher);
return {
data: data,
isLoading: !error && !data,
isError: error,
};
};
In my page
const { data, isError, isLoading } = useHttp(
`http://localhost:8000/api/admin/bookings/${id}`,
session.user.servertoken
);
const deleteBooking = async (id) => {
//immediatly remove record from the screen before getting response from the server
mutate(
[
`http://localhost:8000/api/admin/bookings/${id}`,
session.user.servertoken,
],
data.filter((d) => d._id !== id),
false
);
try {
const response = await axios.delete(
`${process.env.NEXT_PUBLIC_API_ENDPOINT}/admin/bookings/${id}`,
{ headers: { Authorization: "Bearer " + session.user.servertoken } }
);
mutate([
`http://localhost:8000/api/admin/bookings/${id}`,
session.user.servertoken,
]);
} catch (err) {
console.log(err);
}
};
The button:
<Button
variant="danger"
onClick={() => deleteBooking(d._id)}
>
Delete
</Button>
Absolutely nothing happens in terms of the ui though (record IS deleted in database). The record should disappear from the screen once I click on delete and the deleteBooking function runs.
*If I add this to my deleteBooking function it shows the correct data:
const filteredData = data.filter((d) => d._id !== id);
I'm trying to get user details after user looged in but user is getting 401 error even user is looged in with 200 ok.
Explanation of process:
i have logged in user using fetch post request.
stored username,role,staffid to async storage
now i want to list all user (with /api/staff endpoint response throws user firstname and last name )with fetch get request but whenever i make GET request it
throws 401 error.
It will be lifesaver to crack this step for me,thank you!
here is my code
import AsyncStorage from "#react-native-community/async-storage";
import React, { useState, useEffect } from "react";
import { SafeAreaView, Text, StyleSheet, Alert } from "react-native";
import AuthService from "../api/auth-service";
import BASE_URL from "../api/baseUrl";
export default function HomeScreen(props) {
const [firstName, setFirstName] = useState({});
const [lastName, setLastName] = useState({});
const [userValue, setUserValue] = useState({});
useEffect(() => {
let mounted = true;
if (mounted) {
getDataFromStorage();
getUserInfo();
}
return () => {
mounted = false;
};
}, []);
const getDataFromStorage = async () => {
let user = await AsyncStorage.getItem("LoggedInUser");
setUserValue(JSON.parse(user));
};
const getUserInfo=async()=>{
return fetch(BASE_URL+"/api/staff")
.then((response) => {
if(response.ok){
console.log(response);
}else{
console.log(response.status);
}
})
.catch((error) => {
console.log(error);
this.setState({ errorMsg: "Error retreiving data" });
});
}
return (
<SafeAreaView>
<Text>
{"Good morning " + userValue.username + " "}
{"you role is " + userValue.role +"your staff id is " + userValue.staffId+" " + "your first name is "+ firstName +"this is your last name"+lastName}
</Text>
</SafeAreaView>
);
}
authservice.js
import AsyncStorage from "#react-native-community/async-storage";
import BASE_URL from "./baseUrl";
class AuthService {
login(Username, Password, role) {
console.log(Username, role);
return fetch(BASE_URL + "/api/authentication/login", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
Username,
Password,
}),
}).then((res) => {
if (res.ok) {
console.log("the login response", res);
return res.json();
} else {
alert("Invalid Username or Password");
window.stop();
}
});
}
logout() {
AsyncStorage.getAllKeys().then((keys) => AsyncStorage.multiRemove(keys));
}
}
export default new AuthService();
login.js
const submitData = async () => {
AuthService.login(Username, Password).then(
(data) => {
console.log(JSON.stringify(data));
AsyncStorage.setItem("LoggedInUser", JSON.stringify(data));
if (data.role == "Admin") {
console.log(data.username);
navigation.navigate("adminPage");
} else {
navigation.navigate("staffpage");
}
},
(error) => {
Alert.alert(error);
}
);
};
According to developer.mozilla.org
The HTTP 401 Unauthorized client error status response code indicates that the request has not been applied because it lacks valid authentication credentials for the target resource.
It's seems the user doesn't have right to access the API. Make sure that the getUserInfo() API, /api/staff, don't need any authentication token in header of your HTTP request.
I have a feeling that you may need to resolve one more promise in AuthService.login.
res.json() is actually a promise which needs to be resolved as well, so you may need one more then block like so:
return fetch(BASE_URL + "/api/authentication/login", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
Username,
Password,
}),
}).then((res) => {
if (res.ok) {
console.log("the login response", res);
return res.json();
} else {
alert("Invalid Username or Password");
window.stop();
}
}).then(finalData=>finalData )// <---------- add this
.catch(err=> err)
It happens because getDataFromStorage is an async function so you have to resolve it first. Also in login.js we have to await before navigate to make sure that the data is saved in AsyncStorage. Please update the following part of your code:
login.js
const submitData = async () => {
AuthService.login(Username, Password).then(
async (data) => {
console.log(JSON.stringify(data));
await AsyncStorage.setItem("LoggedInUser", JSON.stringify(data));
if (data.role == "Admin") {
console.log(data.username);
navigation.navigate("adminPage");
} else {
navigation.navigate("staffpage");
}
},
(error) => {
Alert.alert(error);
}
);
};
next, initialize userValue with null
const [userValue, setUserValue] = useState(null);
Now have 2 useEffect,
useEffect(() => {
let mounted = true; //Why this required as it doesn't mean anything
if (!userValue) {
getDataFromStorage();
}
return () => {
mounted = false;
};
}, []);
useEffect(()=>{
if(userValue){
getUserInfo()
}
},[userValue])