Use nextjs routing outside react hooks in regular javascript - javascript

I have a NextJS app. I want to add a function that can redirect to any page using nextjs routing.
For example, after finishing signup I want to redirect to a certain page.
If I create this function (reusable everywhere) :
import { useRouter } from 'next/router'
const goTo = (href) => {
const router = useRouter()
router.push(href)
}
I want to add this to my signup Logic, the problem is that I break React Hooks rules :
react-dom.development.js?ac89:14906 Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
And effectively useRouter is a React Hook, and I'm trying to call it outside a React function, as stipulated here https://reactjs.org/docs/hooks-rules.html it will not work.
How can I then have a routing solution for NextJS to be callable in regular Javascript ?

So If you're using purely React.js (without Next.js on top of it), you can simple do it this way:
Import React from 'react'
export const handleRoutes = (url) => {
const history = React.useHistory()
return history.push(url)
}
Then You'd import this regular js function in any of your react files, like this:
import { handleRoutes } from 'fileName.js'
<button onClick={() => handleRoutes("/routeToBeRedirectedTo")}> Click to redirect </button>
However when using Nextjs I'm not sure that the above way would work. My personal method would be simply implementing this function (in a utility.js file):
export const handleRedirect = (router, url) => {
return router.push(url)}
Then just importing the function & useRouter hook in the file you want:
import { handleRedirect } from "./utility.js"
import { useRouter } from "next/router"
const router = useRouter
Then inside your JSX return statement:
<button onClick={() => handleRedirect(router, "/routeToBeRedirectedTo")}> Click to redirect </button>
And if it's a redirect after sign in/sign up, just simply useEffect like so:
// depends if you're storing your user credentials in your local storage or cookies, this example below would be if your User credentials are stored in localstorage
const user = JSON.parse(localstorage.getItem("user"))
UseEffect(() => {
if (user) return handleRedirect(router, "/routeToBeRedirectedTo")
}, [user])

Related

Can't call custom hook from custom hook in react

I have a React app created with create-react-app.
I'm trying to make a custom hook using Microsoft Authentication Library (MSAL). MSAL has a custom React hook that I want to call from my own custom hook.
When I use a hook (any hook) inside my custom hook in a separate file I get this in the browser:
Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
// ourhook/index.ts
import { useEffect } from "react";
export const useMsal2 = () => {
useEffect(() => {
console.log("Hello from our hook!");
});
};
// app.tsx
import React from "react";
import { useMsal2 } from "./ourhook";
const App = () => {
useMsal2();
return <div>App</div>;
};
export default App;
If I call
const { instance } = useMsal();
directly from App.tsx everything works fine. It only appears to be a problem if my custom hook is in its own file.
From what I see I'm not violating any hook rules. I'm calling a hook that's calling a hook, and the first call is from a top level component.
I have read other threads here about hooks in hooks, but none of them has an answer that fits this problem.
Have I missed something about hook rules, or what might be causing this?
Okay, I forgot that we tried to have /ourhook as a freestanding project and then copy pasted it into a create react app app.
Some of you were right, it did have its own version of react.
I'm just going to hide under a rock for the rest of the week.
Thanks for all your help! <3
Try to add this comment just above:
import { useMsal } from "#azure/msal-react";
export const useMsal2 = () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { instance } = useMsal();
const request = "";
return {
loginRedirect: () => console.log(""),
}
};
I don't know what useMsal looks like, but from what I see, you don't actually violate any hook rule.

React Native. Error: Invalid hook call. Hooks can only be called inside of the body of a function component

My App was working fine and suddenly i got this error.
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
import { useContext } from "react";
import jwtDecode from "jwt-decode";
import AuthContext from "./context";
import authStorage from "./storage";
const useAuth = () => {
const { user, setUser } = useContext(AuthContext);
const logIn = (authToken) => {
const user = jwtDecode(authToken);
setUser(user);
authStorage.storeToken(authToken);
};
const logOut = () => {
setUser(null);
authStorage.removeToken();
};
return { user, logIn, logOut };
};
export default useAuth;
All looks fine. except maybe actually importing React
import React, { useContext } from "react";
I know you don't need this for React from React 17, but there's no official statement from react native saying they use the new JSX compiler that doesn't require the import statement
also check the AuthContext file you imported

how to listen for route change in react-router-dom v6

am trying to migrate the old react router dom code to v6 and I want to know how to listen for route change, I am now using useHistory
const history = useHistory()
//then
history.listen(...)
I did read the new docs and I did find that useHistory was changed to useNavigate
const navigate = useNavigate()
//then
navigate.listen(...) // listen is not a function
can you please help me find a way to listen to the route change in v6
// This is a React Router v6 app
import { useNavigate } from "react-router-dom";
function App() {
let navigate = useNavigate();
function handleClick() {
navigate("/home");
}
return (
<div>
<button onClick={handleClick}>go home</button>
</div>
);
}
From documentation (https://reactrouter.com/en/main/hooks/use-location), use this hook
let location = useLocation();
React.useEffect(() => {
ga('send', 'pageview');
}, [location]);
The navigate function is a function, not an object like the older react-router-dom version 5's history object.
You can still create a custom history object but you'll need to create a custom router to use it. This allows you to import your history object and create listeners.
Create a custom router example, use one of the higher-level routers as an example for how they manage the location and state, i.e. BrowserRouter:
const CustomRouter = ({ history, ...props }) => {
const [state, setState] = useState({
action: history.action,
location: history.location
});
useLayoutEffect(() => history.listen(setState), [history]);
return (
<Router
{...props}
location={state.location}
navigationType={state.action}
navigator={history}
/>
);
};
In your code create the custom history object for use by your new custom router and other components. Ensure you have history#5 installed as a project dependency. This is the same version used by RRDv6. If you need to install it run npm i history#5 to add it to the project's dependencies.
const history = createBrowserHistory();
export default history;
Use your router and pass your history object to it.
import CustomRouter from '../CustomRouter';
import history from '../myHistory';
...
<CustomRouter history={history}>
....
</CustomRouter>
In a component you want to listen to location changes on, import your history object and invoke the listen callback as you did previously.
import history from '../myHistory';
...
useEffect(() => {
const unlisten = history.listen((location, action) => {
// ... logic
});
return unlisten;
}, []);
If you want, you may be able to also create your own custom useHistory hook that simply returns your history object.
Update
react-router-dom has started exporting a HistoryRouter for a use case like this. Instead of importing the low-level Router and implementing the internal logic you import unstable_HistoryRouter as HistoryRouter and pass your custom history object (memory, hash, etc).
import { unstable_HistoryRouter as HistoryRouter } from "react-router-dom";
import history from "../myHistory";
...
<HistoryRouter history={history}>
....
</HistoryRouter>
Notes on RRDv6.4+
If you are using RRDv6.4+ and not using the Data routers the good-ish news is that unstable_HistoryRouter is still being exported through at least RRDv6.8.0. You can follow along the filed issue in the repo here.
If you are using the Data routers then the new "unstable" method is to use an attached navigate function from the router object directly.
Example:
import { createBrowserRouter } from 'react-router-dom';
// If you need to navigate externally, instead of history.push you can do:
router.navigate('/path');
// And instead of history.replace you can do:
router.navigate('/path', { replace: true });
// And instead of history.listen you can:
router.subscribe((state) => console.log('new state', state));
I've had mixed results with using the history.listen solution between versions 6.4 and 6.8, so probably best to keep an eye on the linked issue for whatever the RRD maintainers say is the current "unstable" method of accessing the "history".
To add to the accepted answer (can't comment, not enough rep points), subscribing to the history through a useEffect with location.pathname in the dependency array won't work if the navigation unmounts the component you're attempting to call the useEffect from.
If you need to react to a change in the route due to back button specifically:
In react-router-dom v6.8.0 or even earlier, trying to attach a listener to the history, will throw an error: A history only accepts one active listener.
I learnt that react-router-dom seems to introduce a lot of changes between the minor versions as well, so you should take words like unsafe and unstable , like in unstable_HistoryRouter especially serious. They will break sooner or later, if you're not very lucky.
In my case I had to upgrade to get the reintroduced optional route params, and the UNSAFE_NavigationContext my former colleague decided to use, didn't work anymore.
So here's a high level approach, that allows you to listen to the actions on the Router's history stack, without attaching another listener to the router yourself. Which is fine, as it already has one listener by default, and it's just not exposed, but the actions derived from it are, which is enough.
In the following example we are reacting to changes in location and for each change, we check if it was due to a POP action, thats e.g. triggered when the browser's back button is used, and then execute whatever..
import { useEffect } from "react";
import {
Location,
NavigationType,
useLocation,
useNavigationType,
} from "react-router-dom";
export const useBackListener = (callback: () => void) => {
const location: Location = useLocation();
const navType: NavigationType = useNavigationType();
useEffect(() => {
if (navType === "POP" && location.key !== "default") {
if (someCondition === true) callback();
else {
doSomethingElse();
}
}
}, [location]);
};

NextJS useRouter error when used inside function body

I hope this is a simple question. I can't figure out why it's doing this. Anyways, using NextJS i'm trying to access the params in the router using the useRouter hook and combining it with the querystring plugin to split asPath, since NextJS doesn't allow you to access the query part of the router if using stateless. This is my code:
import { useRouter } from 'next/router';
import queryString from "query-string";
const withPageRouter = (router) => {
router.query = { ...queryString.parse(router.asPath.split(/\?/)[1]) };
return router;
};
function getRouterParams(){
const router = useRouter();
router = withPageRouter(router);
return router;
}
export async function getTown() {
const town = await getRouterParams();
return town;
}
NOw when I attempt to run it, I get this error:
Server Error
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
This error happened while generating the page. Any console logs will be displayed in the terminal window.
Source
lib/api.js (34:26) # getRouterParams
32 |
33 | function getRouterParams(){
> 34 | const router = useRouter();
| ^
35 | router = withPageRouter(router);
36 | return router;
37 | }
But to me it looks like it should be fine; it is in a function body? I feel like i'm missing something obvious. I appreciate the help.
You can't be call useRouter() in normal function.
You can call only useRouter() inside of Top of the React function component or custom hooks
Learn more about Rules of Hooks here : https://reactjs.org/docs/hooks-rules.html
As an alternative to useRouter you might want to use withRouter (can be used for class components). Also see following related SO question:
How to use "useRouter()" from next.js in a class component?
import { withRouter } from 'next/router'
import React from "react";
export default withRouter(class extends React.Component {
render() {
return (
<div>{ this.props.router.query.id }</div>
)
}
})

Wrapping an external library as a controlled component with react hooks: problem with useEffect dependencies

I'm trying to make a thin wrapper to the "jsoneditor" library using a functionnal component. I'm rather new to React and worked so far mainly with hooks. So I tried to adapt the example given by the author of the library to use hooks:
https://github.com/josdejong/jsoneditor/tree/master/examples/react_demo
This is what I came up with so far:
import React, {useRef, useState, useEffect, useCallback} from 'react'
import JSONEditor from 'jsoneditor'
import styles from './JSONEditorReact.module.css'
import 'jsoneditor/dist/jsoneditor.css';
function App(){
const [json, setJson] = useState({some_key:"some_value"})
function onChangeJson(json){
setJson(json)
}
return <JSONEditorReact onChangeJson={onChangeJson} json={json}/>
}
function JSONEditorReact({onChangeJson, json}){
const containerRef = useRef()
const editorRef = useRef() // used as a namespace to have a reference to the jsoneditor object
useEffect(
() => {
console.log("mounting")
const options = {
modes: ['tree','form','view','text'],
onChangeJSON: onChangeJson
}
editorRef.current = new JSONEditor(containerRef.current, options)
return () => editorRef.current.destroy()
},
[] //eslint complains about the missing dependency "onChangeJson" here
)
useEffect(
() => {
console.log("updating")
editorRef.current.update(json)
},
[json]
)
return (
<div className={styles.container} ref={containerRef} />
)
}
export default App;
It works - but eslint complains about onChangeJson being a missing dependency in useEffect. If I add it as a dependency, useEffect runs each time the user inputs something into the json editor. This implies that the user looses focus on the editor each time he enters a character. My understanding is that when it occurs, setJson function of App is called, so App component is refreshed, causing the onChangeJson function to be-reinstanciated, so the first useEffect is rerun, and a new JSONEditor is instanciated.
I had some ideas but they don't seem satisfying:
define onChangeJson with useCallback - issue : I find it daunting to call useCallback each time I want to use my component
pass the setter function setJson of App as the "onChangeJson" property of JSONEditorReact - issue: what if I want to perform more actions than just setting the state in my callback?
Any more relevant ideas to solve the missing dependency issue without running the first useEffect on each input?
Is this a kind of use case where class components are more relevant than functional components using hooks? (the wrapper using class components looks more straightforward than mine, where I had to use a ref to create a namespace to hold my JSONEditor instance)

Categories

Resources