I have custom hook which is catching data from dummyjson API. When I render products, it works fine and perfectly. When I try to catch only one product with this hook via parameter passed via url with useParams in the end it catch this one product, but it cannot render. It seems that a single product didn't manage to load with the help of the hook before it renders. So what is difference when all products are catched are rendered correctly
import axios, { Canceler } from 'axios';
import { useEffect, useState } from 'react';
import { dummyProductType } from '../types/types';
export const useFetch = ({ limit, id }: any) => {
const [products, setProducts] = useState<dummyProductType[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [hasMore, setHasMore] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
let cancel: Canceler;
const config =
id === null || id === undefined
? {
method: 'GET',
url: `https://dummyjson.com/products/`,
params: { limit: limit },
cancelToken: new axios.CancelToken((c) => (cancel = c)),
}
: {
method: 'GET',
url: `https://dummyjson.com/products/${id}`,
cancelToken: new axios.CancelToken((c) => (cancel = c)),
};
async function fetchData() {
setIsLoading(true);
{
await axios(config)
.then((response) => {
if (Object.hasOwn(config, 'params')) {
setProducts((prev) => {
return [...prev, ...response.data.products];
});
} else {
setProducts({ ...response.data });
}
if (products.length < response.data.total) setHasMore(true);
setIsLoading(false);
})
.catch((err) => {
if (axios.isCancel(err)) return;
setError(true);
});
}
}
fetchData();
return () => cancel();
}, [limit, id]);
return { products, isLoading, error, hasMore };
};
import React, { useCallback, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import { useFetch } from '../../hooks/useFetch';
import { CardProduct } from '../CardProduct';
import styles from './Cards.module.scss';
const { wrapperContainer } = styles;
const Cards = () => {
const [limit, setLimit] = useState(10);
const { products, isLoading, hasMore } = useFetch({ limit: limit });
const observer = useRef<IntersectionObserver | null>(null);
const lastProduct = useCallback(
(node: Element) => {
if (isLoading) {
return;
}
if (observer.current) {
observer.current.disconnect();
}
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setLimit((prev) => prev + 10);
}
});
if (node) observer.current.observe(node);
},
[isLoading, hasMore]
);
console.log(products);
return (
<div className={wrapperContainer}>
{products.map((product, index) => {
if (products.length === index + 1) {
return (
<Link to={`books/${index + 1}`}>
<CardProduct
key={`${index} ${product.title}`}
{...product}
innerRef={lastProduct}
/>
</Link>
);
} else {
return (
<Link to={`books/${index + 1}`}>
<CardProduct key={`${index} ${product.title}`} {...product} />
</Link>
);
}
})}
</div>
);
};
export default Cards;
import {
Button,
CardContent,
Card,
CardHeader,
CardMedia,
dividerClasses,
} from '#mui/material';
import { useParams } from 'react-router-dom';
import { useFetch } from '../../hooks/useFetch';
export const CardDetail = () => {
const { id } = useParams();
console.log(id);
const { products, isLoading, hasMore } = useFetch({
id: Number.parseInt(id),
});
console.log(products, isLoading, hasMore);
return (
<Card key={id}>
<CardHeader title={products[0].title}></CardHeader>
<CardMedia
component='img'
image={products[0].thumbnail}
sx={{ height: '150px' }}></CardMedia>
</Card>
);
};
What am I doing wrong? Or maybe it should be done different?
Related
I have this custom http hook with abort when you try to go to a different page (I saw it in a tutorial but I am not truly sure I need it). When I fetch data with it and useEffect(), I have this error on the backend but the request is executed and everything is as planned. My question is, how to improve my code so it does not throw this error and do I need this functionality with abortController() ?
http-hook.ts
import { useCallback, useRef, useEffect } from "react";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { selectError, showError } from "src/redux/error";
import { selectLoading, startLoading, stopLoading } from "src/redux/loading";
export const useHttpClient = () => {
const dispatch = useDispatch();
const error = useSelector(selectError);
const loading = useSelector(selectLoading);
const activeHttpRequests: any = useRef([]);
const sendRequest = useCallback(
async (url, method = "GET", body = null, headers = {}) => {
dispatch(startLoading());
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);
}
dispatch(stopLoading());
return responseData;
} catch (err) {
dispatch(showError(err.message));
dispatch(stopLoading());
throw err;
}
},
[]
);
useEffect(() => {
return () => {
activeHttpRequests.current.forEach((abortCtrl: any) => abortCtrl.abort());
};
}, []);
return { loading, error, sendRequest };
};
UserInfo.tsx
import React, { Fragment, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { useHttpClient } from "src/hooks/http-hook";
import classes from "./UserInfo.module.css";
const UserInfo = () => {
const { sendRequest } = useHttpClient();
const [currentUser, setCurrentUser] = useState<any>();
const userId = useParams<any>().userId;
useEffect(() => {
const fetchCurrentUser = async () => {
try {
const responseData = await sendRequest(
`http://localhost:5000/api/user/${userId}`
);
setCurrentUser(responseData.user);
console.log("currentUser ", currentUser);
} catch (err) {
console.log(err);
}
};
fetchCurrentUser();
}, [sendRequest ,userId]);
return currentUser ? (
<Fragment>
<div className={classes.cover} />
<div className={classes.user_info}>
<img
alt="user_img"
src={`http://localhost:5000/${currentUser.image}`}
className={classes.user_img}
/>
<div className={classes.text}>
<p>
Name: {currentUser.name} {currentUser.surname}
</p>
<p>Email: {currentUser.email}</p>
<p>Age: {currentUser.age}</p>
</div>
</div>{" "}
</Fragment>
) : (
<p>No current user</p>
);
};
export default UserInfo;
Backend
getCurrentUser.ts controller
const getCurrentUser = async (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => {
const userId = req.params.userId;
let user;
try {
user = await User.findById(userId);
} catch (err) {
const error = new HttpError("Could not fetch user", 500);
return next(error);
}
res.json({ user: user.toObject({ getters: true }) });
};
I'm building a searchable index of lessons in Next.js for a school using Airtable as my backend, and I am able to pull the initial page of lessons with getStaticProps and display them. I added a search form that passes an array of search terms through context to a search API component, and though I am able to console log the search terms, I think I am passing the searchTerms array incorrectly, or the search API is not parsing them.
I'm using Auth0 so the component are wrapped with auth, and that all works. Just not returning anything from Airtable when I submit the form.
What I suspect is that I am mangling the fetch or not passing the searchTerms array correctly.
// index.js
import Head from "next/head";
import Image from "next/image";
import Navbar from "../components/Navbar";
import Lesson from "../components/Lesson";
import LessonSearchForm from "../components/LessonSearchForm";
import { table, minifyRecords } from "./api/utils/Airtable";
import { lessonsProvider, LessonsContext } from "../contexts/LessonsContext";
import { useEffect, useState, useContext } from "react";
import { useUser, withApiAuthRequired, getSession } from "#auth0/nextjs-auth0";
export default function Home({ initialLessons }) {
const { lessons, setLessons } = useContext(LessonsContext);
const { searchTerms, setSearchTerms } = useContext(LessonsContext);
const { user, error, isLoading } = useUser();
const [lessonsError, setLessonsError] = useState(null);
useEffect(() => {
setLessons(initialLessons);
}, []);
return (
<div>
<Head>
<title>Lesson Locator</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<Navbar user={user} />
<main>
{user && (
<>
<h2 className="text-2xl text-center mb-4">Lesson Locator Search</h2>
<LessonSearchForm />
{lessons &&
lessons.map((lesson) => (
<Lesson key={lesson.id} lesson={lesson} />
))}
</>
)}
</main>
</div>
);
}
export async function getServerSideProps(context) {
try {
const lessons = await table.select({}).firstPage();
return {
props: {
initialLessons: minifyRecords(lessons),
},
};
} catch (err) {
console.error(err);
return {
props: {
err: "Something went wrong!",
},
};
}
}
// LessonSearchForm.js component ( everything before the return statement )
import React, { useState, useContext } from "react";
import { LessonsContext } from "../contexts/LessonsContext";
export default function LessonSearchForm() {
const [lessonTitle, setLessonTitle] = useState("");
const { findLessons } = useContext(LessonsContext);
const [lessonSource, setLessonSource] = useState("");
const [lessonNumber, setLessonNumber] = useState("");
const [textFieldOption, setTextFieldOption] = useState("text");
const [position, setPosition] = useState("");
const [lessonErrorMsg, setLessonErrorMsg] = useState("");
const [isFindingLesson, setIsFindingLesson] = useState(false);
const { searchTerms, setSearchTerms } = useContext(LessonsContext);
const handleChange = (e) => {
setSearchTerms({
...searchTerms,
// Trimming any whitespace
[e.target.name]: e.target.value.trim(),
});
};
const handleSubmit = (e) => {
e.preventDefault();
console.log({ searchTerms });
findLessons([searchTerms]);
setLessonErrorMsg("");
setIsFindingLesson(false);
};
// LessonsContext.js
import { createContext, useState } from "react";
const LessonsContext = createContext();
const LessonsProvider = ({ children }) => {
const [lessons, setLessons] = useState([]);
const [searchTerms, setSearchTerms] = useState([]);
const refreshLessons = async () => {
try {
const res = await fetch("/api/getLessons");
const lessons = await res.json();
setLessons(initialLessons);
} catch (error) {
console.log(err);
}
};
const findLessons = async ([searchTerms]) => {
try {
const res = await fetch("/api/searchLessons", {
method: "GET",
// body: JSON.stringify(searchTerms),
// fields: [searchTerms],
headers: { "Content-Type": "application/json" },
});
const foundLessons = await res.json();
console.log(foundLessons);
} catch (error) {
console.error("Error performing search", error);
}
};
return (
<LessonsContext.Provider
value={{
lessons,
setLessons,
refreshLessons,
searchTerms,
setSearchTerms,
findLessons,
}}
>
{children}
</LessonsContext.Provider>
);
};
export { LessonsProvider, LessonsContext };
// searchLessons.js in API (Just trying to parse the Title field for testing)
import { table, minifyRecords, findRecordByFilter } from "./utils/Airtable";
import { useUser, withApiAuthRequired, getSession } from "#auth0/nextjs-auth0";
// export default withApiAuthRequired(async (req, res) => {
const searchLessons = async (req, res) => {
// const { user } = useUser();
// const searchTerms = req.fields;
try {
const records = await table
.select({
filterByFormula: `FIND("${req.title}",{Title})`,
})
.firstPage();
const minifiedRecords = minifyRecords(records);
res.statusCode = 200;
res.json(minifiedRecords);
} catch (err) {
res.statusCode = 500;
res.json({ meg: "something went wrong!" }, err);
}
};
export default searchLessons;
// Airtable.js (Utils for Airtable)
const Airtable = require("airtable");
const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base(
process.env.AIRTABLE_BASE_ID
);
const table = base(process.env.AIRTABLE_TABLE_NAME);
const getMinifiedRecord = (record) => {
if (!record.fields.completed) {
record.fields.completed = false;
}
return {
id: record.id,
fields: record.fields,
};
};
const minifyRecords = (records) => {
return records.map((record) => getMinifiedRecord(record));
};
export { table, getMinifiedRecord, minifyRecords };
import React, {useState, useEffect} from 'react';
import {connect} from 'react-redux';
import {
fetchRecipes
} from '../../store/actions';
import './BeerRecipes.css';
const BeerRecipes = ({recipesData, fetchRecipes}) => {
const [page, setPage] = useState(1);
const [recipes, setRecipes] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchRecipes();
}, [])
return (
<div className='beer_recipes_block'>
<div className='title_wrapper'>
<h2 className='title'>Beer recipes</h2>
</div>
<div className='beer_recipes'>
<ul className='beer_recipes_items'>
{
recipesData && recipesData.recipes && recipesData.recipes.map(recipe =>
<li className='beer_recipes_item' id={recipe.id}>{recipe.name}</li>
)
}
</ul>
</div>
</div>
);
};
const mapStateToProps = state => {
return {
recipesData: state.recipes
}
}
const mapDispatchToProps = dispatch => {
return {
fetchRecipes: () => dispatch(fetchRecipes())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(BeerRecipes);
this is my component where I would like to create infinite scroll and below is my redux-action with axios:
import axios from "axios";
import * as actionTypes from "./actionTypes";
export const fetchRecipesRequest = () => {
return {
type: actionTypes.FETCH_RECIPES_REQUEST
}
}
export const fetchRecipesSuccess = recipes => {
return {
type: actionTypes.FETCH_RECIPES_SUCCESS,
payload: recipes
}
}
export const fetchRecipesFailure = error => {
return {
type: actionTypes.FETCH_RECIPES_FAILURE,
payload: error
}
}
export const fetchRecipes = (page) => {
return (dispatch) => {
dispatch(fetchRecipesRequest)
axios
.get('https://api.punkapi.com/v2/beers?page=1')
.then(response => {
const recipes = response.data;
dispatch(fetchRecipesSuccess(recipes));
})
.catch(error => {
const errorMsg = error.message;
dispatch(fetchRecipesFailure(errorMsg));
})
}
}
I want to create a scroll. I need, firstly, to display first 10 elements and then to add 5 elements with every loading. I have 25 elements altogether and when the list is done it should start from the first five again.
I am new to react hooks I write a react custom hook
Hook:
import { useState, useEffect } from 'react';
export const useFetch = (url, options) => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
} catch (error) {
setError(error);
}
};
fetchData();
}, []);
return { response, error };
};
And I also write a functional component and i want component render when data comes
here is my component
Component
import React, { useState, useEffect } from 'react';
import './index.scss';
import { List } from '../components';
import { useFetch } from '../../hooks';
export const Subscription = () => {
const res = useFetch('http://localhost:8080/test', {});
const [isLoading, setLoading] = useState(true);
useEffect(() => {
if (res.response.length > 0) {
console.log('this is the test');
setLoading(false);
}
});
const list = res.response;
return (
<div>
{isLoading && <div>Loading...</div>}
{!isLoading && (
<div className="list">
<List subscriptions={list} />
</div>
)}
</div>
);
};
but i am unable to render List component I didn't understand once data comes from backend why list note having data still it having null value and lists is not renderd
I got proper values from backend
useFetch return return { response, error }; ==> const response = useFetch('http://localhost:8080/test', {}); the response is an object containing { response, error }
Do this instead const {response} = useFetch('http://localhost:8080/test', {});
And you should handle loading in useFetch
UseFetch
import { useState, useEffect } from 'react';
export const useFetch = (url, options) => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
setLoading(false)
} catch (error) {
setError(error);
setLoading(false)
}
};
fetchData();
}, []);
return { response, error,loading };
};
Subscription
import React, { useState, useEffect } from 'react';
import './index.scss';
import { List } from '../components';
mport { useFetch } from '../../hooks';
export const Subscription = () => {
const {response: subscriptions, loading} = useFetch('http://localhost:8080/test', {});
return (
<div>
{isLoading && <div>Loading...</div>}
{!isLoading && (
<div className="list">
<List subscriptions={subscriptions} />
</div>
)}
</div>
);
};
i am a newbie to react but i'm learning and need your help here.
I use Auth0 for Authentication and i have implemented their react sample in parts:
https://auth0.com/docs/quickstart/spa/react/01-login
This are parts of my code:
App.js:
<Auth0Provider
domain={AUTH_CONFIG.domain}
client_id={AUTH_CONFIG.clientId}
redirect_uri={AUTH_CONFIG.callbackUrl}
onRedirectCallback={onRedirectCallback}
>
<Router history={history}>
<RequireAuthentication>
<MyTheme>
<MyLayout />
</MyTheme>
</RequireAuthentication>
</Router>
</Auth0Provider>
Auth0Provider:
import React, { useState, useEffect, useContext } from "react";
import createAuth0Client from "#auth0/auth0-spa-js";
import jwtDecode from "jwt-decode";
import axios from "axios";
import AUTH_CONFIG from "./auth0Config";
import { useDispatch } from "react-redux";
import * as authActions from "app/auth/store/actions";
const DEFAULT_REDIRECT_CALLBACK = () =>
window.history.replaceState({}, document.title, window.location.pathname);
export const Auth0Context = React.createContext();
export const useAuth0 = () => useContext(Auth0Context);
export const Auth0Provider = ({
children,
onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
...initOptions
}) => {
const [isAuthenticated, setIsAuthenticated] = useState();
const [user, setUser] = useState();
const [auth0Client, setAuth0] = useState();
const [loading, setLoading] = useState(true);
const [popupOpen, setPopupOpen] = useState(false);
const dispatch = useDispatch();
useEffect(() => {
const initAuth0 = async () => {
console.log("initAuth0 start");
const auth0FromHook = await createAuth0Client(initOptions);
setAuth0(auth0FromHook);
const isAuthenticated = await auth0FromHook.isAuthenticated();
console.log("Authenticated from init: " + isAuthenticated);
setIsAuthenticated(isAuthenticated);
setLoading(false);
console.log("initAuth0 end");
};
initAuth0();
// eslint-disable-next-line
}, []);
const loginWithPopup = async (params = {}) => {
setPopupOpen(true);
try {
await auth0Client.loginWithPopup(params);
} catch (error) {
console.error(error);
} finally {
setPopupOpen(false);
}
const user = await getUserData();
setUser(user);
dispatch(authActions.setUserDataAuth0(user));
setIsAuthenticated(true);
};
const handleRedirectCallback = async () => {
if (!auth0Client) {
console.warn("Auth0 Service didn't initialize, check your configuration");
return;
}
setLoading(true);
await auth0Client.handleRedirectCallback();
const user = await getUserData();
setLoading(false);
setIsAuthenticated(true);
setUser(user);
dispatch(authActions.setUserDataAuth0(user));
};
const getAccessToken = async () => {
const accessToken = await auth0Client.getTokenSilently({
audience: AUTH_CONFIG.identity_audience,
scope: "read:allUsers read:UserPermission"
});
return accessToken;
};
const getIdToken = async () => {
if (!auth0Client) {
console.warn("Auth0 Service didn't initialize, check your configuration");
return;
}
const claims = await auth0Client.getIdTokenClaims();
return claims.__raw;
};
const getTokenData = async () => {
const token = await getIdToken();
const decoded = jwtDecode(token);
if (!decoded) {
return null;
}
return decoded;
};
const getUserData = async () => {
console.log("getuserdata");
const tokenData = await getTokenData();
const accessToken = await getAccessToken();
return new Promise((resolve, reject) => {
const { sub: userId } = tokenData;
const UserService =
"https://localhost:44312/api/v1/usermanagement/user/" + userId;
axios
.get(UserService, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "GET,HEAD,OPTIONS,POST,PUT",
"Access-Control-Allow-Headers":
"Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers",
"Content-Type": "application/json",
Authorization: "Bearer " + accessToken
}
})
.then(response => {
resolve(response.data);
})
.catch(error => {
// handle error
console.warn("Cannot retrieve user data", error);
reject(error);
});
});
};
return (
<Auth0Context.Provider
value={{
isAuthenticated,
user,
loading,
popupOpen,
loginWithPopup,
handleRedirectCallback,
getIdTokenClaims: (...p) => auth0Client.getIdTokenClaims(...p),
loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
getTokenWithPopup: (...p) => auth0Client.getTokenWithPopup(...p),
logout: (...p) => auth0Client.logout(...p)
}}
>
{children}
</Auth0Context.Provider>
);
};
RequireAuthentication:
import React, { useEffect } from "react";
import { useAuth0 } from "app/auth/AuthProvider";
import { SplashScreen } from "#my";
import history from "#history";
export const RequireAuthentication = ({ children }) => {
const { isAuthenticated, loading } = useAuth0();
useEffect(() => {
console.log("checkAuth");
if (!loading) checkAuth();
// eslint-disable-next-line
}, []);
const checkAuth = () => {
console.log("checkAuth isAuthenticated: " + isAuthenticated);
console.log("checkAuth loading: " + loading);
if (!isAuthenticated && !loading) {
history.push("/login");
}
};
return isAuthenticated ? (
<React.Fragment>{children}</React.Fragment>
) : (
<SplashScreen />
);
};
callback.js:
import React, { useEffect } from "react";
import { SplashScreen } from "#my";
import { useAuth0 } from "app/auth/AuthProvider";
function Callback(props) {
const { isAuthenticated, handleRedirectCallback, loading } = useAuth0();
useEffect(() => {
const fn = async () => {
if (!loading) {
console.log("handleRedirectCallback: " + loading);
await handleRedirectCallback();
}
};
fn();
}, [isAuthenticated, loading, handleRedirectCallback]);
return <SplashScreen />;
}
export default Callback;
The problem is that the RequireAuthentication Component is rendered before the Auth0Provider is completely initialized and therefore i get never the isAuthenticated on "true".
The RequireAuthentication Component is a child of the Auth0Provider. Is it possible to wait for the Auth0Provider is fully initialized before rendering the RequireAuthentication Component???
What is the right way here?? Am I completely wrong?
Thanks
Chris
Depend on loading and isAuthenticated items in useEffect so that component will re render once they change.
useEffect(() => {
console.log("checkAuth");
if (!loading) checkAuth();
// eslint-disable-next-line
}, [loading, isAuthenticated]);