How to manage component states (loading, error, data) in React - javascript

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;

Related

React custom hook does not give result of POST

The custome hook post method working fine at the same time the response adding state taking time.
console.log(jsonResult)
shows the response of POST method at the same time responseData shows null
usePostQuery
import { useCallback, useState } from "react";
interface bodyData {
message: string,
author: string
}
const usePostQuery = (url: string, data?: bodyData )=> {
const [responseData, setResponseData] = useState();
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState('');
const callPost = useCallback( async (data: bodyData) => {
try {
setLoading(true);
const response = await fetch(url, {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: data.message,
userId: 15
})
});
const jsonResult = await response.json();
console.log('--------jsonResult---------');
console.log(jsonResult)
setResponseData(jsonResult);
} catch (error: any) {
setError(error.message);
} finally {
setLoading(false);
}
},
[url]
);
return { responseData, loading, error, callPost };
};
export default usePostQuery;
const { responseData, loading, error, callPost } = usePostQuery('https://jsonplaceholder.typicode.com/posts')
The responseData is not giving post call response
useEffect(() => {
if (draftMessage && myMessage) {
// submitNewMessage()
console.log("post my message to server");
callPost({
message: myMessage,
author: "Mo"
});
if (loading === false) {
setMyMessage("");
setdraftMessage(false);
console.log("after ", responseData);
}
console.log("responseData ", responseData);
}
}, [draftMessage, myMessage]);
The fetch is successful because the console in side fetch response shows the API response.
There's nothing wrong with your custom hook. The issue is in your effect hook.
It only triggers when its dependencies change, ie draftMessage and myMessage. It does not re-evaluate loading or responseData so will only ever see their states at the time it is triggered.
It's really unclear what you're using the draftMessage state for. Instead, I would simply trigger the callPost in your submit handler...
export default function App() {
const [myMessage, setMyMessage] = useState("");
const { responseData, loading, callPost } = usePostQuery(
"https://jsonplaceholder.typicode.com/posts"
);
const handleMyMessage = (val) => {
setMyMessage(val);
};
const handleSubmit = async (event) => {
event.preventDefault();
await callPost({ message: myMessage, author: "Mo" });
setMyMessage("");
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{loading ? (
<p>Loading...</p>
) : (
<ChatForm
onChange={handleMyMessage}
myMessage={myMessage}
handleSubmit={handleSubmit}
/>
)}
<pre>responseData = {JSON.stringify(responseData, null, 2)}</pre>
</div>
);
}
Your hook controls the loading and responseData states so there's really very little for your components to do.

Database updates only on the first click of the button with this functions, what is wrong and how could i fix it?

I am new to react and MongoDB, I am trying to add months to a date in my database in mongo, but it only updates the first time I click on the <Price> button, I need it to update every time I click it. The user has to log out and log back in for it to work again, but still only 1 update can be made to the database. Can someone explain to me why this is happening, and how could it be fixed?
This is the function
import React, { useContext } from "react";
import { useState } from "react";
import useFetch from "../../hooks/useFetch";
import Footer from "../../components/Footer";
import Navbar from "../../components/Navbar";
import Sidebar from "../../components/Sidebar";
import {
ContractContainer,
HeadingContainer,
TypeH1,
ActiveUntil,
MonthlyWrapper,
MonthlyContainer,
MonthNumber,
Price,
Navbarback,
} from "./userinfoElements";
import { AuthContext } from "../../context/AuthContext";
import moment from "moment";
import axios from "axios";
const Userinfo = () => {
// for nav bars
const [isOpen, setIsOpen] = useState(false);
// set state to true if false
const toggle = () => {
setIsOpen(!isOpen);
};
const { user } = useContext(AuthContext);
let { data, loading, reFetch } = useFetch(`/contracts/${user.contractType}`);
let dateFormat = moment(user.activeUntil).format("DD/MMMM/yyyy");
const updateDate = async () => {
try {
let newDate = moment(user.activeUntil).add(1, "months");
dateFormat = newDate.format("DD/MMMM/yyyy");
axios.put(`/activedate/${user.namekey}`, {
activeUntil: newDate,
});
} catch (err) {
console.log(err);
}
reFetch();
};
return (
<>
<Sidebar isOpen={isOpen} toggle={toggle} />
{/* navbar for smaller screens*/}
<Navbar toggle={toggle} />
<Navbarback /> {/* filling for transparent bacground navbar*/}
{loading ? (
"Loading components, please wait"
) : (
<>
<ContractContainer>
<HeadingContainer>
<TypeH1>{data.contractType}</TypeH1>
<ActiveUntil>Subscription active until {dateFormat}</ActiveUntil>
</HeadingContainer>
<MonthlyWrapper>
<MonthlyContainer>
<MonthNumber>1 Month</MonthNumber>
<Price onClick={updateDate}>{data.month1Price}$</Price>
</MonthlyContainer>
<MonthlyContainer>
<MonthNumber>3 Month</MonthNumber>
<Price onClick={updateDate}>{data.month3Price}$</Price>
</MonthlyContainer>
<MonthlyContainer>
<MonthNumber>6Month</MonthNumber>
<Price onClick={updateDate}>{data.month6Price}$</Price>
</MonthlyContainer>
<MonthlyContainer>
<MonthNumber>12Month</MonthNumber>
<Price onClick={updateDate}>{data.month12Price}$</Price>
</MonthlyContainer>
</MonthlyWrapper>
</ContractContainer>
</>
)}
<Footer />
</>
);
};
export default Userinfo;
this is the fetch hook
import { useEffect, useState } from "react";
import axios from "axios";
const useFetch = (url) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const res = await axios.get(url);
setData(res.data);
} catch (err) {
setError(err);
}
setLoading(false);
};
fetchData();
}, [url]);
const reFetch = async () => {
setLoading(true);
try {
const res = await axios.get(url);
setData(res.data);
} catch (err) {
setError(err);
}
setLoading(false);
};
return { data, loading, error, reFetch };
};
export default useFetch;
Any help is appreciated!
EDIT: added AuthContext file and server sided controllers if needed
import React from "react";
import { createContext, useEffect, useReducer } from "react";
const INITIAL_STATE = {
user: JSON.parse(localStorage.getItem("user")) || null,
loading: false,
error: null,
};
export const AuthContext = createContext(INITIAL_STATE);
const AuthReducer = (state, action) => {
switch (action.type) {
case "LOGIN_START":
return {
user: null,
loading: true,
error: null,
};
case "LOGIN_SUCCESS":
return {
user: action.payload,
loading: false,
error: null,
};
case "LOGIN_FAILURE":
return {
user: null,
loading: false,
error: action.payload,
};
case "LOGOUT":
return {
user: null,
loading: false,
error: null,
};
default:
return state;
}
};
export const AuthContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(AuthReducer, INITIAL_STATE);
useEffect(() => {
localStorage.setItem("user", JSON.stringify(state.user));
}, [state.user]);
return (
<AuthContext.Provider
value={{
user: state.user,
loading: state.loading,
error: state.error,
dispatch,
}}
>
{children}
</AuthContext.Provider>
);
};
api Controller to update active date
import User from "../models/User.js";
export const updateActiveDate = async (req, res, next) => {
try {
await User.updateOne({ $set: { activeUntil: req.body.activeUntil } });
res.status(200).json("Active date has been updated.");
} catch (err) {
next(err);
}
};
api Controller to find contracts
import Contracts from "../models/Contracts.js";
export const getContract = async (req, res, next) => {
try {
const Contract = await Contracts.findOne({
contractType: req.params.contractType,
});
res.status(200).json(Contract);
} catch (err) {
next(err);
}
};
api Controller for login authentication
export const login = async (req, res, next) => {
try {
const user = await User.findOne({ namekey: req.body.namekey });
if (!user) return next(createError(404, "User not found!"));
if (req.body.password === undefined) {
return next(createError(500, "Wrong password or namekey!"));
}
const isPasswordCorrect = await bcrypt.compare(
req.body.password,
user.password
);
if (!isPasswordCorrect)
return next(createError(400, "Wrong password or namekey!"));
const token = jwt.sign({ id: user._id }, process.env.JWT);
const { password, ...otherDetails } = user._doc;
res
.cookie("access_token", token, {
httpOnly: true,
})
.status(200)
.json({ details: { ...otherDetails } });
} catch (err) {
next(err);
}
};
You should update the stored user state to reflect the activeUntil date change.
Define a 'UPDATE_USER_DATE' action in your reducer to update the user instance:
case "UPDATE_USER_DATE":
const updatedUser = { ...state.user };
updatedUser.activeUntil = action.payload;
return {
...state,
user: updatedUser
};
Then, after updating the date in updateDate, update the user state as well:
const { user, dispatch } = useContext(AuthContext);
const updateDate = async () => {
try {
let newDate = moment(user.activeUntil).add(1, "months");
dateFormat = newDate.format("DD/MMMM/yyyy");
await axios.put(`/activedate/${user.namekey}`, {
activeUntil: newDate,
});
dispatch({ type: "UPDATE_USER_DATE", payload: newDate });
} catch (err) {
console.log(err);
}
reFetch();
};
Give this a try. It awaits the put request, and only once that has responded it calls reFetch. Without the await you're calling the reFetch before the put request has had a chance to complete its work.
const updateDate = async () => {
try {
let newDate = moment(user.activeUntil).add(1, "months");
dateFormat = newDate.format("DD/MMMM/yyyy");
await axios.put(`/activedate/${user.namekey}`, {
activeUntil: newDate,
});
} catch (err) {
console.log(err);
} finally {
reFetch();
}
};

throw error message is not getting caught by the catch method

I'm following a react tutorial about handling fetch errors on YouTube. I did exactly what the instructor did but for some reason the catch method is not catching the throw error message. Here's the code:
const Home = () => {
const [blogs, setBlogs] = useState(null);
const [isPending, setIsPending] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setTimeout(() => {
fetch("http://localhost:8000/blogs")
.then((res) => {
if (!res.ok) {
throw Error("This error is not getting caught");
}
return res.json();
})
.then((data) => {
setBlogs(data);
setIsPending(false);
setError(null);
})
.catch((err) => {
setIsPending(false);
setError(err.message);
});
}, 1000);
}, []);
return (
<div className="home">
{error && <div>{error} </div>}
{isPending && <div>Loading...</div>}
{blogs && <BlogList blogs={blogs} title="All Blogs!" />}
</div>
);
};
export default Home;
Note: the server is not running.
The first .then from fetch will be entered into when the response headers are received. The response headers may indicate that there's a problem - if the response isn't .ok - in which case your throw Error will be entered into as desired and send control flow down to the lower .catch.
But if no response headers are received at all, the first .then will not be entered into - instead, a network error will cause a rejection and the .catch below will be entered into directly.
Your code results in Failed to fetch being displayed, since that's the error message from a failed request that doesn't even get any headers back:
const { useState, useEffect } = React;
const Home = () => {
const [blogs, setBlogs] = useState(null);
const [isPending, setIsPending] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setTimeout(() => {
fetch("http://localhost:8000/blogs")
.then((res) => {
if (!res.ok) {
throw Error("This error is not getting caught");
}
return res.json();
})
.then((data) => {
setBlogs(data);
setIsPending(false);
setError(null);
})
.catch((err) => {
setIsPending(false);
setError(err.message);
});
}, 1000);
}, []);
return (
<div className="home">
{error && <div>{error} </div>}
{isPending && <div>Loading...</div>}
{blogs && <BlogList blogs={blogs} title="All Blogs!" />}
</div>
);
};
ReactDOM.render(<Home />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='react'></div>
If you wanted to have This error is not getting caught displayed in this sort of situation too, change
setError(err.message);
to
setError('This error is not getting caught');

React custom hook is not updating from first click

I've created a custom hook to fetch data with events handlers, when I using it on click event the hook makes the request on the second click
useFetch.js
import { useState, useEffect } from 'react';
import { makeRequest } from '../utils';
const useFetch = (query = {}) => {
const [request, setRequest] = useState({ ...query });
const [data, setData] = useState({
response: null,
isError: null,
isLoading: request.isLoading,
});
const fetchData = async () => {
if (!request.url) {
return;
}
try {
const res = await makeRequest(
request.url,
request.method || 'get',
request.body || null,
);
setData({
response: res,
isLoading: false,
error: null,
});
} catch (error) {
setData({
response: null,
error,
isLoading: false,
});
}
};
const onEvent = (req) => {
if (req) {
setRequest({ ...req });
}
};
useEffect(() => fetchData(), [request]);
return { ...data, onEvent };
};
export default useFetch;
Component File
const { isLoading, isError, response, onEvent } = useFetch();
const ClickMe = () => {
onEvent({
url: 'v1/profile/login',
method: 'post',
body: {
username: 'eee#ddd.com',
password: '2342332',
},
});
console.log('response', response);
};
return (
<>
<button onClick={() => ClickMe()} type="button">
Click Me
</button>
)
the log inside the ClickMe function is null in the first click but in the second click it returns the value
Because fetchData is asynchronous function you cannot know when resposne will be set, that's why you cannot access it like normal sync code
in your app code you could observe response change to console it like
useEffect(() => { console.log(response) }, [ response ]);
At the time of console.log, the response is not fetched. Since when ever response changes, the component re-renders, you can try like below to see the updated values of isLoading and response.
return (
<>
{isLoading && <div> Loading... </div>}
{`Resonse is ${JSON.stringify(response)}`}
<button onClick={() => ClickMe()} type="button">
Click Me
</button>
</>
);
As the others said, it's an asynchronous operation. If you want to use the response as soon as you called onEvent, you can do something along these lines using a promise :
import { useState, useEffect } from 'react';
import { makeRequest } from '../utils';
const useFetch = (query = {}) => {
useEffect(() => {
if (query) {
fetchData(query)
}
}, []) // if query is provided, run query
const [data, setData] = useState({
response: null,
isError: null,
isLoading: true
});
const fetchData = async (query) => {
return new Promise((resolve, reject) => {
if (!query.url) {
reject('url needed')
}
makeRequest(query).then(res => {
setData({
response: res,
isLoading: false,
error: null
})
resolve(res)
).catch(error => {
setData({
response: null,
error,
isLoading: false
});
reject(error)
});
})
})
};
// provide fetchData directly for lazy calls
return { ...data, fetchData };
};
export default useFetch;
And then call it like so :
const { response, fetchData } = useFetch()
fetchData({
url: 'v1/profile/login',
method: 'post',
body: {
username: 'eee#ddd.com',
password: '2342332',
},
}).then(res => ...);

React.js I can't get rid of the loader icon

I'm developing a Movie App. I dont have problem about receiving data and viewing it on the screen. But when i want to add a Loader to my project. It never goes away instead of staying for 1-2 seconds.
const Movies = () => {
const { movies, isLoading } = useGlobalContext();
if (isLoading) {
return <div className="loading"></div>;
}
return (
<section className="movies">
{movies.map((movie) => {
const {
imdbID: key,
Poster: poster,
Title: title,
Year,
year,
} = movie;
return (
<Link to={`/movies/${key}`} key={key} className="movie">
<article>
<img src={poster} alt={title} />
<div className="movie-info">
<h4 className="title">{title}</h4>
<p>{year}</p>
</div>
</article>
</Link>
);
})}
</section>
);
};
It's my context page useGlobalContext and isLoading coming from here
const AppContext = React.createContext();
const AppProvider = ({ children }) => {
const [isLoading, setIsLoading] = useState(true);
const [isError, setError] = useState({ show: false, msg: "" });
const [movies, setMovies] = useState([]);
const [query, setQuery] = useState("spider-man");
const fetchMovies = async (url) => {
setIsLoading(true);
try {
const response = await fetch(url);
const data = await response.json();
if (data.Response === "True") {
setMovies(data.Search);
setError({ show: false, msg: "" });
} else {
setError({ show: true, msg: data.Error });
}
} catch (error) {
console.log(error);
}
};
useEffect(() => {
fetchMovies(`${API_ENDPOINT}&s=${query}`);
}, []);
return (
<AppContext.Provider
value={{ isLoading, isError, movies, query, setQuery }}
>
{children}
</AppContext.Provider>
);
};
export const useGlobalContext = () => {
return useContext(AppContext);
};
export { AppContext, AppProvider };
You never set your isLoading state back to false after you loaded your assets
const AppProvider = ({ children }) => {
const [isLoading, setIsLoading] = useState(true);
const [isError, setError] = useState({ show: false, msg: "" });
const [movies, setMovies] = useState([]);
const [query, setQuery] = useState("spider-man");
const fetchMovies = async (url) => {
setIsLoading(true);
try {
const response = await fetch(url);
const data = await response.json();
if (data.Response === "True") {
setMovies(data.Search);
setError({ show: false, msg: "" });
} else {
setError({ show: true, msg: data.Error });
}
setIsLoading(false); // <--- added this bit
} catch (error) {
console.log(error);
setIsLoading(false); // <--- added this bit
}
};
useEffect(() => {
fetchMovies(`${API_ENDPOINT}&s=${query}`);
}, []);
return (
<AppContext.Provider
value={{ isLoading, isError, movies, query, setQuery }}
>
{children}
</AppContext.Provider>
);
};

Categories