I am getting this error in Next.js:
Error: The provided 'href' (/subject/[subject]) value is missing query values (subject) to be interpolated properly. Read more: https://err.sh/vercel/next.js/href-interpolation-failed`.
I have a dynamic page set up as /subject/[subject].tsx. Now in my navigation I have:
<Link href={'/subject/${subject}'} passHref><a>{subject}</a></Link>
It works fine when I access the page with different paths but when I am pressing on a button on the page it throws the error above which I imagine is because the component rerenders. If you go to the page in the error it says: Note: this error will only show when the next/link component is clicked not when only rendered.
I have tried to look for a solution and I tried doing:
<Link href={{pathname: '/subject/[subject]', query: {subject: subject}}}></Link>
but nothing changed. I read the docs and it seems that the as prop is only an optional decorator that is not used anymore so I fail to see how that can help me.
I got the same issue when trying to redirect user to locale. I did it in useEffect. After investigate I discovered that on first render router.query is empty, so it's missing required field id. I fix it by using router.isReady
export const useUserLanguageRoute = () => {
const router = useRouter()
useEffect(() => {
const {
locales = [],
locale,
asPath,
defaultLocale,
pathname,
query,
isReady // here it is
} = router
const browserLanguage = window.navigator.language.slice(0, 2)
const shouldChangeLocale =
isReady && // and here I use it
locale !== browserLanguage
&& locale === defaultLocale
&& locales.includes(browserLanguage)
if (shouldChangeLocale) {
router.push(
{
pathname,
query,
},
asPath,
{ locale: browserLanguage }
)
}
}, [router])
}
Another possible solution could be redirect using the router.push function:
const myRedirectFunction = function () {
if (typeof window !== 'undefined') {
router.push({
pathname: router.pathname,
query: {...router.query, myqueryparam: 'myvalue'},
})
}
}
return (
<>
<button onClick={() => {myRedirectFunction()}}> Continue </button>
</>
)
It is important to include ...router.query because there is where the [dynamic] current value is included, so we need to keep it.
Reference: https://github.com/vercel/next.js/blob/master/errors/href-interpolation-failed.md
You better do a null || undefined check in prior.
#Bentasy, Like you rightly pointed out, any re-render of the page is expecting you to pass in the dynamic route params again. The way I avoided page re-rendering with subsequent clicks on the page was to replace Link tag on the page with a label tag. I was using the Next Link tag to navigate between the tabs on this image which generated this same error. So I replaced the Link tags with label tags and the error was solved.
I was having the same problem, this is how I solved it.
While pushing something to the router, we need to give the old values in it.
In that:
const router = useRouter()
router.push({
...router,
pathname: '/your-pathname'
})
try add this function, it work for me:
export async function getServerSideProps(context) {
return {
props: {},
};
}
Related
I have created an app with Vue with some different views/components. The views/components have to get run through in an specific order (MainPage -> Step1 -> Step2 -> Step3 -> FinalPage --> MainPage...).
As I am learning Vue I managed the navigation with v-if directives which worked well, as there are no URLs, so I was able to implement the logic in each view/component like
//pseudocode in each view/component
checkConditionsToMoveOn ? moveToNextPae : showErrorWithModalAndStayOnPage;
Getting more professional I implemented vue-router, which somehow broke my app as I am able now to trick the logic to pass a step with just changing the URL manually.
I can think of implementing some navigation guards, but then I have to move the conditions to my router/index.js file, which I think is not really pretty.
Is there any best practice to solve this problem?
I'd appreciate a hint, thanks.
Using Vuex, track the current state of step progress:
state: {
step: 1
}
When the user visits any step, you'll check this state. And if the requested step route doesn't match this, you'll redirect to it. To implement this, define the route with route params:
// import store from '#/store';
{
path: '/step/:sid',
name: 'step',
component: Step,
...
Now you can direct your app to routes like http://localhost:8080/step/5. The variable to.params.sid would match the 5 in that route. So, in the beforeEnter guard, check that the param matches the store's step.
...
beforeEnter: (to, from, next) => {
const nextStep = parseInt(to.params.sid); // convert string param to number
if (nextStep === store.state.step) { // is this the current allowed step?
next(); // if so, it's ok
} else { // if not, direct to that step
next({ name: 'step', params: { sid: store.state.step }})
}
}
},
Whenever a step is completed, you'll increment the store step and then direct the router to the next step.
Extra:
You could also change the if condition to allow for visiting previous steps:
if (nextStep > 0 && nextStep <= store.state.step) {
I had similar issue.
I think best solution is put steps into single route. It is easy to handle and hard to trick the logic.
But, if you must split steps into routes for some reasons (each step is so big to put in single route or ...), you can use step counter as global (above router-view).
I suggest two way to set global value.
Set value in App.vue (contains router-view) as data. Serve value as props to every routes. Each route checks step is valid and change url to valid step using $router.replace to prevent backward navigation. If step is valid for this url, process the logic and emit new step number before change url.
App.vue
<template>
<router-view :step="fooStep" #changeStep=changStep />
</template>
<script>
export default {
data () {
return {
fooStep: 0
}
},
methods: {
changeStep (value) {
this.step = value
}
}
}
SomeComponent.vue
<script>
export default {
name: 'Step3', /* optional */
props: ['step'],
mounted () {
/* some logic to check step is valid */
if (this.step !== 3) {
this.$router.replace(this.step > 0 ? '/step' + this.step : '/')
}
},
methods: {
submit () {
...
/* after submit data, send signal to App.vue to change step value */
this.$emit('changeStep', 4)
/* change url */
this.$router.replace('step4')
}
}
}
</script>
Second way is using Vuex the vue store. Vuex is good way to manage global value of vue app. My example is too brief that violate vuex rule. Find more information about Vuex. It is too long to write here.
SomeComponent.vue
<script>
export default {
name: 'Step3', /* optional */
mounted () {
/* some logic to check step is valid */
const step = this.$store.state.step
if (step !== 3) {
this.$router.replace(step > 0 ? '/step' + step : '/')
}
},
methods: {
submit () {
...
/* after submit data, store new step value in vuex */
/* change the value directly is not good */
this.$store.state.step = 4
/* change url */
this.$router.replace('step4')
}
}
}
</script>
Is it possible to check a condition before changing routes?
I have this function which listens to routes and logs everytime the app changes. I use it for debugging purposes but is it possible to check something like if localstorage contains a value, if not move to login page?
useEffect(() => {
history.listen(loc => {
console.log(`route: ${loc.pathname}`);
});
}, []);
Something like if (loc.pathname !== '/login') push to login. Within this history.listen function
I've tried using protected routes but I am getting an error where the graphql client loads the user in but if the token expires, it still loads the component. Then redirects.
Yes, you can do it using history.push method:
useEffect(() => {
history.listen((loc) => {
if (localStorage.getItem('someItem')) {
history.push('/login');
}
});
}, []);
Read about that method and other available methods you can read in documentation.
To change the current route, you can use the following line:
window.location = '<YOUR_ROUTE>';
So for example, if you configured your login route to be like:
<Route path="/login" component={Login} />
Then, in your useEffect function, you would just add the line
useEffect(() => {
history.listen(loc => {
console.log(`route: ${loc.pathname}`);
window.location = '/#/login'; // <-- this line here
});
}, []);
And if you need to check a condition first before redirecting to the login route, you'll juste need to put the window.location line within your if, like:
if (loc.pathname !== '/login') {
window.location = '/#/login';
}
When I am on my home page and click on a "lesson" component, the lesson page loads, takes the id of the lesson from the url and gets the data (from a local js file) to populate the lesson page. Well, that only happens when I click from the home page. But when I'm already on the lesson page with the data populated and I reload the page, I takes the id from the url but this time the data appear "undefined"... I don't understand why it doesn't take the data as previously?
Here is my component implementation:
const Lesson = () => {
const { id } = useParams();
const [lesson, setLesson] = useState(getLesson(id));
console.log("id: ", id);
console.log("lesson: ", lesson);
return (...);
};
Here is the console when I click on my lesson component from the home page:
the console when it works
There is the console when I simply reload the lesson page: the console when it doesn't work
I tried using useEffect() with a setLesson(getLesson(id)) inside but nothing changed...
I also tried this:
if (id) lesson = getLesson(id);
But again, it didn't work... :(
getLesson() gets its data from this file called fakeLessonsService.js:
import getHipHopLessons from "./hipHopLessons";
const lessons = [...getHipHopLessons()];
export function getLesson(lessonId) {
return lessons.find((lesson) => lesson._id === lessonId);
}
The file "./hipHopLessons" simply returns an array of lesson objects.
getLesson() is only loaded on this page.
The argument to useState is only used on initial render.
You should use a useEffect to update the state if the id changes
const [lesson, setLesson] = useState({});
useEffect(() => {
getLesson(id).then(val => setLesson(val));
}, [id]);
PS. Make sure getHipHopLessons is not asynchronous
If it is async, then you must write the code like
import getHipHopLessons from "./hipHopLessons";
export async function getLesson(lessonId) {
const lessons = getHipHopLessons();
return lessons.find((lesson) => lesson._id === lessonId);
}
Not sure what the issue is but my Navigo router is duplicating routes.
The Router:
this.Navigo.hooks({
before: (done, params) => {
// some tomfoolery
done();
}
});
this.Navigo.on({
'/:region/travel': (params) => {
// import Travel module
// some nonsense
},
'/:region/travel/car': (params) => {
// import TravelCar module
// some nonsense
}
)};
this.Navigo.resolve();
The Problem
this.Navigo.navigate('/london/travel/car');
Navigating to /london/travel/car is also triggering the route for /london/travel and thus causing all kinds of twaddle.
Is this standard behaviour? If not, what could be wrong?
I could rewrite the routes so they don't collide e.g. /london/travel-by-car, but I really don't want to if I can avoid it.
UPDATE 1:
I tried switching the order of routes but makes no difference. I did this by declaring the longest travel routes first, /:region/travel/car, and the smallest, /:region/travel, last.
UPDATE 2:
The more I look into this, the more I'm convinced this cannot be achieved with Navigo. Navigo do not support nested routes. If somebody could confirm that my routes are in fact 'nested', I will use an alternative routing library that does support them.
My code is a little different, but works the way you expect:
var router = new Navigo("/");
var render = (content) => (document.querySelector("#app").innerHTML = content);
router
.on('/:id', ({ data }) => {
setuserId(data.id)
if (verifiedUser) {
console.log("User verified");
} else {
console.log("User NOT verified");
}
rendertemplate(userDataURL(), "#landing-template", "#app")
})
.on('/:id/q', ({ data }) => {
// Example - flaging a send with 's' from 'SMS', perhaps a diff flow?
setuserId(data.id)
rendertemplate(userDataURL(), "#landing-template", "#app")
console.log("Source was a QRcode");
})
.on('/:id/q/t', ({ data }) => {
// Example - flaging a send with 's' from 'SMS', perhaps a diff flow?
setuserId(data.id)
rendertemplate(userDataURL(), "#landing-template", "#app")
console.log("Source was a QRcode in a Train");
})
This will give me a single discreet ".. verified"/"Source was a QRcode"/"Source was a QRcode in a Train" console.log response.
B
We are using a javascript framework(Not react) to render the ui.
main.js
function logout(){
someObj.lock($('#container'), 'Logging out', true);
document.location = app.context + `/${appName}/signout.action?name=${appName}`;
}
function action(event){
switch(event.target){
case 'user:logout':
logout();
break;
case 'user:application':
document.location = app.context + "/";
break;
}
}
module.exports = {
action: action,
logout: logout
}
main.js along with another js file renders a navbar and a dropdown. My intention is to check whether title, dropdown in the navbar is rendered. Also I
am testing whether the browser redirect takes place in the right way.
action method takes an event object and based on its type, either performs signout('user:logout') or redirects to application page('user:application').
tests/main.js
import main from '../main';
describe("some title", () => {
it("some behavior", () => {
let event = {
target: 'user:logout'
}
let app = {
context: ''
}
let appName = 'Some app';
main.logout = jest.fn();
someObj = jest.fn();
someObj.lock = jest.fn();
document.location.assign = jest.fn();
main.action(event);
expect(main.logout).toHaveBeenCalled();
expect(document.location.assign).toBeCalledWith(app.context + `/${appName}/signout.action?name=${appName}`);
})
});
In the test file, I am trying to mock logout function. However it is executing someObj.lock function. someObj is not availabe to tests/main.js
and I am mocking it as well. I'm not sure whether I have to use spyOn instead. I'm using document.location.assign to test for browser redirects.
This test is not working and the terminal is displaying TypeError: Could not parse "/application name/signout.action?name=application name" as URL.
I have spent an entire day testing this feature but to no avail. I need some advice on the best way to test this feature.
Links explored: Intercept navigation change with jest.js (or how to override and restore location.href)
jest documentation