I am working on displaying a "message" on the component based on the server response, and i wanted that message to disappear after 5 second. I tried my best with setTimeout but no luck, can you help me?
Here is my code:
import React, { useState } from "react";
import { Form, Button, Container, Row, Col} from 'react-bootstrap'
import axios from 'axios'
export default function Users() {
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [message, setMessage] = useState("")
function handleSubmit(e){
e.preventDefault()
const credential = { email, name };
axios
.post('/', credential)
.then(response => {
if(response.status === 201) {
resetInputs()
setMessage(response.data.message)
}
})
.catch(error => {
if (error.response.status === 409) {
setMessage(error.response.data.message)
}
})
}
function resetInputs(){
setEmail("")
setName("")
}
return (
<div className="form">
<div className="hero-container">
<h1>Welcome to <span className="hi">my</span><span>website</span></h1>
<h5>Enter your name and your email to join our waiting list!</h5>
<p></p>
<div>
{message}
</div>
<p></p>
</div>
)
}
You call setTimeout after setting the message, telling it to fire after five seconds, and then clear the message:
function handleSubmit(e){
e.preventDefault()
const credential = { email, name };
axios
.post('/', credential)
.then(response => {
if(response.status === 201) {
resetInputs()
setMessage(response.data.message)
}
})
.catch(error => {
if (error.response.status === 409) {
setMessage(error.response.data.message)
}
})
.finally(() => { // ***
setTimeout(() => { // ***
setMessage(""); // *** If you want to clear the error message as well
}, 5000); // *** as the normal message
}); // ***
}
or
function handleSubmit(e){
e.preventDefault()
const credential = { email, name };
axios
.post('/', credential)
.then(response => {
if(response.status === 201) {
resetInputs()
setMessage(response.data.message)
setTimeout(() => { // *** If you only want to automatically clear
setMessage(""); // *** this message and not an error message
}, 5000); // ***
}
})
.catch(error => {
if (error.response.status === 409) {
setMessage(error.response.data.message)
}
});
}
You can add setTimout to your axios call, or you can reset it independently like this:
import { useEffect } from "react";
...
useEffect(() => {
let isUnmounted = false;
if (message !== "") {
setTimeout(() => {
if (!isUnmounted ) {
setMessage("");
}
}, 5000);
}
return () => { isUnmounted = true; };
}, [message])
isUnmounted prevents using setMessage() in an unmounted component, it is possible for a user to close the component before time is reached.
Something like this may work (untested):
const useTimedState = (initialState, duration) => {
const [state, setState] = setState(initialState);
useEffect(() => {
if (typeof state === 'undefined') {
return;
}
const timer = setTimeout(() => {
setState();
}, duration);
return () => clearTimeout(timer);
}, [state]);
return [state, setState];
}
export default function Users() {
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [message, setMessage] = useTimedState(undefined, 5000);
function handleSubmit(e){
e.preventDefault()
const credential = { email, name };
axios
.post('/', credential)
.then(response => {
if(response.status === 201) {
resetInputs()
setMessage(response.data.message)
}
})
.catch(error => {
if (error.response.status === 409) {
setMessage(error.response.data.message)
}
})
}
}
Related
When I load my Nextjs page, I get this error message: "Error: Rendered more hooks than during the previous render."
If I add that if (!router.isReady) return null after the useEffect code, the page does not have access to the solutionId on the initial load, causing an error for the useDocument hook, which requires the solutionId to fetch the document from the database.
Therefore, this thread does not address my issue.
Anyone, please help me with this issue!
My code:
const SolutionEditForm = () => {
const [formData, setFormData] = useState(INITIAL_STATE)
const router = useRouter()
const { solutionId } = router.query
if (!router.isReady) return null
const { document } = useDocument("solutions", solutionId)
const { updateDocument, response } = useFirestore("solutions")
useEffect(() => {
if (document) {
setFormData(document)
}
}, [document])
return (
<div>
// JSX code
</div>
)
}
useDocument hook:
export const useDocument = (c, id) => {
const [document, setDocument] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const ref = doc(db, c, id)
const unsubscribe = onSnapshot(
ref,
(snapshot) => {
setIsLoading(false)
if (snapshot.data()) {
setDocument({ ...snapshot.data(), id: snapshot.id })
setError(null)
} else {
setError("No such document exists")
}
},
(err) => {
console.log(err.message)
setIsLoading(false)
setError("failed to get document")
}
)
return () => unsubscribe()
}, [c, id])
return { document, isLoading, error }
}
You cannot call a hook, useEffect, your custom useDocument, or any other after a condition. The condition in your case is this early return if (!router.isReady) returns null. As you can read on Rules of Hooks:
Donโt call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns...
Just remove that if (!router.isReady) returns null from SolutionEditForm and change useDocument as below.
export const useDocument = (c, id) => {
const [document, setDocument] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!id) return; // if there is no id, do nothing ๐๐ฝ
const ref = doc(db, c, id);
const unsubscribe = onSnapshot(
ref,
(snapshot) => {
setIsLoading(false);
if (snapshot.data()) {
setDocument({ ...snapshot.data(), id: snapshot.id });
setError(null);
} else {
setError("No such document exists");
}
},
(err) => {
console.log(err.message);
setIsLoading(false);
setError("failed to get document");
}
);
return () => unsubscribe();
}, [c, id]);
return { document, isLoading, error };
};
The if (!router.isReady) return null statement caused the function to end early, and subsequent hooks are not executed.
You need to restructure your hooks such that none of them are conditional:
const [formData, setFormData] = useState(INITIAL_STATE)
const router = useRouter()
const { solutionId } = router.query
const { document } = useDocument("solutions", solutionId, router.isReady) // pass a flag to disable until ready
const { updateDocument, response } = useFirestore("solutions")
useEffect(() => {
if (document) {
setFormData(document)
}
}, [document])
// Move this to after the hooks.
if (!router.isReady) return null
and then to make useDocument avoid sending extra calls:
export const useDocument = (c, id, enabled) => {
and updated the effect with a check:
useEffect(() => {
if (!enabled) return;
const ref = doc(db, c, id)
const unsubscribe = onSnapshot(
ref,
(snapshot) => {
setIsLoading(false)
if (snapshot.data()) {
setDocument({ ...snapshot.data(), id: snapshot.id })
setError(null)
} else {
setError("No such document exists")
}
},
(err) => {
console.log(err.message)
setIsLoading(false)
setError("failed to get document")
}
)
return () => unsubscribe()
}, [c, id, enabled])
UseEffect cannot be called conditionally
UseEffect is called only on the client side.
If you make minimal representation, possible to try fix this error
I am trying to save the user data when he loged in like this.
const handleLogin = () => {
firebase
.auth()
.signInWithEmailAndPassword(Email, passWord)
.then((res) => {
firebase.auth().onAuthStateChanged((userData) => {
setuserData(userData);
const jsonValue = JSON.stringify(userData);
AsyncStorage.setItem("userData", jsonValue);
console.log(userData);
});
})
.then(() => navigation.navigate("HomeScreen"))
.catch((error) => console.log(error));
};
and in the Spalch I am trying to check if the userData is in local storage or not .the problem is that it goes directly to HomeScreen even if there is No Data in Local storage
any help please
const SplashScreen = ({ navigation }) => {
const [animating, setAnimating] = useState(true);
useEffect(() => {
setTimeout(() => {
setAnimating(true);
navigation.replace(AsyncStorage.getItem("userData") ? "HomeScreen" : "Log_In");
}, 500);
},
[]);
AsyncStorage.getItem returns promise so either you need to write it with async/await or in promise
useEffect(() => {
setTimeout(async() => {
setAnimating(true);
const user = await AsyncStorage.getItem("userData")
navigation.replace(user ? "HomeScreen" : "Log_In");
}, 500);
},
[]);
Here is my solution for this, a bit long but you can try it out
const SplashScreen = ({ navigation }) => {
const [animating, setAnimating] = useState();
const [redirect, setRedirect] = useState('');
const getUserData = useCallback(async () => {
const response = await AsyncStorage.getItem('userData');
setRedirect(response ? 'HomeScreen' : 'Log_In');
},[]);
useEffect(() => {
setTimeout(() => {
getUserData();
}, 500);
}, []);
useEffect(() => {
if (redirect) {
setAnimating(true);
navigation.replace(redirect);
}
}, [redirect]);
};
I have implemented the following code to fetch data and render a component if everything goes well along with checking loading, error states.
import { useEffect, useState } from "react";
function Posts() {
const [posts, setPosts] = useState([]);
const [loader, setLoader] = useState(false);
const [error, setError] = useState({ status: false, message: "" });
const fetchPosts = () => {
setLoader(true);
setTimeout(async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts"
);
// throw new Error("Some error occured");
const data = await response.json();
if (data.error) {
setError({ status: true, message: data.error });
} else {
setPosts(data);
}
setLoader(false);
} catch (error) {
console.log("error", error);
setError({ status: true, message: error.message });
setLoader(false);
}
}, 2000);
};
useEffect(() => {
fetchPosts();
}, []);
if (loader) return <h3>Loading...</h3>;
if (error.status) return <h3>Error: {error.message}</h3>;
return (
<div>
<h1>Posts</h1>
{posts.length === 0 && <h3>There are no posts</h3>}
{posts.length > 0 && (
<div>
{posts.map((post) => (
<Post post={post} key={post.id} />
))}
</div>
)}
</div>
);
}
export default Posts;
Is this the right way to handle loading, error and success states when fetching data? or is there a better and more elegant solution than repeating this for every component?
Instead of checking for data.error in the try block, you could check for response.ok; if it is true, call response.json(), otherwise throw an error.
Also move the setLoader call to the finally block to avoid the duplicate calls in try and catch blocks.
try {
const response = await fetch(...);
if (response.ok) {
let data = await response.json();
setPosts(data);
} else {
throw new Error(/* error message */);
}
} catch (error) {
console.log("error", error);
setError({ status: true, message: error.message });
} finally {
setLoader(false);
}
If you want to check for data.error property in a response, you can change the following if condition
if (response.ok) {
to
if (response.ok && !data.error) {
is there a better and more elegant solution than repeating this for
every component?
Make a custom hook to make the fetch request and use that in every component that needs to fetch data from the backend.
const useFetch = (apiUrl, initialValue) => {
const [data, setData] = useState(initialValue);
const [loader, setLoader] = useState(false);
const [error, setError] = useState({ status: false, message: "" });
useEffect(() => {
async function fetchData = (url) => {
setLoader(true);
try {
const response = await fetch(url);
if (response.ok) {
let responseData = await response.json();
setData(responseData);
} else {
throw new Error(/* error message */);
}
} catch (error) {
console.log("error", error);
setError({ status: true, message: error.message });
} finally {
setLoader(false);
}
}
fetchData(apiUrl);
}, [apiUrl]);
return [data, error, loader];
};
Your solution should be good enough to do, but, to me, I would prefer not to set timeout for getting data, and I will use .then and .catch for better readable and look cleaner to me
import { useEffect, useState } from "react";
function Posts() {
const [posts, setPosts] = useState([]);
const [loader, setLoader] = useState(false);
const [error, setError] = useState({ status: false, message: "" });
const fetchPosts = () => {
fetch("https://jsonplaceholder.typicode.com/posts")
.then(response => response.json())
.then(data => {
setPosts(data);
setLoader(false);
})
.catch(error =>{
console.log("error", error);
setError({ status: true, message: error.message });
setLoader(false);
});
};
useEffect(() => {
setLoader(true);
fetchPosts();
}, []);
if (loader) return <h3>Loading...</h3>;
if (error.status) return <h3>Error: {error.message}</h3>;
return (
<div>
<h1>Posts</h1>
{posts.length === 0 && <h3>There are no posts</h3>}
{posts.length > 0 && (
<div>
{posts.map((post) => (
<Post post={post} key={post.id} />
))}
</div>
)}
</div>
);
}
export default Posts;
I have api file with requests
import * as axios from "axios";
export const productAPI = {
getProducts() {
return axios({
method: 'get',
url: `/api/products`
});
}
};
which reaches to transport.js and sends request(i think that part is not important).
Method above is called from my component like this
useEffect(()=> {
setLoading(true);
productAPI.getProducts()
.then((response) => {
if(response.status === 200) {
history.push(`${pathWithLocation}${PAGES.newLoan}`);
}
})
.catch((error) => {
if (error.response.data.error.message) {
dispatch(addModal({
type: 'basic',
size: 'middle',
title: 'some title',
text: error.response.data.error.message,
buttons: [{ buttonLabel: 'ะะ', onClick: ()=> dispatch(removeModal()) }]
}))
}
})
.finally(() => {
setLoading(false);
});
},[])
I want to cancel this specific request when component is unmounted. (switched route for example)
You can just use a isCurrent flag. (I have to admit that I have not considered what the benefit of using the axios.cancelToken mechanism would be here. Maybe it would make it cleaner, maybe it would just make it more convoluted.)
useEffect(() => {
const isCurrent = true;
setLoading(true);
productAPI.getProducts()
.then((response) => {
if(isCurrent && response.status === 200) {
history.push(`${pathWithLocation}${PAGES.newLoan}`);
}
})
.catch((error) => {
if (isCurrent && error.response.data.error.message) {
dispatch(addModal({/*...*/}))
}
})
.finally(() => {
if (isCurrent) setLoading(false);
});
return () => { isCurrent = false };
}, [])
getProducts(cancelToken) {
return axios({
method: 'get',
url: `/api/products`,
cancelToken
});
}
useEffect(()=> {
const source= CancelToken.source();
const isMounted= true;
setLoading(true);
productAPI.getProducts(source.token)
.then((response) => {
if(response.status === 200) {
history.push(`${pathWithLocation}${PAGES.newLoan}`);
}
})
.catch((error) => {
if (error.response.data.error.message) {
dispatch(addModal({
...
}))
}
})
.finally(() => {
isMounted && setLoading(false);
});
return ()=>{
isMounted= false;
source.cancel();
}
},[])
Or a bit magic way (Codesandbox demo):
import React, { useState } from "react";
import {
useAsyncEffect,
CanceledError,
E_REASON_UNMOUNTED
} from "use-async-effect2";
import cpAxios from "cp-axios";
export default function TestComponent(props) {
const [text, setText] = useState("");
const [loading, setLoading] = useState(true);
const cancel = useAsyncEffect(
function* () {
try {
const response = yield cpAxios(props.url);
setText(JSON.stringify(response.data));
setLoading(false);
if (response.status === 200) {
//history.push(`${pathWithLocation}${PAGES.newLoan}`);
}
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED);
setLoading(false);
setText(err.toString());
//dispatch(addModal({})
}
},
[props.url]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button onClick={cancel} disabled={!loading}>
Cancel request
</button>
</div>
);
}
Basically i've created one custom component for api calling
import React, {useState, useEffect} from 'react';
import axios from 'axios';
export const useFetch = config => {
const [Response, setResponse] = useState({});
const [Error, setError] = useState({});
const [ShowModal, setShowModal] = useState(false);
const [ShowLoader, setShowLoader] = useState(false);
useEffect(() => {
callAPI();
}, []);
const callAPI = () => {
setShowLoader(true);
axios(config)
.then(res => {
console.log('==>>', res);
if (res.status == 200) {
setShowLoader(false);
setResponse(res.data);
}
})
.catch(err => {
console.log('==>>', err.response);
setError(err.response.data);
setShowLoader(false);
setShowModalErrorMessage(err.response.data.error);
setShowModal(true);
});
};
return {Response, Error, ShowModal, ShowLoader};
};
with the help on this i can call api and get response if i use it with useEffect/componentDidMount in component. But how to use same for calling different api on Button click. is it possible?
i followed this=> post
Add setUrl method (can expand to setConfig) in useFetch.
Here working demo for this in stackblitz
import React, {useState, useEffect} from 'react';
import axios from 'axios';
const useFetch = ({}) => {
const [Response, setResponse] = useState({});
const [Error, setError] = useState({});
const [ShowModal, setShowModal] = useState(false);
const [ShowLoader, setShowLoader] = useState(false);
const [url, setUrl] = useState("");
useEffect(() => {
if (url) {
console.log('making request ', url);
callAPI();
}
}, [url]);
const callAPI = () => {
setShowLoader(true);
axios(url)
.then(res => {
console.log('==>>', res);
if (res.status == 200) {
setShowLoader(false);
setResponse(res.data);
}
})
.catch(err => {
console.log('==>>', err.response);
setError(err.response.data);
setShowLoader(false);
setShowModalErrorMessage(err.response.data.error);
setShowModal(true);
});
};
return {Response, Error, ShowModal, ShowLoader, setUrl};
};
export default useFetch;
On the button click, set url (expand to config)
import React, {useState, useEffect} from 'react';
import useFetch from './use-fetch';
export default ({ name }) => {
const {Response, Error, ShowModal, ShowLoader, setUrl } = useFetch({});
return (<div>
<button key={'1'} onClick={() => setUrl("http://foo/items")}> Request 1 </button>
<button key={'2'} onClick={() => setUrl("http://foo/other")}> Request 2 </button>
</div>)
};
Common Request.js file using the fetch method
Request("POST","http://localhost/users/user",{'name':"",'add':""})
export default function Request(reqMethod, endPointUrl, bodyData) {
if (reqMethod === "POST") {
return fetch(endPointUrl, {
method: reqMethod,
body: JSON.stringify(bodyData),
})
.then((response) => {
return response.json();
})
.catch(() => {
localStorage.clear();
});
} else if (reqMethod === "GET") {
return fetch(endPointUrl, {
method: reqMethod,
})
.then((response) => {
return response.json();
}).catch(() => {
localStorage.clear();
});
}
}