I am encountering a browser infinite looping issue in my NextJS project when I add the SSRKeycloakProvider component from #react-keycloak/ssr npm package. This infinite loop only happens with a specific application the Reports page.
My investigations have led me to believe it is related to the way cookies are handled with keycloak integration, and in the Reports page I am also using windows.replaceState() javascript function.
Everytime the loop starts, this is the message I get from the application:
[Function: setReportq]
Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes.
at Overflow (/home/ubuntu/growth-admin/node_modules/rc-overflow/lib/Overflow.js:42:32)
at SelectSelector (/home/ubuntu/growth-admin/node_modules/rc-select/lib/Selector/MultipleSelector.js:36:18)
at div
at Selector (/home/ubuntu/growth-admin/node_modules/rc-select/lib/Selector/index.js:38:35)
at Trigger (/home/ubuntu/growth-admin/node_modules/rc-trigger/lib/index.js:79:36)
at SelectTrigger (/home/ubuntu/growth-admin/node_modules/rc-select/lib/SelectTrigger.js:74:25)
at div
at BaseSelect (/home/ubuntu/growth-admin/node_modules/rc-select/lib/BaseSelect.js:67:18)
at Select (/home/ubuntu/growth-admin/node_modules/rc-select/lib/Select.js:66:18)
at InternalSelect (/home/ubuntu/growth-admin/node_modules/antd/lib/select/index.js:55:31)
at div
at div
at div
at Col (/home/ubuntu/growth-admin/node_modules/antd/lib/grid/col.js:59:33)
at FormItemInput (/home/ubuntu/growth-admin/node_modules/antd/lib/form/FormItemInput.js:44:25)
at div
at Row (/home/ubuntu/growth-admin/node_modules/antd/lib/grid/row.js:56:34)
at FormItem (/home/ubuntu/growth-admin/node_modules/antd/lib/form/FormItem.js:101:20)
at form
at Form (/home/ubuntu/growth-admin/node_modules/rc-field-form/lib/Form.js:33:19)
at SizeContextProvider (/home/ubuntu/growth-admin/node_modules/antd/lib/config-provider/SizeContext.js:19:23)
at InternalForm (/home/ubuntu/growth-admin/node_modules/antd/lib/form/Form.js:66:27)
at div
at Report (webpack-internal:///./common/components/DominoReport/index.js:152:3)
at div
at appClassicstyle__ContentWrapper (/home/ubuntu/growth-admin/node_modules/styled-components/dist/styled-components.cjs.js:1:19220)
at div
at appClassicstyle__AppWrapper (/home/ubuntu/growth-admin/node_modules/styled-components/dist/styled-components.cjs.js:1:19220)
at AppClassic (webpack-internal:///./common/components/Admin/report.js:27:3)
at div
at main
at Basic (/home/ubuntu/growth-admin/node_modules/antd/lib/layout/layout.js:78:25)
at Content (/home/ubuntu/growth-admin/node_modules/antd/lib/layout/layout.js:61:37)
at section
at BasicLayout (/home/ubuntu/growth-admin/node_modules/antd/lib/layout/layout.js:93:34)
at Layout (/home/ubuntu/growth-admin/node_modules/antd/lib/layout/layout.js:61:37)
at section
at BasicLayout (/home/ubuntu/growth-admin/node_modules/antd/lib/layout/layout.js:93:34)
at Layout (/home/ubuntu/growth-admin/node_modules/antd/lib/layout/layout.js:61:37)
at exports.ThemeProvider (/home/ubuntu/growth-admin/node_modules/styled-components/dist/styled-components.cjs.js:1:24917)
at Growth (webpack-internal:///./containers/Admin/growth.js:81:3)
at AppClassic (webpack-internal:///./pages/report.js:19:3)
at ThemeProvider (/home/ubuntu/growth-admin/node_modules/#material-ui/styles/ThemeProvider/ThemeProvider.js:48:24)
at KeycloakProvider (/home/ubuntu/growth-admin/node_modules/#react-keycloak/core/lib-commonjs/provider.js:72:51)
at SSRKeycloakProvider (/home/ubuntu/growth-admin/node_modules/#react-keycloak/ssr/lib-commonjs/SSRKeycloakProvider.js:64:28)
at CustomApp (webpack-internal:///./pages/_app.tsx:63:3)
at StylesProvider (/home/ubuntu/growth-admin/node_modules/#material-ui/styles/StylesProvider/StylesProvider.js:57:24)
at ae (/home/ubuntu/growth-admin/node_modules/styled-components/dist/styled-components.cjs.js:1:13296)
at AppContainer (/home/ubuntu/growth-admin/node_modules/next/dist/server/render.js:293:29)
The above message mentions the parseCookies function (in _app.tsx:63:3) ,
the pages/index.tsx line 20 which is basically this line:
const parsedToken: ParsedToken | undefined = keycloak?.tokenParsed
,the windows.replaceState() function setting the URL params in the Growth component, and the specific reportq() function which is also in the Growth.
Here is my _app.tsx, I believe the parseCookies function is of interest here:
import React, { useEffect } from "react"
import App from 'next/app'
import { SSRKeycloakProvider, SSRCookies } from '#react-keycloak/ssr'
import cookie from 'cookie'
import type { IncomingMessage } from 'http'
import { ThemeProvider } from '#material-ui/core/styles';
import theme from '../theme';
const KC_URL = process.env.NEXT_PUBLIC_KC_URL;
const KC_REALM = process.env.NEXT_PUBLIC_KC_REALM
const KC_CLIENT_ID = process.env.NEXT_PUBLIC_KC_CLIENT_ID
const keycloakCfg = {
realm: KC_REALM,
url: KC_URL,
clientId: KC_CLIENT_ID
}
interface InitialProps {
cookies: unknown
}
export default function CustomApp({ Component, pageProps, cookies }) {
const initOptions = {
onLoad: 'login-required',
checkLoginIframe: false
}
return (
<SSRKeycloakProvider
keycloakConfig={keycloakCfg}
persistor={SSRCookies(cookies)}
initOptions={initOptions}
>
<ThemeProvider theme={theme}>
<Component {...pageProps} />
</ThemeProvider>
</SSRKeycloakProvider>
);
}
// I think the cookies and this function have to do with the issue
function parseCookies(req?: IncomingMessage) {
if (!req || !req.headers) {
return {}
}
return cookie.parse(req.headers.cookie || '')
}
CustomApp.getInitialProps = async (appContext) => {
// Your static props paths
const staticPropsPaths = [
"/paper/[paperId]/[paperName]",
"/hubs"
]
if (process.browser || !staticPropsPaths.includes(appContext.router.route)) {
const appProps = await App.getInitialProps(appContext)
return { ...appProps, cookies: parseCookies(appContext?.ctx?.req) }
}
}
And here is my index.tsx:
import type { NextPage } from 'next'
import { useKeycloak } from '#react-keycloak/ssr'
import type { KeycloakInstance, KeycloakTokenParsed } from 'keycloak-js'
import Growth from '../containers/Admin/growth'
type ParsedToken = KeycloakTokenParsed & {
email?: string
preferred_username?: string
given_name?: string
family_name?: string
}
const Home = ({ query }) => {
const { keycloak } = useKeycloak<KeycloakInstance>()
const parsedToken: ParsedToken | undefined = keycloak?.tokenParsed
const loggedinState = keycloak?.authenticated ? (
<span className="text-success">logged in</span>
) : (
<span className="text-danger">NOT logged in</span>
)
const welcomeMessage =
keycloak?.authenticated || (keycloak && parsedToken)
? `Welcome back ${parsedToken?.preferred_username ?? ''}!`
: 'Welcome ! Please login to continue.'
return <Growth query={query} page={'home'} />
}
Home.getInitialProps = async ({ query, res }) => {
return { query }
}
export default Home
the Growth code is below: and I believe the line of interest in the Growth component:
window.history.replaceState('state', 'Growth ', `${BASE_URL}${page}${reportq}`)
The Growth Component:
const Growth = ({ query, page }) => {
const { keycloak, initialized } = useKeycloak()
const router = useRouter()
let [p, setP] = useState(page)
let reportq;
let prospectq;
// I believe this function is involved
const setReportq = (params) => {
window.sessionStorage.setItem("reportq", params)
}
const setProspectq = (params) => {
window.sessionStorage.setItem("prospectq", params)
}
useEffect(() => {
if (typeof window !== 'undefined') {
reportq = window.sessionStorage.getItem('reportq');
if (!reportq)
reportq = '';
prospectq = window.sessionStorage.getItem('prospectq');
if (!prospectq)
prospectq = '';
}
if (typeof window !== 'undefined' && page == 'report') {
const keys = Object.keys(query);
let params = "?";
for (let i = 0; i < keys.length; i++) {
params += `${keys[i]}=${encodeURIComponent(query[keys[i]])}&`
}
// console.log("eval reportq vs query", { reportq, params })
if (!reportq && keys && keys.length > 0) {
console.log("growth line 78 setting reportq")
setReportq(params)
}
else {
if (params != reportq) {
console.log("growth line 81 updating url with ", reportq)
window.history.replaceState('state', 'Growth ', `${BASE_URL}${page}${reportq}`)
}
}
}
})
const key = query.key;
const setPage = (page) => {
setP(page)
setTimeout(() => {
const newUrl = `${BASE_URL}${page == 'home' ? '' : page}${page == 'report' ? reportq : page == 'prospect' ? prospectq ? prospectq : `?key=${key}` : `?key=${key}`}`
router.push(newUrl);
}, 1);
}
return (
<ThemeProvider theme={theme}>
<>
<Head>
<title>Growth</title>
<meta name="robots" content="noindex" />
<meta name="theme-color" content="#2563FF" />
</Head>
<Layout>
<Sider
style={{
overflow: 'auto',
height: '100vh',
position: 'fixed',
left: 0,
}}
>
<Menu theme="dark" mode="inline" defaultSelectedKeys={[page]}>
<Menu.Item key="report" onClick={() => setPage('report')}>
Reports
</Menu.Item>
</Menu>
</Sider>
<Layout className="site-layout" style={{ marginLeft: 200 }}>
{p == 'report' && <Report query={query} setReportq={setReportq} />}
</Layout>
</Layout>
</>
</ThemeProvider >);
}
Growth.getInitialProps = async ({ query, res }) => {
return { query }
}
export default Growth;
Change onLoad: 'login-required' to onLoad: 'check-sso',
Related
I am creating an application that displays information in three languages without using any APIS. In the settings page , user can click change to spanish button which will be stored in the ASYNC storage .I am new to react native and would like to know whether this is best practice .
ACCOUNT SETTINGS
import { View, Text ,Button} from 'react-native'
import React, { useState , useEffect} from 'react'
import {AsyncStorage} from 'react-native';
const Account = () => {
const setspanish=()=> {
const lanugage = {
language:"spanish",
}
AsyncStorage.getItem('lang').then((datacart)=>{
if (datacart !== null) {
// We have data!!
const lang = JSON.parse(datacart)
lang.push(lanugage)
AsyncStorage.setItem('lang',JSON.stringify(lang));
}
else{
const lang = []
lang.push(lanugage)
AsyncStorage.setItem('lang',JSON.stringify(lang));
}
alert("ChangedLnag")
})
.catch((err)=>{
alert(err)
})
}
return (
<View>
<Button onPress={setspanish} title="spanish"/>
</View>
)
}
export default Account
I have create a state in different pages , but none update automatically . Once i navigate to other pages , i have used ternary operators to render out depending on the state which recieves async storage language value but none works .
You should set it on the context and save it, and for first time you should take and set it again to context
I write example about that:
interface IConfig{
lang: "en" | "lalala";
}
interface IContextConfig{
config: IConfig;
setConfig?: (val: any) => void;
}
export const ContextConfigApp = React.createContext<IContextConfig>({
config: {lang: "en"},
});
interface IPropsProvider{
init?: IConfig;
children: React.ReactNode;
}
const Provider = ({init = {lang: "en"}}) => {
const [config,setConfig] = useState<IConfig>(init);
useEfect(() => {
AsyncStorage.setItem('config',JSON.stringify(config));
},[config]);
useEfect(() => {
(async () => {
const tmp = await AsyncStorage.getItem('config');
if(!!tmp && tmp) setConfig({...config,...JSON.parse(tmp)});
})();
},[]);
return (
<ContextConfigApp.Provider value={{config,setConfig}}>
{children}
</ContextConfigApp.Provider>
)
}
const App = () => {
return (
<Provider>
<Header />
</Provider>
)
}
const Header = () => {
const {setConfig,config} = useContext(ContextConfigApp);
return (
<Button onPress={() => {
setConfig({...config,lang: "en"})
}}>EN</Button>
)
}
Usage
const Example = () => {
const {config} = useContext(ContextConfigApp)
return (
<Text>{config.lang}</Text>
)
}
this is just example I hope it help you
I tried implementing infinite scroll using useSWRInfinite hook.
Found a code from an Youtube tutorial 👈 and made little alteration.
But got an error named "React has detected a change in the order of Hooks called by InfiniteDataList."
2 hours of debugging -- no solution found.
Picture of the error
Youtube tutorial code --> https://github.com/gdangelo/micro-blogging-workshop/blob/main/components/InfiniteDataList.js
Youtube tutorial link --> https://www.youtube.com/watch?v=FsngdxyvFrQ
MY CODE:
InfiniteDataList.js
import React, { useEffect, useRef } from "react";
import { useInfiniteQuery } from "../../hooks";
import MessageWrapper from "../../UI/MessageWrapper";
import { isInViewport } from "../../Utility/windowUtils";
import { useDebouncedCallback } from "use-debounce";
import DefaultListItemComponent from "./components/DefaultListItemComponent";
import DefaultContainerComponent from "./components/DefaultContainerComponent";
import DefaultLoadMoreComponent from "./components/DefaultLoadMoreComponent";
const InfiniteDataList = ({
queryKey,
initialData = [],
listItemComponent: ListItemComponent = DefaultListItemComponent,
containerComponent: ContainerComponent = DefaultContainerComponent,
onError = () => {},
onEmpty = () => {},
onEmptyComponent: OnEmptyComponent = null,
onNoMoreData = () => {},
noMoreDataComponent: NoMoreDataComponent = null,
isAutoLoadMoreAtEnd = true,
autoLoadMoreAtEndOptions: {
timeout = 500,
onLoadMoreDetected = () => {},
} = {},
loadMoreComponent: LoadMoreComponent = DefaultLoadMoreComponent,
}) => {
// hooks
const {
data,
error,
hasNextPage,
fetchNextPage,
isFetchingInitialData,
isFetchingNextPageData,
} = useInfiniteQuery(queryKey, { initialData });
const moreRef = useRef();
const loadMore = useDebouncedCallback(() => {
if (isInViewport(moreRef.current)) {
onLoadMoreDetected();
fetchNextPage();
}
}, timeout);
const getLoadMoreRef = () => moreRef;
useEffect(() => {
if (isAutoLoadMoreAtEnd) {
window.addEventListener("scroll", loadMore);
}
return () => window.removeEventListener("scroll", loadMore);
}, []);
// some configuration
OnEmptyComponent = OnEmptyComponent && (() => <h4>No Details found</h4>);
NoMoreDataComponent =
NoMoreDataComponent &&
(() => <MessageWrapper message="No More Data found !" />);
// helper utils
const infiniteQueryProps = {
data,
error,
hasNextPage,
fetchNextPage,
isFetchingInitialData,
isFetchingNextPageData,
};
// if error occurs
if (error) {
onError(error);
console.log("error");
}
// no data found
if (!isFetchingInitialData && data?.length === 0) {
onEmpty();
console.log(typeof OnEmptyComponent);
return <OnEmptyComponent />;
}
// no more data to load
if (!hasNextPage) {
onNoMoreData();
}
return (
<ContainerComponent loading={isFetchingInitialData}>
{data?.map((item, index) => (
<ListItemComponent key={index} {...item} />
))}
{hasNextPage ? (
<LoadMoreComponent
{...infiniteQueryProps}
getLoadMoreRef={getLoadMoreRef}
/>
) : (
<NoMoreDataComponent {...infiniteQueryProps} />
)}
</ContainerComponent>
);
};
export default InfiniteDataList;
useInfiniteQuery.js
import useSWRInfinite from "swr/infinite";
import { axiosInstance } from "../Utility/axiosInstance";
function getFetcher(requestType = "get") {
return (url, dataToPost) =>
axiosInstance[requestType](url, dataToPost).then((res) => res.data);
}
export function useInfiniteQuery(
queryKey,
{ initialData, requestType = "get" }
) {
const { data, error, size, setSize } = useSWRInfinite(
(pageIndex, previousPageData) => {
// reached the end
if (previousPageData && !previousPageData.after) return null;
// first page
if (pageIndex === 0) return queryKey;
// next pages
const search = queryKey.includes("?");
return `${queryKey}${search ? "$" : "?"}cursor=${encodeURIComponent(
JSON.stringify(previousPageData.after)
)}`;
},
getFetcher(requestType),
initialData
);
// to fetch next page from react component
function fetchNextPage() {
setSize((prev) => prev + 1);
}
// flatten all the data obtained so far to a single array
const flattenPages = data?.flatMap((page) => page.data) ?? [];
// indicates whether the api will have data for another page
const hasNextPage = !!data?.[size - 1]?.after;
// isLoading for initial request
const isFetchingInitialData = !data && !error;
// isLoading for other requests including the initial request
const isFetchingNextPageData =
isFetchingInitialData ||
(size > 0 && data && typeof data[size - 1] === "undefined");
return {
data: flattenPages,
error,
hasNextPage,
fetchNextPage,
isFetchingInitialData,
isFetchingNextPageData,
};
}
isInViewport.js
// Check if element is visible inside the viewport
export function isInViewport(element) {
if (!element) return false;
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
DefaultLoadMoreComponent.js
import React, { useState } from "react";
const DefaultLoadMoreComponent = ({ getLoadMoreRef = () => {} }) => {
const ref = getLoadMoreRef();
return <div ref={ref} />;
};
export default DefaultLoadMoreComponent;
DefaultListItemComponent.js
import React from "react";
const DefaultListItemComponent = ({ children = [] }) => <div>{children}</div>;
export default DefaultListItemComponent;
DefaultContainerComponent.js
import React from "react";
import AsyncDiv from "../../../UI/AsyncDiv";
const DefaultContainerComponent = ({ children = [], ...rest }) => (
<AsyncDiv {...rest}>{children}</AsyncDiv>
);
export default DefaultContainerComponent;
Component where I render InfiniteDataList component
import React from "react";
import InfiniteDataList from "../../../../../UI/InfiniteDataList";
import PaginatedLeads from "./components/PaginatedLeads";
import { getError } from "../../../../../Utility/apiUtils";
const ViewAllLeads = (props) => {
return (
<InfiniteDataList
initialData={[]}
listItemComponent={PaginatedLeads}
onError={(err) =>
window.flash({ title: getError(err).message, type: "error" })
}
queryKey="/employee/leads"
/>
);
};
export default ViewAllLeads;
PaginatedLeads.js
import React from "react";
const PaginatedLeads = (props) => {
console.log(props);
return <div>PaginatedLeads</div>;
};
export default PaginatedLeads;
It is my mistake.
In useInfiniteQuery.js file, I passed the initial data in wrong format.
Wrong syntax -- see the last before line(initialData)
const { data, error, size, setSize } = useSWRInfinite(
(pageIndex, previousPageData) => {
// reached the end
if (previousPageData && !previousPageData.after) return null;
// first page
if (pageIndex === 0) return queryKey;
// next pages
const search = queryKey.includes("?");
return `${queryKey}${search ? "&" : "?"}cursor=${encodeURIComponent(
JSON.stringify(previousPageData.after)
)}`;
},
getFetcher(requestType),
initialData
);
Right syntax -- see the last before line({ fallbackData: initialData })
const { data, error, size, setSize } = useSWRInfinite(
(pageIndex, previousPageData) => {
// reached the end
if (previousPageData && !previousPageData.after) return null;
// first page
if (pageIndex === 0) return queryKey;
// next pages
const search = queryKey.includes("?");
return `${queryKey}${search ? "&" : "?"}cursor=${encodeURIComponent(
JSON.stringify(previousPageData.after)
)}`;
},
getFetcher(requestType),
{ fallbackData: initialData }
);
In a React & Next.js app I'm trying to implement a back button. To do that I've added currentPath and prevPath to the session storage in the _app.js file.
// pages/_app.js
function MyApp({ Component, pageProps }) {
const router = useRouter();
useEffect(() => {
const storage = globalThis?.sessionStorage;
if (!storage) return;
storage.setItem('prevPath', storage.getItem('currentPath'));
storage.setItem('currentPath', globalThis.location.pathname);
}, [router.asPath]);
return <Component {...pageProps} />
}
export default MyApp
Then I am trying to get this data in a Navigation.js component.
// Navigation.js
const router = useRouter();
const [prevPath, setPrevPath] = useState('/');
useEffect(() => {
const getPrevPath = globalThis?.sessionStorage.getItem('prevPath');
setPrevPath(getPrevPath);
}, [router.asPath]);
return (
// …
<Link href={prevPath || '/'}>
<a>Back</a>
</Link>
//…
)
While the session storage works correctly, the value returned is one from the previous page (that is previous page's prevPath) instead of the current one. Technically, asking for a currentPath instead of a prevPath would be the solution to what I'm trying to do but I'd like to (learn to) do it the right way.
Additional info:
I've tried to get data with async/await but it didn't make any difference.
useEffect(async () => {
const getPrevPath = await globalThis?.sessionStorage.getItem('prevPath');
setPrevPath(getPrevPath);
}, [router.asPath]);
Also, earlier in a day (the implementation was different) I've tried as an experiment adding a delay of 1/1000th of a second and it did make it work correctly. Given that, I'm not confident waiting a fixed number of seconds (or a fixed fraction of a second) would be a good solution (could someone confirm?).
Would appreciate the help.
Problem
I'm assuming you want to add and remove history (similar to a real browser history) instead of just constantly replacing the history with whatever route was previous. Instead of constantly replacing the pathname upon a route change, you'll want to conditionally add/remove it from some sort of history.
Solution
Here's a hook that utilizes an Array (basically a flat array of asPath strings -- you may want to limit the size of the Array to prevent performance issues):
import * as React from "react";
import { useRouter } from "next/router";
const usePreviousRoute = () => {
const { asPath } = useRouter();
// initialize history with current URL path
const [history, setHistory] = React.useState([asPath]);
const lastHistoryIndex = history.length - 2;
// get second to last route in history array
const previousRoute = history[lastHistoryIndex > 0 ? lastHistoryIndex : 0];
const removeHistory = () => {
// get current history
setHistory((prevHistory) =>
// check if the history has more than 1 item
prevHistory.length > 1
// if it does, remove the last history item
? prevHistory.filter((_, index) => index !== prevHistory.length - 1)
// else don't remove any history
: prevHistory
);
};
React.useEffect(() => {
// get current history
setHistory((prevHistory) =>
// check if the last history item is the current path
prevHistory[prevHistory.length - 1] !== asPath
// if not, add current path to history
? [...prevHistory, asPath]
// else don't add any history
: prevHistory
);
}, [asPath]);
return { previousRoute, removeHistory };
};
export default usePreviousRoute;
With capped history:
React.useEffect(() => {
// get current history
setHistory((prevHistory) =>
// check if last history item is current path
prevHistory[prevHistory.length - 1] !== asPath
// if not...
? [
// check if history has more than 10 items
// spread result into shallow copied array
...(prevHistory.length > 9
// if it does have more than 10 items, remove first item
? prevHistory.filter((_, index) => index !== 0)
// else don't remove history
: prevHistory),
asPath
]
// else don't remove history
: prevHistory
);
}, [asPath]);
Demo
Source Code:
Browser Demo URL: https://knfoj.sse.codesandbox.io/
Demo Code
Navigation.js
/* eslint-disable jsx-a11y/anchor-is-valid */
import * as React from "react";
import Link from "next/link";
import { useHistoryContext } from "../../hooks/useRouteHistory";
import GoBackLink from "../GoBackLink";
import styles from "./Navigation.module.css";
const Navigation = () => {
const { history } = useHistoryContext();
return (
<>
<nav className={styles.navbar}>
{[
{ title: "Home", url: "/" },
{ title: "About", url: "/about" },
{ title: "Example", url: "/example" },
{ title: "NoLayout", url: "/nolayout" }
].map(({ title, url }) => (
<Link key={title} href={url} passHref>
<a className={styles.link}>{title}</a>
</Link>
))}
</nav>
<GoBackLink />
<div className={styles.history}>
<h4 style={{ marginBottom: 0 }}>History</h4>
<pre className={styles.code}>
<code>{JSON.stringify(history, null, 2)}</code>
</pre>
</div>
</>
);
};
export default Navigation;
useRouteHistory.js
import * as React from "react";
import { useRouter } from "next/router";
export const HistoryContext = React.createContext();
export const useHistoryContext = () => React.useContext(HistoryContext);
export const usePreviousRoute = () => {
const { asPath } = useRouter();
const [history, setHistory] = React.useState([asPath]);
const lastHistoryIndex = history.length - 2;
const previousRoute = history[lastHistoryIndex > 0 ? lastHistoryIndex : 0];
const removeHistory = () => {
setHistory((prevHistory) =>
prevHistory.length > 1
? prevHistory.filter((_, index) => index !== prevHistory.length - 1)
: prevHistory
);
};
React.useEffect(() => {
setHistory((prevHistory) =>
prevHistory[prevHistory.length - 1] !== asPath
? [...prevHistory, asPath]
: prevHistory
);
}, [asPath]);
return { history, previousRoute, removeHistory };
};
export const HistoryProvider = ({ children }) => {
const historyProps = usePreviousRoute();
return (
<HistoryContext.Provider
value={{
...historyProps
}}
>
{children}
</HistoryContext.Provider>
);
};
export default HistoryProvider;
_app.js
import * as React from "react";
import HistoryContext from "../hooks/useRouteHistory";
const App = ({ Component, pageProps }) => (
<HistoryContext>
<Component {...pageProps} />
</HistoryContext>
);
export default App;
index.js
import Layout from "../components/Layout";
const IndexPage = () => (
<Layout>
<h1>Index Page</h1>
<p>
...
</p>
</Layout>
);
export default IndexPage;
I'm implementing an infinite scroll with Apollo and React. Everything works fine. When I navigate away from Feed and then back to Feed I'm getting this weird error:
TypeError: Cannot read property 'fetchMore' of undefined
Has anyone else had experience with this issue? I found this but there doesn't seem to yet be any solutions. One of the answers mentions a partial solution "checking the route before executing fetchMore" but I don't know what means.
I'm still in the middle of development so I haven't had a chance to clean this component up yet, but here it is:
import React, { useEffect, useRef } from 'react';
import { useQuery } from '#apollo/client';
import PostUpdateOrShow from '../posts/types/showOrUpdate/PostUpdateOrShow.js'
import Cookies from 'js-cookie';
import Queries from '../../graphql/queries';
import InfiniteScroll from './util/Infinite_Scroll.js';
const { FETCH_USER_FEED, FETCH_TAG_FEED } = Queries;
const Feed = ({
user, tag
}) => {
let fetchMoreDiv = useRef(null);
let cursorId = useRef(null);
useEffect(() => {
var scroll = document.addEventListener('scroll', function(event) {
fetchMoreDiv.current = document.querySelector('#fetchMore')
var el = fetchMoreDiv.current.getBoundingClientRect()
var elTop = el.top
var elBottom = el.bottom
var innerHeight = window.innerHeight
if (elTop >= 0 && elBottom <= innerHeight) {
fetchMore({
query: gqlQuery,
variables: {
query: query,
cursorId: cursorId.current
},
})
}
})
return () => {
document.removeEventListener('scroll', scroll)
}
})
var gqlQuery
var query
if (user) {
gqlQuery = FETCH_USER_FEED
query = user.blogName
} else if (tag) {
gqlQuery = FETCH_TAG_FEED
query = tag.title.slice(1)
} else {
gqlQuery = FETCH_USER_FEED
query = Cookies.get('currentUser')
}
let { loading, error, data, fetchMore } = useQuery(gqlQuery, {
variables: {
query: query,
cursorId: null
},
})
if (loading) return 'Loading...';
if (error) return `Error: ${error}`;
const { fetchUserFeed, fetchTagFeed } = data
cursorId.current = fetchUserFeed ? fetchUserFeed[fetchUserFeed.length - 1]._id :
fetchTagFeed[fetchTagFeed.length - 1]._id
if (tag) {
return(
<div>
<div>
{fetchTagFeed.map((post, i) => {
return (
<div
className='post'
key={post._id}
>
<PostUpdateOrShow
post={post}
/>
</div>
)
})}
</div>
<InfiniteScroll
fetchMoreDiv={fetchMoreDiv}
/>
<div
id='fetchMore'
>
</div>
</div>
)
} else {
return(
<div>
<div>
{fetchUserFeed.map((post, i) => {
return (
<div
className='post'
key={post._id}
>
<PostUpdateOrShow
post={post}
/>
</div>
)
})}
</div>
<InfiniteScroll
fetchMoreDiv={fetchMoreDiv}
/>
<div
id='fetchMore'
>
</div>
</div>
)
}
}
export default Feed;
Apollo client config:
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
fetchLikesRepostsAndComments: {
merge: (existing = [], incoming = []) => {
return incoming
}
},
fetchUserFeed: {
keyArgs: ['query'],
merge: (existing = [], incoming = []) => {
const elements = [...existing, ...incoming].reduce((array, current) => {
return array.map(i => i.__ref).includes(current.__ref) ? array : [...array, current];
}, []);
return elements
},
}
}
}
}
}),
errorLink
})
I believe the issue is that you are using fetchMore within useEffect hook.
Try to rethink your code to avoid this. Using fetchMore outside the hook would work flawlessly.
I am working on an image transition react component, where it waits to have img1 loaded and then on a user click loads the img2, but fades img1 smoothly.
Tried re-writing the application with hooks to set the states - but when this is applied it creates an re-render loop error.
Is it because we are always setting img1 as currentImgSrc initially?
const [imgSrcOne, setImgSrcOne] = useState(currentImgSrc)
errors in the if/else block? or is setting the useState in the return causing the bug
tried removing the if/else block to make the application functional
https://jsfiddle.net/b531L6ho/
import {
ImageTransition
} from './imageTransition/imageTranition'
const {
useState
} = React
interface ImageContainerProps {
layer: string
currentImgSrc: string
notifyOnError: (url: string) => void
updateLayerLoading: (hasLoaded: boolean) = void
}
export const ImageTransitionContainer: React.SFC < ImageContainerProps > = ({
currentImgSrc,
layer,
notifyOnError,
updateLayerLoading
}) => {
const [imgSrcOne, setImgSrcOne] = useState(currentImgSrc)
const [displayImgOne, setdisplayImgOne] = useState(true)
const [imgOneLoaded, setImgOneLoaded] = useState(false)
const [imgSrcTwo, setImgSrcTwo] = useState(currentImgSrc)
const [displayImgTwo, setdisplayImgTwo] = useState(true)
const [imgTwoLoaded, setImgTwoLoaded] = useState(false)
if (imgSrcOne && currentImgSrc !== imgSrcOne) {
console.log("in the if statement")
setImgSrcTwo(currentImgSrc)
setDisplayImgTwo(two)
}
if (currentImgSrc !== imgSrcOne) {
setImgSrcne(currentImgSrc)
}
if (!imgSrcOne && !imgSrcTwo) {
setImgSrcOne(currentImgSrc)
setDisplayImgOne(true)
} else if (imgSrcOne && currentImgSrc !== imgSrcOne) {
setImgSrcTwo(currentImgSrc)
setDisplayImgTwo(true)
} else if (imgSrcTwo && currentImgSrc !== imgSrcTwo) {
setImgSrcOne(currentImgSrc)
setDisplayImgOne(true)
}
console.log("state --", imgSrcOne, displayImgOne, imgOneLoaded, imgSrcTwo, displayImgTwo, imgTwoLoaded)
return (
<>
<ImageTransition
displayImg={displayImgOne}
imgLoaded={imgOneLoaded}
imgSrc={imgSrcOne}
onExit={() => {
setImgSrcOne(null)
setImgOneLoaded(false)
}}
onLoad={() => {
setImgOneLoaded(true)
setDisplayImgTwo(false)
}}
/>
<ImageTransition
displayImg={displayImgTwo}
imgLoaded={imgTwoLoaded}
imgSrc={imgSrcTwo}
onExit={() => {
setImgSrcTwo(null)
setImgTwoLoaded(false)
}}
onLoad={() => {
setImgTwoLoaded(true)
setDisplayImgOne(false)
}}
/>
</>
)
}