In a normal Next.js 13 app without the experimental app directory, you'd setup keyboard shortcuts something like this:
import { useCallback, useEffect } from 'react';
export default function App() {
const handleKeyPress = useCallback((event: KeyboardEvent) => {
console.log("Key pressed:", event.key);
}, []);
useEffect(() => {
document.addEventListener('keydown', handleKeyPress);
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
}, [handleKeyPress]);
...
With the experimental app directory, I'd imagine that I should do something similar in layout.tsx. I can't do this since I need useEffect (client) and the docs say "The root layout is a Server Component by default and can not be set to a Client Component."
How do I do this?
Related
In a React project, I've created a popup modal which will be displayed when any user tries to do any changes in input field and navigate to other screen. It doesn't work as expected, hence gone through many posts to find the solution but, no luck. Please refer to code below:
useBlock.js
import {useContext, useEffect} from 'react';
import { UNSAFE_NavigationContext as NavigationContext} from 'react-router-dom';
const useBlocker = (blocker, when = true) => {
const navigator = useContext(NavigationContext).navigator
useEffect(() => {
if (!when)
return;
const unblock = navigator.block((tx) => { <-- This line is creating an issue
const autoUnblockingTx = {
...tx,
retry() {
unblock();
tx.retry();
},
};
blocker(autoUnblockingTx);
});
return unblock;
}, [navigator, blocker, when]);
}
export default useBlocker
useCallbackPrompt.js
import { useCallback, useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router';
import useBlocker from './useBlocker';
const useCallbackPrompt = (when) => {
const navigate = useNavigate();
const location = useLocation();
const [showPrompt, setShowPrompt] = useState(false);
const [lastLocation, setLastLocation] = useState(null);
const [confirmedNavigation, setConfirmedNavigation] = useState(false);
const cancelNavigation = useCallback(() => {
setShowPrompt(false);
}, []);
const handleBlockedNavigation = useCallback((nextLocation) => {
if (!confirmedNavigation &&
nextLocation.location.pathname !== location.pathname) {
setShowPrompt(true);
setLastLocation(nextLocation);
return false;
}
return true;
}, [confirmedNavigation]);
const confirmNavigation = useCallback(() => {
setShowPrompt(false);
setConfirmedNavigation(true);
}, []);
useEffect(() => {
if (confirmedNavigation && lastLocation) {
navigate(lastLocation.location.pathname);
}
}, [confirmedNavigation, lastLocation]);
useBlocker(handleBlockedNavigation, when);
return [showPrompt, confirmNavigation, cancelNavigation];
}
export default useCallbackPrompt
So above are the 2 files which I'm using. In useBlocker.js file that particular line is actually causing the root issue. Please refer to the image below
I'm using "react-router-dom": "^6.3.0", Is this causing any issue? Any suggestions or modifications are highly appreciated.
I wasn't able to reproduce the issue using react-router-dom#6.3.0, but I could when bumping to react-router-dom#6.4.0. I suspect with a dependency specified as ^6.3.0 you've actually a more current version actually installed. If you like you can check the installed version by running npm list react-router-dom and verify for yourself.
It seems the navigation context has a mildly breaking change between v6.3.0 and v6.4.0. The v6.3.0 version is a history object (source) while the v6.4.0 is a new navigation context object where navigator is a simpler interface (source).
Solution 1 - Revert to previous version
You could revert back to 6.3.0 though by running npm i -s react-router-dom#6.3.0 to install that exact version. Double-check your package.json file to ensure the entry is "react-router-dom": "6.3.0".
Solution 2 - Use the "real" history object
If you wanted to move forward with the newer RRD versions then an alternative I'd suggest is to use the history#5 history object directly instead of trying to use the react-router#6 navigator. RRDv6 was only ever exporting a subset of the history methods anyway.
Add history#5 as a project dependency.
IMPORTANT: You will want to check what version react-router-dom is using and match if you can.
Create and export a custom history object. createBrowserHistory for a BrowserRouter, createHashHistory for a HashRouter, etc.
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
export default history;
Import your custom history object and the history router from RRD.
import { unstable_HistoryRouter as Router } from "react-router-dom";
import history from './history';
...
<Router history={history}>
<App />
</Router>
Import your custom history object to use in your custom hooks.
import { useCallback, useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import history from "./history"; // <-- import
const useBlocker = (blocker, when = true) => {
useEffect(() => {
if (!when) return;
const unblock = history.block((tx) => { // <-- use history
const autoUnblockingTx = {
...tx,
retry() {
unblock();
tx.retry();
}
};
blocker(autoUnblockingTx);
});
return unblock;
}, [blocker, when]);
};
useCallbackPrompt is untouched.
const useCallbackPrompt = (when) => {
const navigate = useNavigate();
const location = useLocation();
const [showPrompt, setShowPrompt] = useState(false);
const [lastLocation, setLastLocation] = useState(null);
const [confirmedNavigation, setConfirmedNavigation] = useState(false);
const cancelNavigation = useCallback(() => {
setShowPrompt(false);
}, []);
const handleBlockedNavigation = useCallback(
(nextLocation) => {
if (
!confirmedNavigation &&
nextLocation.location.pathname !== location.pathname
) {
setShowPrompt(true);
setLastLocation(nextLocation);
return false;
}
return true;
},
[confirmedNavigation]
);
const confirmNavigation = useCallback(() => {
setShowPrompt(false);
setConfirmedNavigation(true);
}, []);
useEffect(() => {
if (confirmedNavigation && lastLocation) {
navigate(lastLocation.location.pathname);
}
}, [confirmedNavigation, lastLocation]);
useBlocker(handleBlockedNavigation, when);
return [showPrompt, confirmNavigation, cancelNavigation];
};
Demo
From v6.4.0 navigator.block is removed. You can find a workaround here: https://gist.github.com/MarksCode/64e438c82b0b2a1161e01c88ca0d0355.
Also, relevant discussion going on here. https://github.com/remix-run/react-router/issues/8139#issuecomment-1262630360
I am trying to build a basic notification app that uses the react-native-background-task module. For some reasons it gives this error: Cannot read properties of undefined (reading 'schedule')
Below is the code:
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import BackgroundTask from 'react-native-background-task';
// import { Notifications, Permissions, Constants } from 'expo';
import * as Notifications from 'expo-notifications';
import * as Permissions from 'expo-permissions';
import React, { useEffect } from 'react'
console.log("object")
BackgroundTask.define(async () => {
console.log("bgtask")
// if time is 12pm, fire off a request with axios to fetch the pills info
// Notification configuration object
const localNotification = {
title: text,
body: 'msg',
data: data,
ios: {
sound: true
}
}
// trigger notification, note that on ios if the app is open(in foreground) the notification will not show so you will need to find some ways to handling it which is discribed here https://docs.expo.io/versions/latest/guides/push-notifications
Notifications
.presentLocalNotificationAsync(localNotification)
.catch((err) => {
console.log(err)
})
BackgroundTask.finish()
})
export default function App() {
useEffect(() => {
console.log("uef")
const componentDidMount = async () => {
// allows the app to recieve notifications (permission stuff)
console.log("cdm")
registerForPushNotificationsAsync().then(() => {
console.log("bg sche")
BackgroundTask.schedule()
}).catch((e) => {
console.log("err")
console.log(e)
});
}
componentDidMount()
}, []);
const registerForPushNotificationsAsync = async () => {
console.log("reg")
const { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS);
if (status !== 'granted') {
console.log("permission not granted")
return;
}
console.log("permission granted")
let deviceToken = await Notifications.getExpoPushTokenAsync()
}
return (
<View style={styles.container}>
<Text>Open up App.js \o start working on your app!</Text>
<StatusBar style="auto" />
</View>
);
}
The problem comes when I do the BackgroundTask.schedule() function. It says in the error that BackgroundTask is undefined. I know for a fact that all the other functions worked fine because the console.logs all get printed right until "bg sche" and then it goes and print "err" in the catch block.
I also tried to use ctrl + click on the package name that should bring me to source where this BackgroundTask object is exported but it doesn't work like it usually does for other modules. So I think for some reasons the module can't be found but I have already installed it and it is in my package.json file and I see it in my node_modules folder.
I'm trying to integrate CleverTap into my Next.js app. Followed the documentation Web SDK Quick Start Guide but facing issue:
Server Error ReferenceError: window is not defined in Next.js
_app.tsx
import React, { useEffect, useState } from "react";
import type { AppProps } from "next/app";
import { appWithTranslation } from "next-i18next";
import { Hydrate, QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
import nextI18NextConfig from "../next-i18next.config.js";
import "tailwindcss/tailwind.css";
import "styles/globals.scss";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import { useRouter } from "next/router";
import SvgPageLoading from "components/color-icons/PageLoading";
// import { PageLoading } from "components/color-icons/";
import { DefaultSeo } from 'next-seo';
import SEO from 'next-seo.config';
import {cleverTap} from "utils/cleverTapHelper";
cleverTap.initialize('TEST-61c-a12');
function MyApp({ Component, pageProps }: AppProps) {
const [queryClient] = React.useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: Infinity,
},
},
})
);
const router = useRouter();
const [isAnimating, setIsAnimating] = useState(false);
useEffect(() => {
const handleStart = () => {
setIsAnimating(true);
};
const handleStop = () => {
setIsAnimating(false);
};
router.events.on("routeChangeStart", handleStart);
router.events.on("routeChangeComplete", handleStop);
router.events.on("routeChangeError", handleStop);
return () => {
router.events.off("routeChangeStart", handleStart);
router.events.off("routeChangeComplete", handleStop);
router.events.off("routeChangeError", handleStop);
};
}, [router]);
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<DefaultSeo {...SEO} />
<Component {...pageProps} />
{isAnimating && (
<div className="fixed top-0 left-0 flex items-center justify-center w-screen h-screen overflow-visible bg-white bg-opacity-80 z-overlay top-z-index">
<SvgPageLoading />
</div>
)}
<ReactQueryDevtools initialIsOpen={false} />
</Hydrate>
</QueryClientProvider>
);
}
export default appWithTranslation(MyApp, nextI18NextConfig);
cleverTapHelper.ts
export const cleverTap = {
initialize: function (accountId) {
console.log('I see initialize req')
window.clevertap = {event: [], profile: [], account: [], onUserLogin: [], notifications: []};
window.clevertap.account.push({'id': accountId});
(function () {
var wzrk = document.createElement('script');
wzrk.type = 'text/javascript';
wzrk.async = true;
wzrk.src = ('https:' == document.location.protocol ? 'https://d2r1yp2w7bby2u.cloudfront.net' : 'http://static.clevertap.com') + '/js/a.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(wzrk, s);
})();
},
event: function (name, payload = {}) {
console.log('I see event req')
if (payload) {
window.clevertap.event.push(name, payload);
} else {
window.clevertap.event.push(name);
}
},
profile: function (payload) {
console.log('I see profile req')
window.clevertap.profile.push(payload);
},
logout: function () {
console.log('I see logout req')
window.clevertap.logout();
}
};
cleverTap.d.ts
declare global {
interface Window {
clevertap: any;
}
}
export {};
Window object should not be undefined but getting undefined! What's going on?
This is because NextJS is trying to execute that function on the server because it uses SSR, and window is a browser object. Since the window object is available only in the browser (client-side), the server is unable to identify the window object, hence getting undefined. In order to fix this, you should make sure that any functions/components that contain client-side related code be executed only on the browser or client-side. One way is using hooks such as useEffect that run only after the component is mounted. Another way is to use lazy loading which pretty much does the same thing.
Using useEffect hook.
In your _app.tsx component, add a new useEffect hook and move the initialization code into the newly created useEffect function.
useEffect(()=>{
cleverTap.initialize('TEST-61c-a12');
},[])
Using lazy loading. (Dynamic import)
Instead of directly importing the function, import it dynamically and set server-side rendering to false:
import dynamic from 'next/dynamic'
const cleverTap = dynamic(()=>{
return import("utils/cleverTapHelper")
},
{ssr:false}
)
cleverTap.initialize('TEST-61c-a12');
For TS folks struggling out there with clevertap and nextjs, install sdk and types:
npm i -S clevertap-web-sdk #types/clevertap-web-sdk
then, async import while initializing:
import CleverTap from 'clevertap-web-sdk/clevertap';
// ^^ this only imports types
let clevertap: CleverTap;
export const initClevertap = async (clevertapAccountID: string, region: string): Promise<void> => {
clevertap = (await import('clevertap-web-sdk')).default;
clevertap.init(clevertapAccountID, region);
clevertap.privacy.push({ optOut: false });
clevertap.privacy.push({ useIP: true });
clevertap.setLogLevel(0);
clevertap.spa = true;
};
PS: dynamic import didn't worked for me, looks like it's only for components and not libs
I am trying to navigate to a certain screen whenever I click on an Expo Push Notification. The screen that I want to navigate to is rather deep into the NavigationContainer.
However, the issue that I am facing now is being unable to even navigate to anywhere except having the app restart on its own. I'm running all the testing on a real device.
I'm using Expo to work on this school project.
I have only managed to find this question in SO and Expo Forums (duplicate) useful.
This is my application Navigation structure:
-Navigation Structure-
AppNavigator
DrawerNavigator
MainNavigator
TabsNavigator
StackNavigator
StackNavigator
TabsNavigator
ScreenA (Want to navigate to)
ScreenB (Want to navigate to)
StackNavigator
ScreenA
ScreenB
StackNavigator
ScreenA
AuthNavigator
RegisterNavigator
ScreenA
There is a useNotifications hook created and I called it in the main App Navigator where the NavigationContainer resides in.
import React, { useEffect } from 'react';
import * as Notifications from 'expo-notifications';
import navigation from '../navigation/RootNavigation';
const useNotifications = () => {
const notiResponseListener = React.createRef();
useEffect(() => {
notiResponseListener.current =
Notifications.addNotificationResponseReceivedListener(res => {
console.log(res.notification.request.content.data);
console.log('addNotificationResponseReceivedListener');
navigation.navigate(
('DrawerNavigator', { screen: 'ChangePassword' }),
{}
);
});
return () =>
Notifications.removeNotificationSubscription(notiResponseListener);
}, []);
};
export default useNotifications;
There is a ref added to the NavigationContainer.
import { navigationRef } from '../navigation/RootNavigation';
import useNotifications from '../hooks/useNotifications';
const App = createStackNavigator();
const AppNavigator = () => {
useNotifications();
return (
<NavigationContainer ref={navigationRef}>
<App.Navigator headerMode='none'>
...
</App.Navigator>
</NavigationContainer>
);
};
And lastly, the file that contains the ref used in the NavigationContainer.
import React from 'react';
export const navigationRef = React.createRef();
const navigate = (name, params) => {
console.log('entered navigating'); // does not print
navigationRef.current?.navigate(name, params);
};
export default {
navigate
};
I have searced high and low but I can't seem to find out what's wrong. Looked at the documentation for Expo and React Navigation but I'm not sure what's going on. It's my first time working on Push Notifications and such a case.
I appreciate any help, thank you
We have fixed the problem with the usage of useLastNotificationResponse.
const [notification, setNotification] = useState(false);
const notificationListener = useRef();
const responseListener = useRef();
//add this
const lastNotificationResponse =
Notifications.useLastNotificationResponse();
useEffect(() => {
if (lastNotificationResponse) {
//console.log(lastNotificationResponse);
//get the route
const route = JSON.stringify(
lastNotificationResponse.notification.request.content.data.route
);
//use some function to return the correct screen by route
getFullPath(JSON.parse(route));
}
}, [lastNotificationResponse]);
Based on your routes, navigate to correct screen
getFullPath:
import { navigationRef } from "./rootNavigation";
import routes from "./routes";
export function getFullPath(route) {
switch (route) {
case "HomeScreen":
return navigationRef.current?.navigate(routes.HOME);
case "Account":
return navigationRef.current?.navigate(routes.ACCOUNT, {
screen: routes.ACCOUNTSCREEN,
});
default:
return;
}
}
Problem: I am trying to achieve 100% test coverage using Jest with my React component, but it just doesn't seem to detect the function being called. I tried to extract out only the relevant parts below:
LinkedRepos.js:
import { auth } from "../../firebase";
import React, { useEffect } from "react";
import { navigate } from "#reach/router";
const ReposPage = () => {
React.useEffect(() => {
auth.onAuthStateChanged(function (user) {
if (!user) {
console.log("DEBUGGING NO USER");
navigate("/");
}
});
}, []);
};
export default ReposPage;
LinkedRepos.test.js
import React from "react";
import { shallow } from "enzyme";
import ReposPage from "../Components/Pages/LinkedRepos";
import { navigate } from "#reach/router";
jest.mock('#reach/router', () => ({
navigate: jest.fn(),
}))
describe("Linked repos page", () => {
it("not logged in", () => {
jest.clearAllMocks();
jest.spyOn(React, 'useEffect').mockImplementation(f => f());
const repopage = shallow(<ReposPage />);
expect(navigate).toHaveBeenCalledTimes(1);
})
});
The test output:
● Linked repos page › not logged in
expect(jest.fn()).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 0
....
console.log src/Components/Pages/LinkedRepos.js:14
DEBUGGING NO USER
I can't seem to find any answers that apply to my problem. I know my code is quite bad, but does anyone know the reason why it won't detect the "navigate" function being called? Clearly, it is reaching that point in the code, since it is printing out my debugging message. I just can't seem to wrap my head around testing.
It turns out I justed needed an await statement for Jest to recognise that navigate was being called. The final code is something like this:
it("should call navigate once if not logged in", async () => {
await mount(<ReposPage />);
expect(navigate).toHaveBeenCalledTimes(1);
});