Vue: route navigation with router.resolve (to open a page in new tab) with data in params is lost - javascript

I have a component, lets call component 1. A method in component1 makes an axios.post request and the server returns a bunch of data. When data is loaded, a new button appears. When this button is clicked, it will be navigated to another route with another component, let call this component2. Now some of the loaded data from component1 needs to transferred to component2 and should be opened in new tab. Below is the code:
<script>
import axios from 'axios';
export default {
name: "CheckStandard",
data() {
return {
standard: '',
time: {},
programs: {},
example: {},
}
},
methods: {
checkData(){
let std= {
std: this.standard,
}
axios.post('http://localhost:3000/postdata', std)
.then(res => {
if (res.status === 200) {
if (res.data === 0) {
this.invalidID = "This Standard does not exist"
}
else {
let data = res.data
this.time = res.data["Starttime"];
this.programs = res.data["program"]
this.example = res.data["example"]
}
}).catch(error => {
console.log(error);
this.error = error.response
})
},
}
goToPictures(){
let route = this.$router.resolve({
name:'ProgramCheckList',
params: {
programs: this.programs,
time: this.time,
example: this.example
}
})
window.open(route.href,'_blank')
},
}
}
</script>
The function goToPictures is the function that is invoked after clicking the button. Now in this function goToPictures I have defined the route to navigate to another tab. But the problem the data in the params which it should carry is lost. I tried with $router.push ofcourse it works but it is not to open in new tab. Below is the code for the same:
goToPictures(){
this.$router.resolve({
name:'ProgramCheckList',
params: {
programs: this.programs,
time: this.time,
example: this.example
}
})
},
}
Since I am new to vue, I have tried my best to look for an answer for this, even I have came across some posts in several forums mentioning, it is may be not be possible even, instead advised to use vuex. But I still wanted to post it, maybe we have a solution now or any other idea. Thanks

The problem you're seeing stems from the fact that, when you open a new window, Vue is basically going to re-render your components as if you hit refresh. Your Component 2 has props that it can only inherit from another component, and as such, it has no possible way of knowing what the props it needs to use are.
To illustrate in simple terms what's happening:
The user navigates to Component 1. They click the button, which makes the GET request. You now have some data that you can pass onto Component 2 as props.
In a regular environment, the user would simply click on the link leading to Component 2, and the props would be passed on normally, and everything would work as intended.
The problem in your situation is that Component 2 depends on Component 1 for its data. By navigating directly to the Component 2 route (in this situation, opening a new window is functionally identical to a user copy/pasting the url into the adress bar), Vue never has the chance of interacting with Component 1, and never gets told where to get the props it needs to populate Component 2.
Overcoming the issue
There's a few things you can do here to overcome this issue. The most obvious one is to simply open Component 2 as you would normally, without opening a new window, but keep in mind that even if you do this, should a user copy/paste the URL where Component 2 is, they'll run into the exact same issue.
To properly deal with the issue, you have to specify a way for Component 2 to grab the data it needs. Since the data is already fetched, it makes sense to do this in the created() or mounted() hooks, though if you wanted to you could also deal with this in Vue Router's beforeRouteEnter() hook.
Vuex
While you don't necessarily need a state management tool like Vuex, it's probably the simplest way for your needs. When you grab the data from Component 1, store it and access it from the Component 2 mounted() hook. Easy-peasy.
localStorage()
Alternatively, depending on how the data is being served, you could use localStorage(). Since you're opening a new window, sessionStorage() won't work. Do note that localStorage() can only hold strings and nothing else, and isn't necessarily available in every browser.

You can store the data to a global variable and use that in the newly opened window. Asked here Can I pass a JavaScript variable to another browser window?
Provided the windows are from the same security domain, and you have a reference to the other window, yes.
Javascript's open() method returns a reference to the window created (or existing window if it reuses an existing one). Each window created in such a way gets a property applied to it "window.opener" pointing to the window which created it.
Either can then use the DOM (security depending) to access properties of the other one, or its documents,frames etc.
Another example from same thread:
var thisIsAnObject = {foo:'bar'};
var w = window.open("http://example.com");
w.myVariable = thisIsAnObject;
//or this from the new window
var myVariable = window.opener.thisIsAnObject;

Related

Router.push or Link not rendering/refreshing the page even thought the url is updated nextjs

I apologize for my horrendous way of explaining my issue. I have shared a link below description which is exactly the same issue I am experiencing. Any kind of help would be greatly appreciated.
I have directory path like pages/request/[reqid].js . When my url is localhost:3000/xyz and I navigate to pages/request/1 by clicking a button on the current page, the page successfully loads the page with proper data from [reqid=1] but when I try to access pages/request/[reqid].js with different reqid (say suppose reqid=2), the url reflects the correct the reqid pages/request/2 but the page remains the same, doesn't change. However if I go back to other pages like localhost:3000/xyz and click a button there to navigate to pages/request/2 it works but from within pages/request/[reqid] it doesn't render a page associated to the corresponding reqid even thought the url is updated. I have tried both Link and router.push ,both fails to render the correct reqid page.
https://github.com/vercel/next.js/issues/26270
It actually failed to include that I was using getServerProps to fetch the data, which was the reason the page wasn't rendering , unless the page was manually refreshed. The page state is not reset for navigation between dynamic routes that served by the same source component.
for example, give page source /a/[param]/index.js, when navigating from /test/123 to /test/124, states on the page wasn't being reset.
So actually happened is the same React Component been rendered with different props. Thus react takes it as a component is rerendering itself, and causing the new navigated page receive stale states.
To fix it, just add {key: } to page initial props or getserversideprops
export const getServerSideProps = async (ctx) => {
try {
const { reqid } = ctx.params;
//fetch code
return {
props: {
key: reqid,
data:data
},
};
} catch (error) {
console.log(error);
}
};

Vue child component not displaying dynamic data on first page load

Given the code below, my child component alerts trigger before any of the code in the Parent mounted function.
As a result it appears the child has already finished initialization before the data is ready and therefor won't display the data until it is reloaded.
The data itself comes back fine from the API as the raw JSON displays inside the v-card in the layout.
My question is how can I make sure the data requested in the Parent is ready BEFORE the child component loads? Anything I have found focuses on static data passed in using props, but it seems this completely fails when the data must be fetched first.
Inside the mounted() of the Parent I have this code which is retrieves the data.
const promisesArray = [this.loadPrivate(),this.loadPublic()]
await Promise.all(promisesArray).then(() => {
console.log('DATA ...') // fires after the log in Notes component
this.checkAttendanceForPreviousTwoWeeks().then(()=>{
this.getCurrentParticipants().then((results) => {
this.currentP = results
this.notesArr = this.notes // see getter below
})
The getter that retrieves the data in the parent
get notes() {
const newNotes = eventsModule.getNotes
return newNotes
}
My component in the parent template:
<v-card light elevation="">
{{ notes }} // Raw JSON displays correctly here
// Passing the dynamic data to the component via prop
<Notes v-if="notes.length" :notesArr="notes"/>
</v-card>
The Child component:
...
// Pickingn up prop passed to child
#Prop({ type: Array, required: true })
notesArr!: object[]
constructor()
{
super();
alert(`Notes : ${this.notesArr}`) // nothing here
this.getNotes(this.notesArr)
}
async getNotes(eventNotes){
// THIS ALERT FIRES BEFORE PROMISES IN PARENT ARE COMPLETED
alert(`Notes.getNotes CALL.. ${eventNotes}`) // eventNotes = undefined
this.eventChanges = await eventNotes.map(note => {
return {
eventInfo: {
name: note.name,
group: note.groupNo || null,
date: note.displayDate,
},
note: note.noteToPresenter
}
})
}
...
I am relatively new to Vue so forgive me if I am overlooking something basic. I have been trying to fix it for a couple of days now and can't figure it out so any help is much appreciated!
If you are new to Vue, I really recommend reading the entire documentation of it and the tools you are using - vue-class-component (which is Vue plugin adding API for declaring Vue components as classes)
Caveats of Class Component - Always use lifecycle hooks instead of constructor
So instead of using constructor() you should move your code to created() lifecycle hook
This should be enough to fix your code in this case BUT only because the usage of the Notes component is guarded by v-if="notes.length" in the Parent - the component will get created only after notes is not empty array
This is not enough in many cases!
created() lifecycle hook (and data() function/hook) is executed only once for each component. The code inside is one time initialization. So when/if parent component changes the content of notesArr prop (sometimes in the future), the eventChanges will not get updated. Even if you know that parent will never update the prop, note that for performance reasons Vue tend to reuse existing component instances when possible when rendering lists with v-for or switching between components of the same type with v-if/v-else - instead of destroying existing and creating new components, Vue just updates the props. App suddenly looks broken for no reason...
This is a mistake many unexperienced users do. You can find many questions here on SO like "my component is not reactive" or "how to force my component re-render" with many answers suggesting using :key hack or using a watcher ....which sometimes work but is almost always much more complicated then the right solution
Right solution is to write your components (if you can - sometimes it is not possible) as pure components (article is for React but the principles still apply). Very important tool for achieving this in Vue are computed propeties
So instead of introducing eventChanges data property (which might or might not be reactive - this is not clear from your code), you should make it computed property which is using notesArr prop directly:
get eventChanges() {
return this.notesArr.map(note => {
return {
eventInfo: {
name: note.name,
group: note.groupNo || null,
date: note.displayDate,
},
note: note.noteToPresenter
}
})
}
Now whenever notesArr prop is changed by the parent, eventChanges is updated and the component will re-render
Notes:
You are overusing async. Your getNotes function does not execute any asynchronous code so just remove it.
also do not mix async and then - it is confusing
Either:
const promisesArray = [this.loadPrivate(),this.loadPublic()]
await Promise.all(promisesArray)
await this.checkAttendanceForPreviousTwoWeeks()
const results = await this.getCurrentParticipants()
this.currentP = results
this.notesArr = this.notes
or:
const promisesArray = [this.loadPrivate(),this.loadPublic()]
Promise.all(promisesArray)
.then(() => this.checkAttendanceForPreviousTwoWeeks())
.then(() => this.getCurrentParticipants())
.then((results) => {
this.currentP = results
this.notesArr = this.notes
})
Great learning resource

Can Relay work properly with Next.js SSR?

I have started learning Relay by writing a new Next.js application. I have so far been following the with-relay-modern example in the Next.js repo, and it has been working just fine for fetching data from the server. However, I have now moved beyond that example by adding a mutation, and things immediately stopped working.
The mutation updater looks like this:
function updateStore(
store: RecordSourceSelectorProxy,
formInstanceUuid: string,
) {
const mutation = store.getRootField("updateFormValue");
const newFormValue = mutation?.getLinkedRecord("formValue");
if (!newFormValue) {
throw "Expected new form value from server";
}
const localFormInstance = store.get(formInstanceUuid)
const localValueRecords = localFormInstance?.getLinkedRecords("values") || [];
for (const record of localValueRecords) {
if (record.getValue("partUuid") == newFormValue.getValue("partUuid")) {
console.log("Copying fields from server provided value to local value");
record.copyFieldsFrom(newFormValue);
}
}
console.debug("New state of store:", initEnvironment().getStore().getSource())
}
All it does is inject a "value" object returned from the server into a list of value objects in a local "form" object. As you can see I am dumping the state of the store on the last line, so I can confirm that the mutation worked as expected, and the local value was modified as expected.
However, the UI doesn't refresh. I have to reload the window to see the new state.
I can't for the life of me figure out what I've done wrong, so I'm starting to wonder if the example I was following only works for fetching data. I assume it's the QueryRenderer object that is responsible for refreshing the UI when the underlying store changes, and the example doesn't use one. I also can't imagine how a QueryRenderer could be added to the example without ruining SSR.
TL;DR
Does the "with-relay-modern" example work when adding mutations, or is my issue somewhere else?
I also started a Discussion on the Next.js GitHub page about this

how to emulate messages/events with react useState and useContext?

I'm creating a react app with useState and useContext for state management. So far this worked like a charm, but now I've come across a feature that needs something like an event:
Let's say there is a ContentPage which renders a lot of content pieces. The user can scroll through this and read the content.
And there's also a BookmarkPage. Clicking on a bookmark opens the ContentPage and scrolls to the corresponding piece of content.
This scrolling to content is a one-time action. Ideally, I would like to have an event listener in my ContentPage that consumes ScrollTo(item) events. But react pretty much prevents all use of events. DOM events can't be caught in the virtual dom and it's not possible to create custom synthetic events.
Also, the command "open up content piece XYZ" can come from many parts in the component tree (the example doesn't completely fit what I'm trying to implement). An event that just bubbles up the tree wouldn't solve the problem.
So I guess the react way is to somehow represent this event with the app state?
I have a workaround solution but it's hacky and has a problem (which is why I'm posting this question):
export interface MessageQueue{
messages: number[],
push:(num: number)=>void,
pop:()=>number
}
const defaultMessageQueue{
messages:[],
push: (num:number) => {throw new Error("don't use default");},
pop: () => {throw new Error("don't use default");}
}
export const MessageQueueContext = React.createContext<MessageQueue>(defaultMessageQueue);
In the component I'm providing this with:
const [messages, setmessages] = useState<number[]>([]);
//...
<MessageQueueContext.Provider value={{
messages: messages,
push:(num:number)=>{
setmessages([...messages, num]);
},
pop:()=>{
if(messages.length==0)return;
const message = messages[-1];
setmessages([...messages.slice(0, -1)]);
return message;
}
}}>
Now any component that needs to send or receive messages can use the Context.
Pushing a message works as expected. The Context changes and all components that use it re-render.
But popping a message also changes the context and also causes a re-render. This second re-render is wasted since there is no reason to do it.
Is there a clean way to implement actions/messages/events in a codebase that does state management with useState and useContext?
Since you're using routing in Ionic's router (React-Router), and you navigate between two pages, you can use the URL to pass params to the page:
Define the route to have an optional path param. Something like content-page/:section?
In the ContentPage, get the param (section) using React Router's useParams. Create a useEffect with section as the only changing dependency only. On first render (or if section changes) the scroll code would be called.
const { section } = useParams();
useEffect(() => {
// the code to jump to the section
}, [section]);
I am not sure why can't you use document.dispatchEvent(new CustomEvent()) with an associated eventListener.
Also if it's a matter of scrolling you can scrollIntoView using refs

How much of this business logic belongs in Vuex?

I have a simple app which pulls products from an API and displays them on-page, like this:
I've added Vuex to the app so that the search results as well as the product search array doesn't disappear when the router moves the user to a specific product page.
The search itself consists of the following steps:
show loading spinner (update the store object)
dispatch an action to access the API
update the store object with products, spinner
decide if the product list is exhausted
hide loading spinner
You get the idea.
With all of the variables stored in Vuex, it stands to reason all of the business logic should belong there as well, but should it really?
I'm talking specifically about accessing store params such as productsExhausted (when there are no more products to display) or productPage (which increments every time the infinite scroller module is triggered) etc.
How much logic - and what kind - belongs in Vuex? How much does not?
I was under the impression that Vuex is used for storage only but since all of the data is located there, fetching it all back to the Vue app only to send it all back seems like an overly verbose way to address the problem.
Vuex allows you to share data !
For everything that concerns the state of the app its pretty straightforward.
All the data that can be used by multiple components should be added
to the store.
Now concerning the business logic, even though I find its not really clear in the official documentation, it should follow the same principle.
What I mean is that logic that can be used by multiple components should be stored in actions.
Moreover actions allows you to deal with async operations. Knowing this, your code that pulls the data should definitely be stored in vuex's actions.
What I think you should do is to put the request inside an action, then mutate the state of your variables and automatically your UI will reflect the changes.
Moreover, a good pattern to apply is to convert most of the logic to a state logic. For instance consider this demo of a jumping snowman. In here the click action results on updating a value from the store. Although the interesting part is that one component uses the watch functionnality to be notified when the store changes. This way we keep the logic inside the component but use the store as an event emitter.
var store = new Vuex.Store({
state: {
isJumping: 0
},
mutations: {
jump: function(state){
state.isJumping++;
}
}
})
Vue.component('snowman', {
template: '<div id="snowman" :class="color">⛄</div>',
computed: {
isJumping: function(){
return this.$store.state.isJumping;
}
},
watch: {
isJumping: function(){
var tl = new TimelineMax();
tl.set(this.$el,{'top':'100px'})
tl.to(this.$el, 0.2, {'top':'50px'});
tl.to(this.$el, 0.5, {'top':'100px', ease: Bounce.easeOut});
}
}
})

Categories