ReactQuery in NextJs getStaticPaths - reactjs

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

Related

Stay logged in on refresh, JWT

On my website switching between pages is completely fine and works (it doesnt refresh or load due to redux) but the moment the page is refreshed or i manually enter a link to access, it logs me out. Also it just started happening now, yesterday when i was working on some other things in code, it never logged me out when I manually with links/urls navigated thru website or refreshing but now for some reason it doesnt work and I'm 99% sure I havent touched any auth part of the code...
This is my code:
authApiSlice:
import { apiSlice } from "../../app/api/apiSlice";
import { logOut, setCredentials } from "./authSlice";
export const authApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
login: builder.mutation({
query: (credentials) => ({
url: "/auth",
method: "POST",
body: { ...credentials },
}),
}),
sendLogout: builder.mutation({
query: () => ({
url: "/auth/logout",
method: "POST",
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
console.log(data);
dispatch(logOut());
setTimeout(() => {
dispatch(apiSlice.util.resetApiState());
}, 1000);
} catch (err) {
console.log(err);
}
},
}),
refresh: builder.mutation({
query: () => ({
url: "/auth/refresh",
method: "GET",
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
console.log(data);
const { accessToken } = data;
dispatch(setCredentials({ accessToken }));
} catch (err) {
console.log(err);
}
},
}),
}),
});
export const { useLoginMutation, useSendLogoutMutation, useRefreshMutation } =
authApiSlice;
authSlice:
import { createSlice } from "#reduxjs/toolkit";
const authSlice = createSlice({
name: "auth",
initialState: { token: null },
reducers: {
setCredentials: (state, action) => {
const { accessToken } = action.payload;
state.token = accessToken;
},
logOut: (state, action) => {
state.token = null;
},
},
});
export const { setCredentials, logOut } = authSlice.actions;
export default authSlice.reducer;
export const selectCurrentToken = (state) => state.auth.token;
import { Outlet, Link } from "react-router-dom";
import { useEffect, useRef, useState } from "react";
import { useRefreshMutation } from "./authApiSlice";
import usePersist from "../../hooks/usePersist";
import { useSelector } from "react-redux";
import { selectCurrentToken } from "./authSlice";
const PersistLogin = () => {
const [persist] = usePersist();
const token = useSelector(selectCurrentToken);
const effectRan = useRef(false);
const [trueSuccess, setTrueSuccess] = useState(false);
const [refresh, { isUninitialized, isLoading, isSuccess, isError, error }] =
useRefreshMutation();
useEffect(() => {
if (effectRan.current === true || process.env.NODE_ENV !== "development") {
// React 18 Strict Mode
const verifyRefreshToken = async () => {
console.log("verifying refresh token");
try {
//const response =
await refresh();
//const { accessToken } = response.data
setTrueSuccess(true);
} catch (err) {
console.error(err);
}
};
if (!token && persist) verifyRefreshToken();
}
return () => (effectRan.current = true);
// eslint-disable-next-line
}, []);
let content;
if (!persist) {
// persist: no
console.log("no persist");
content = <Outlet />;
} else if (isLoading) {
//persist: yes, token: no
console.log("loading");
} else if (isError) {
//persist: yes, token: no
console.log("error");
content = (
<p className="errmsg">
{`${error?.data?.message} - `}
<Link to="/login">Please login again</Link>.
</p>
);
} else if (isSuccess && trueSuccess) {
//persist: yes, token: yes
console.log("success");
content = <Outlet />;
} else if (token && isUninitialized) {
//persist: yes, token: yes
console.log("token and uninit");
console.log(isUninitialized);
content = <Outlet />;
}
return content;
};
export default PersistLogin;
RequireAuth
import { useLocation, Navigate, Outlet } from "react-router-dom";
import useAuth from "../../hooks/useAuth";
const RequireAuth = ({ allowedRoles }) => {
const location = useLocation();
const { roles } = useAuth();
const content = roles.some((role) => allowedRoles.includes(role)) ? (
<Outlet />
) : (
<Navigate to="/prijava" state={{ from: location }} replace />
);
return content;
};
export default RequireAuth;
It just stopped worked for some reason, it shouldnt log me out when I refresh.

Why does the background color only change when the page is reloaded and not after a successful login?

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);
}
};

RangeError: Maximum call stack size exceeded, react hooks

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?

Next.js with next-redux-wrapper state is being reset to initial value when navigating using Link

Next.js v12.0
next-redux-wrapper.
Whenever I navigate away from the page using the appropriate next/link element and then back again (using another link el) the state is reset to the initial value and so another fetch is executed. What is strange about this is that I have another 'transaction' slice setup in an identical manner except it holds an array of transaction objects and that one is working just fine (navigate away and back and the data is not re-fetched as it persisted in store) code is below any suggestions would be greatly appreciated.
store.js
import { HYDRATE, createWrapper } from "next-redux-wrapper";
import thunkMiddleware from "redux-thunk";
import address from "./address/reducer";
import transactions from "./transaction/reducer";
const bindMiddleware = (middleware) => {
if (process.env.NODE_ENV !== "production") {
const { composeWithDevTools } = require("redux-devtools-extension");
return composeWithDevTools(applyMiddleware(...middleware));
}
return applyMiddleware(...middleware);
};
const combinedReducer = combineReducers({
transactions,
address,
});
const rootReducer = (state, action) => {
if (action.type === HYDRATE) {
const nextState = {
...state, // use previous state
...action.payload, // apply delta from hydration
};
if (state.address.id){
nextState.address = state.address;
}
return nextState;
} else {
return combinedReducer(state, action);
}
};
const initStore = () => {
return createStore(rootReducer, bindMiddleware([thunkMiddleware]));
};
export const wrapper = createWrapper(initStore);
address/reducer.js
const addressInitialState = {
id: null,
timestamp: null,
address: null,
balance: null,
received: null,
sent: null,
groupid: null,
last_txs: []
};
export default function reducer(state = addressInitialState, action) {
switch (action.type) {
case addressActionTypes.GET_WALLET_DETAILS:
return {id: action.payload.address, ...action.payload};
default:
return state;
}
}
address/action.js
export const addressActionTypes = {
GET_WALLET_DETAILS: "GET_WALLET_DETAILS",
};
export const getWalletDetails = (address) => {
return async (dispatch) => {
const fetchData = async () => {
const response = await fetch(
`https:someapi.com/api/getaddress/?address=${address}`
);
if (!response.ok) {
throw new Error("Could not fetch address data!");
}
const data = await response.json();
console.log('req sent');
return data;
};
try {
const addressData = await fetchData();
dispatch({
type: addressActionTypes.GET_WALLET_DETAILS,
payload: addressData,
});
} catch (err) {
console.log(err);
}
};
};
pages/[address].js
import { Fragment } from "react";
import Head from "next/head";
import AddressDetails from "../../../components/crypto/rvn/AddressDetails";
import AddressTransactions from "../../../components/crypto/rvn/AddressTransactions";
import { connect } from "react-redux";
import { getWalletDetails } from "../../../store/address/action";
import { wrapper } from "../../../store/store";
function Address(props) {
return (
<Fragment>
<Head>
<title>RVN</title>
<meta name="description" content="RVN Address" />
</Head>
<AddressDetails address={props.addressDetails}></AddressDetails>
<AddressTransactions
transactions={props.addressDetails["last_txs"]}
address={props.addressDetails.address}
></AddressTransactions>
</Fragment>
);
}
export const getServerSideProps = wrapper.getServerSideProps(
(store) => async (context) => {
const state = store.getState();
if(state.address.id === null) {
await store.dispatch(getWalletDetails(context.params.address));
}
else{
return{
props: {
addressDetails: state.address,
}
}
}
}
);
const mapStateToProps = (state) => ({
addressDetails: state.address,
});
export default connect(mapStateToProps, null)(Address);
Solved this by converting this
const initStore = () => {
return createStore(rootReducer, bindMiddleware([thunkMiddleware]));
};
in store.js
which direclty from https://github.com/vercel/next.js/tree/canary/examples/with-redux-thunk
to this
const store = createStore(rootReducer, bindMiddleware([thunkMiddleware]));
const initStore = () => {
return store
};
so it does not reinitialize the store every time the wrapper is used
this is more in line with the documentation at
https://github.com/kirill-konshin/next-redux-wrapper

React Custom Hooks Circular Dependency

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,
};
};

Resources