I am trying to translate a react native app(MobX) to a reactJS app and I want to use react hooks and context(with localStorage) for state management. I have a mainStore file:
export const MainStore = () => {
const AppContext = createContext();
const initialState = {
...
}
const reducer = (state, action) => {
return { ...state, ...action };
}
const [ initState, setInitState ] = useLocalStorage('state', initialState);
const [ state, dispatch ] = useReducer(reducer, initState);
const { getAssetData } = assets();
const getInstMetaData = async () => {
const params = {
asset_type_name: "...",
with: "..."
};
return getAssetData(params);
};
const checkForRefreshTokenUpdate = async () => {
...
}
return {
AppContext,
initialState,
state,
dispatch,
reducer,
checkForRefreshTokenUpdate,
getInstMetaData
};
My asset file is this:
import { interceptedAxios } from "./axios/config";
import { buildUrl } from "./utils/urls";
import { getAccessToken, getBaseUrl } from "./index";
const assets = () => {
const { customAxios } = interceptedAxios();
/**
* Fetches asset data.
* #returns
*/
const getAssetData = async (params) => {
const url = buildUrl({
baseUrl: getBaseUrl(),
endpoint: "/api/asset",
params
});
const reqOpts = {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${getAccessToken()}`
}
};
return customAxios.get(url, reqOpts).then(res => { res.data });
};
return { getAssetData }
}
export { assets };
and my interceptedAxios:
import axios from "axios";
import { MainStore } from "../../stores/mainStore";
import { getBaseUrl } from "../index";
export const interceptedAxios = () => {
// Create axios instance for customization and import this instead of "axios"
const customAxios = axios.create();
const { checkForRefreshTokenUpdate } = MainStore();
customAxios.interceptors.request.use(async (config) => {
if (config.url.includes(getBaseUrl()) && config.headers["Authorization"] != null) {
await checkForRefreshTokenUpdate()
.then((token) => {
if (token != null) {
config.headers["Authorization"] = `Bearer ${token}`;
}
});
}
return config;
},
(error) => {
return Promise.reject(error);
});
return { customAxios }
}
The error is: RangeError: Maximum call stack size exceeded, on running the app.
I know that the fault that is that I am using MainStore() in inreceptedAxios. I am making something like a circle in dependencies, but I can't think of another way to do it and somehow i need to use checkForRefreshTokenUpdate. Any thoughts?
Related
When the user hits the login button, it redirects to the Unsplash login page. After a successful login, the page redirects back to "localhost" with the "code=" parameter in the URL (http://localhost:3000/?code=VbnuDo5fKJE16cjR#=). After that, I need to get the username of the current user and change the background color of his liked images.
Why does the background color only change when the page is reloaded and not after a successful login?
There are too many requests happening at the same time and I don't know how to handle them properly.
Home.js
import React, { useState, useEffect } from "react";
import axios from "axios";
import ImageList from "../components/ImageList";
import SearchBar from "../components/SearchBar";
import Loader from "../helpers/Loader";
import Login from "../components/Login";
function Home() {
const [page, setPage] = useState(1);
const [query, setQuery] = useState("landscape");
const [images, setImages] = useState([]);
const [loading, setLoading] = useState(false);
const clientId = process.env.REACT_APP_UNSPLASH_KEY;
const url = `https://api.unsplash.com/search/photos?page=${page}&query=${query}&client_id=${clientId}&per_page=30`;
const fetchImages = () => {
setLoading(true);
axios
.get(url)
.then((response) => {
setImages([...images, ...response.data.results]);
})
.catch((error) => console.log(error))
.finally(() => {
setLoading(false);
});
setPage(page + 1);
};
useEffect(() => {
fetchImages();
setQuery("");
}, []);
return (
<div>
<Login />
{loading && <Loader />}
<ImageList images={images} />
</div>
);
}
export default Home;
Login.js
import React, { useEffect } from "react"
import { useAppContext } from "../context/appContext";
function Login() {
const { handleClick, getToken, token, getUserProfile } = useAppContext();
useEffect(() => {
if (window.location.search.includes("code=")) {
getToken();
}
if (token) {
getUserProfile();
}
}, [token]);
return (
<div>
<button onClick={() => handleClick()}>Log in</button>
</div>
);
}
export default Login;
appContext.js
import React, { useReducer, useContext } from "react";
import reducer from "./reducer";
import axios from "axios";
import {SET_TOKEN,SET_LIKED_PHOTOS_ID } from "./actions";
const token = localStorage.getItem("token");
const username = localStorage.getItem("username");
const initialState = {
token: token,
username: username,
likedPhotosId: [],
};
const AppContext = React.createContext();
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const handleClick = () => {
window.location.href = `${api_auth_uri}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope.join(
"+"
)}`;
};
const getToken = async () => {
const urlCode = window.location.search.split("code=")[1];
try {
const { data } = await axios.post(
`${api_token_uri}?client_id=${client_id}&client_secret=${client_secret}&redirect_uri=${redirect_uri}&code=${urlCode}&grant_type=${grant_type}`
);
const { access_token } = data;
localStorage.setItem("token", access_token);
dispatch({
type: SET_TOKEN,
payload: { access_token },
});
} catch (error) {
console.log(error);
}
};
const getUserProfile = async () => {
try {
const { data } = await axios.get(`https://api.unsplash.com/me`, {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + state.token,
},
});
const { username } = data;
localStorage.setItem("username", username);
} catch (error) {
console.log(error);
}
};
const getLikedPhotos = async () => {
try {
const { data } = await axios.get(
`https://api.unsplash.com/users/${state.username}/likes`,
{
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + state.token,
},
}
);
const likedPhotosId = data.map((photo) => photo.id);
dispatch({
type: SET_LIKED_PHOTOS_ID,
payload: { likedPhotosId },
});
} catch (error) {
console.log(error);
}
};
return (
<AppContext.Provider
value={{
...state,
handleClick,
getToken,
getUserProfile,
getLikedPhotos,
}}
>
{children}
</AppContext.Provider>
);
};
const useAppContext = () => useContext(AppContext);
export { AppProvider, initialState, useAppContext };
ImageList.js
import React, {useEffect } from "react";
import "../styles/ImageList.scss";
import { useAppContext } from "../context/appContext";
function ImageList({ images }) {
const { username, likedPhotosId, getLikedPhotos } = useAppContext();
useEffect(() => {
if (username) {
getLikedPhotos();
}
}, [username]);
return (
<div className="result">
{images?.map((image) => (
<div
style={{
backgroundColor: likedPhotosId?.includes(image.id) ? "red" : "",
}}
>
<div key={image.id}>
<img src={image.urls.small} alt={image.alt_description} />
</div>
</div>
))}
</div>
);
}
export default ImageList;
reducer.js
import { SET_TOKEN, SET_LIKED_PHOTOS_ID } from "./actions";
const reducer = (state, action) => {
if (action.type === SET_TOKEN) {
return {
...state,
token: action.payload.access_token,
};
}
if (action.type === SET_LIKED_PHOTOS_ID) {
return {
...state,
likedPhotosId: action.payload.likedPhotosId,
};
}
throw new Error(`no such action : ${action.type}`);
};
export default reducer;
The problem is in your function. You save the username in localStorage but not in your reducer state:
const getUserProfile = async () => {
try {
const { data } = await axios.get(`https://api.unsplash.com/me`, {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + state.token,
},
});
const { username } = data;
localStorage.setItem("username", username);
} catch (error) {
console.log(error);
}
};
The issue here is that react doesn't trigger a rerender of components when you set something in localStorage and in your ImageList component use have a useEffect expecting username to change before calling the getLikedPhotos:
const { username, likedPhotosId, getLikedPhotos } = useAppContext();
useEffect(() => {
if (username) {
getLikedPhotos();
}
}, [username]);
So to fix you need to add an action for setting the username state in your reducer:
import { SET_TOKEN, SET_LIKED_PHOTOS_ID, SET_USERNAME } from "./actions";
const reducer = (state, action) => {
if (action.type === SET_TOKEN) {
return {
...state,
token: action.payload.access_token,
};
}
if (action.type === SET_USERNAME) {
return {
...state,
username: action.payload.username,
};
}
if (action.type === SET_LIKED_PHOTOS_ID) {
return {
...state,
likedPhotosId: action.payload.likedPhotosId,
};
}
throw new Error(`no such action : ${action.type}`);
};
export default reducer;
And then dispatch that action from the getUserProfile:
const getUserProfile = async () => {
try {
const { data } = await axios.get(`https://api.unsplash.com/me`, {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + state.token,
},
});
const { username } = data;
localStorage.setItem("username", username);
dispatch({
type: SET_USERNAME,
payload: { username },
});
} catch (error) {
console.log(error);
}
};
I would like to query an individual ID with useQuery hook, getStaticPaths, and getStaticProps in Next.js.
I achieved the whole list, however, I don't know how to get details for individual ID?
The code for the whole list of users is below:
import { QueryClient, useQuery } from 'react-query';
import axios from 'axios'
export default function Index() {
const { isLoading, data, isError, error } = useQuery('users', fetchUsers)
if(isLoading){
return <h2>Loading....</h2>
}
if(isError){
return<h2>{error.message}</h2>
}
return (
<>
{
data?.data.map(user => {
return <div key={user.id}>
<Link href={`users/${user.id}`}>.{user.name}. </Link>
</div>
})
}
</>
)
}
export async function getStaticProps(dehydratedState) {
const queryClient = new QueryClient()
await queryClient.prefetchQuery('users', fetchUsers)
return {
props: { dehydratedState: dehydrate(queryClient).toString()}
};
}
For individual ID's nothing worked, I left below:
It fetches a single user, but not with React Query.
import { dehydrate } from 'react-query/hydration'
import Link from 'next/link'
const fetchUsers = () => {
return axios.get('https://jsonplaceholder.typicode.com/users')
}
function User({ user }){
return (
<div>{user.name}</div>
)
}
export async function getStaticPaths() {
const res = await fetch('https://jsonplaceholder.typicode.com/users')
const users = await res.json()
const paths = users.map((user) => ({
params: { id: user.id.toString() },
}))
return { paths, fallback: false }
}
export async function getStaticProps({ params }) {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${params.id}`)
const user = await res.json()
return { props: { user } }
}
export default User
The third part (this is the single ID fetch with hydrate, not simple Next.js)
import { QueryClient } from "react-query"
import { dehydrate, useQuery } from "react-query"
import axios from "axios"
const userIdFetch = (props) => {
return axios.get(`https://jsonplaceholder.typicode.com/users/${props.id}`)
}
function User(user){
const { isLoading, data, isError, error } = useQuery(['users', user.id], userIdFetch)
console.log(user)
return (
<div></div>
)
}
export async function getStaticPaths() {
const res = await fetch('https://jsonplaceholder.typicode.com/users')
const users = await res.json()
const paths = users.map((user) => ({
params: { id: user.id.toString() },
}))
return { paths, fallback: false }
}
export async function getStaticProps({ params }) {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${params.id}`)
const userFetch = await res.json()
const queryClient = new QueryClient()
await queryClient.prefetchQuery('users', userFetch)
return {
props: { dehydratedState: dehydrate(queryClient).toString()}
}
}
export default User
I want to integrate to React query for fetching the data from server.
So far I've been fetching the rest api via Axios.
I have created a custom hook for fetching and want to transform and implement with react query.
While trying to implement the same logic I encountered an error trying to destructure the fetched data const { data } = useApiRequest(headersUrl):
error - TypeError: (0 , _hooks_useApiRequest__WEBPACK_IMPORTED_MODULE_1__.UseApiRequest) is not a function
Here is the old logic for fetching
import * as React from "react";
import { useState, useEffect } from "react";
import axios from "axios";
import { HeaderToken } from "../services/api";
export const useApiRequest = (url: any) => {
const [data, setData] = useState<[] | any>([]);
useEffect(() => {
const fetchData = () => {
axios
.get(url, {
headers: {
Authorization: `Basic ${HeaderToken}`,
},
})
.then((response) => {
setData(response.data);
})
.catch((error) => console.error(error));
};
fetchData();
}, [url]);
return { data };
};
And here is how I'm trying to convert it:
import { HeaderToken } from "../services/api";
import { useQuery } from "react-query";
export const useApiRequest = (url: any) => {
const { isLoading, data } = useQuery("bc", async () => {
const response = await fetch(url, {
method: "get",
headers: {
Authorization: `Basic ${HeaderToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) throw new Error(response.statusText);
return await response.json();
});
return { data };
};
I can't see the problem, actually, I tried the same code you shared with a local API I have and it's working
The Hook
import { useQuery } from 'react-query'
export const clientAPI = (url) => {
const { isLoading, data } = useQuery("bc", async () => {
const response = await fetch(url, {
method: "get"
});
if (!response.ok) throw new Error(response.statusText);
return await response.json();
});
return { data };
};
React Component
import * as React from "react";
import { clientAPI } from "../hooks/clientAPI";
export default function Home() {
const { data } = clientAPI('http://localhost:5000/')
return (
<div>
{JSON.stringify(data)}
</div>
)
}
_app.js
import { QueryClient, QueryClientProvider, useQuery } from 'react-query'
const queryClient = new QueryClient()
function MyApp({ Component, pageProps }) {
return (<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>)
}
export default MyApp
I'm using next#11.1.2, react-query#3.28.0
what are the versions you are using?
I am trying to persist the data of a user authenticated with AOuth2 (Google Account), but I have problems making the request to the API using contexts.
But when logging in and sending the tokenId of the user, it shows me the following error:
Unhandled Rejection (TypeError): serviceCall is not a function
Request to the API provider:
import { useGoogleAuth } from "../providers/authentication";
import { useApi } from "../providers/API";
export const useUsers = () => {
const { signOut }: any = useGoogleAuth();
const { serviceCall, setTokenId }: any = useApi();
const signInUser = (tokenId: string) => {
serviceCall("/users/signin", "GET", tokenId)
.then(() => console.log("User signed in"))
.catch(() => {
signOut();
setTokenId();
});
};
return {
signInUser,
};
};
API provider:
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
// context
import { useGoogleAuth } from "./authentication";
const ApiContext = React.createContext({});
interface IAPIProviderProps {
children: React.ReactNode;
}
interface IServiceCallProps {
endpoint: string;
method: string;
tokenId: string;
request?: any;
}
export const APIProvider = ({ children }: IAPIProviderProps) => {
const [tokenId, setTokenId] = useState();
const { signOut }: any = useGoogleAuth();
const history = useHistory();
const serviceCall = async (props: IServiceCallProps) => {
const {endpoint, method, tokenId, request} = props;
const url = `${process.env.REACT_APP_API_URL}${endpoint}`;
const response = await fetch(url, {
method,
body: JSON.stringify(request),
headers: {
"Content-Type": "application/json",
Authorization: tokenId,
},
});
if (response.ok) {
return response.json();
} else {
const status = response.status;
switch (status) {
case 401:
signOut();
history.push("/login");
break;
case 403:
history.push("/no-access");
break;
default:
throw new Error();
}
}
};
const value = {
serviceCall,
setTokenId,
tokenId,
};
return <ApiContext.Provider value={value}>{children}</ApiContext.Provider>;
};
export const useApi = () => React.useContext(ApiContext);
Note: I find it a bit difficult because I have started using typescript recently and all the validations it does are difficult for me.
I have two custom hooks i.e useFetch and useAuth. useAuth has all API calls methods (e.g logIn, logOut, register, getProfile etc) and they use useFetch hook method for doing API calls. useFetch also uses these methods for example logOut method when API return 401, setToken etc. So, they both need to share common methods. But that results into circular dependency and call size stack exceeded error. How to manage this
UseFetch.js
import React, { useState, useContext } from "react";
import { AuthContext } from "../context/authContext";
import { baseURL } from "../utils/constants";
import { useAuth } from "./useAuth";
const RCTNetworking = require("react-native/Libraries/Network/RCTNetworking");
export const useFetch = () => {
const {token, setAuthToken, isLoading, setIsLoading, logIn, logOut} = useAuth();
const fetchAPI = (method, url, body, isPublic, noBaseURL) => {
setIsLoading(true);
const options = {
method: method
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
};
return fetch(url, options, isRetrying).then(() => {
......
})
......
};
return { fetchAPI };
};
UseAuth.js
import React, { useContext, useEffect } from "react";
import { AuthContext } from "../context/authContext";
import { useFetch } from "./useFetch";
export const useAuth = () => {
const {
removeAuthToken,
removeUser,
setUser,
...others
} = useContext(AuthContext);
const { fetchAPI } = useFetch();
const register = (body) => {
return fetchAPI("POST", "/customers/register", body, true);
};
const logIn = (body) => {
return fetchAPI("POST", "/customers/login", body, true);
};
const logOut = () => {
return (
fetchAPI("POST", "/customers/logout")
.catch((err) => console.log("err", err.message))
.finally(() => {
removeAuthToken();
removeUser();
})
);
......
};
return {
...others,
register,
logIn,
logOut,
};
};