Why is my Stimulus JS controller firing twice? - turbolinks

So, I have a Rails application with webpacker, vue, turbolinks and stimulus js installed.
The problem that I am having is that even though the controller is only imported once and even if I temporarily disable turbolinks the initialize() funcition along with the connect() one get called twice.
This only happens if I do a refresh (i.e. not when I visit the page for the first time, but only if I perform a page reload).
Strangely enough the disconnect() is only getting called once (when I do leave the page)
This sucks because I need to modify the DOM in the initialize, so I get elements added twice.
Does someone have any clue to what's causing this, and / or a solution?
EDIT: application.js as requested
require("#rails/ujs").start()
require("turbolinks").start()
require("#rails/activestorage").start()
require("channels")
import "stylesheets"
import "controllers"
import "components"
components/index.js
import Vue from 'vue/dist/vue.esm'
import BootstrapVue from 'bootstrap-vue'
import TurbolinksAdapter from 'vue-turbolinks'
Vue.use(BootstrapVue)
Vue.use(TurbolinksAdapter)
const components = {}
const context = require.context("components", true, /_component\.vue$/)
context.keys().forEach(filename => {
const component_name = filename.replace(/^.*[\\\/]/, '').replace(/_component\.vue$/, '')
const component = context(filename).default
components[component_name] = component
})
document.addEventListener('turbolinks:load', () => {
const app = new Vue({
el: '#vueapp',
mounted() {
let input = document.querySelector('[autofocus]');
if (input) {
input.focus()
}
},
components: { ...components }
})
})
controllers/index.js
import { Application } from "stimulus"
import { definitionsFromContext } from "stimulus/webpack-helpers"
const application = Application.start()
const context = require.context("controllers", true, /_controller\.js$/)
application.load(definitionsFromContext(context))
in each and every file, if I console.log it gets logged only once...
only in the stimulus controller initialize or connect gets printed twice
this happens only when I reload the page, not when I first visit it.

This is probably due to Turbolinks. When navigating to a new page, Turbolinks first presents a cached version of the page (first stimulus connect) and afterwards replaces the body with the actual requested version of the page (second stimulus connect). See here for more details:
https://mrcodebot.com/turbolinks-calls-stimulus-connect-twice/

Related

nextjs reload page with Link component

I have a simple nextjs 13 application (appDir) with 2 pages and a <Link/> component navigation.
first page Home - static
second page Test - receiving dynamic random content on the server side (fetch) from a third-party source.
Problem: When the application is rendered everything works fine, but when I try to switch between pages, my test page shows the old content, I can refresh the browser to get the actual data, is very similar to navigating through regular links <a/>, but i need without reloading the entire application.
Q: How can I force nextjs 13 to reload the Test page when I switch between pages with <Link/> component?
// src/components/navbar.tsx
'use client'
import {usePathname} from "next/navigation";
import Link from "next/link";
const navItems = [
{text: 'Home', href: '/'},
{text: 'Test', href: '/test'}
];
const Navbar = () => {
const pathname = usePathname();
return <nav className="nav nav-masthead justify-content-center float-md-end">
{navItems.map((item: { text: string, href: string, link?: boolean }, idx: number) => (
<Link key={idx} href={item.href} className={`nav-link${item.href === pathname ? ' active' : ''}`}>
{item.text}
</Link>
)
)}
</nav>
}
export default Navbar;
// src/app/test/page.tsx
import * as crypto from "crypto";
const getData = async () => {
const res = await fetch('http://localhost:3000/random-data', {cache: 'no-store'});
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return res.json();
}
export default async function Page() {
return <p>{crypto.createHash('sha256').update(JSON.stringify(await getData())).digest('hex')}</p>
};
I've recently asked about this same topic on their feedback discussion on github: https://github.com/vercel/next.js/discussions/41745?sort=new#discussioncomment-4620262
The cause of the problem is Link is only doing client side navigation and seems to serve a cached state of the previously visited component. You'll notice that the client never calls back to the server and thus the server component never runs the second time.
I've been searching for days, but haven't found a way to force Link to reload or force refresh the component to re-render.
My conclusion is that if you have dynamic data that needs to refreshed periodically, it's best to render it in a client component and not use a server component for now.
Also, if you'd like to use Suspense, you'll need to use a library like SWR or React Query for any client side data fetching.

VurRouter transitions on more than one router

I prepared a boiled-down example on stackblitz:
https://stackblitz.com/edit/quasarframework-vy4eiw?file=README.md
The problem I try to resolve is this:
A quasar 2 app build with vite and vue 3 (and GSAP) uses layouts
Currently there are 2 layouts: StartpageLayout for the startpage at route ´/´and MainpageLayout for all the other pages at route ´/main´ and any children of it (/main/:child´`)
The MainpageLayout also contains the navigation menu
The navigation menu should be created (later on with an animation) when any route starting with ´/main´ is hit and destroyed, when there is a change to any other route
While navigating through any ´/main[/:child]´ route, the nav menu shall remain "stable" (not rebuild or anything like that)
The app uses 2 router-views for this, one in App.vue, one in MainLayout.vue. Changes between those states should mainly be handled in onBeforeRouteLeave and onBeforeRouteUpdate
To check, whether the app is in a "layout context", the routes have a meta.layoutKey, which is used in router guards to check, whether sth changed or not:
// Example: src/layouts/MainLayout.vue
onBeforeRouteUpdate((to, from, next) => {
console.log('%cMAIN_LAYOUT: onBeforeRouteUpdate invoked', consColRouter);
// compare meta.layoutKeys of routes
if (from.meta.layoutKey !== to.meta.layoutKey) {
console.warn(' YES, invoke router guard onBeforeRouteUpdate - animate!');
next() // this would be actually be called form an onComplete animation callback
} else {
console.log(' NOPE, do not invoke router guard onBeforeRouteUpdate');
next() // invoked as written
}
})
A pinia store manages state that (should) remember(s) activateMenu:
// Pinia store "pageTransitions.js" (composition API)
import { ref, reactive, computed } from 'vue'
import { defineStore } from 'pinia'
export const usePageTransitionsStore = defineStore('pageTransitions', () => {
// Pinia state in composition API
const pageTransitions = ref({
parent: false,
activateMenu: false
})
const setPageTransitions = (level, bool) => {
switch(level) {
case 'parent': {
pageTransitions.value.parent = bool
break
}
default: { console.log('default... must never happen!'); }
}
}
const setActivateMenu = (bool) => {
pageTransitions.value.activateMenu = bool
}
return {
pageTransitions,
setPageTransitions,
setActivateMenu
}
})
If store.pageTransitions.activateMenu is true, show the menu, if false, remove it. It is imported in MainLayout in order to use the activateMenu constant to manage the state of the nav menu. The onMount method sets this store variable to true. And it should be set to false in a ònBeforeRouteLeave`... (not yet implemented)
While the change from the startpage at ´/´to the MainPage at ´/main´ and vice versa works fine (even with animation, due to the store variable store.pageTransitions.parent), I keep having troubles with changes from ´/main´ to any child route ´/main/:child´ and vice versa. E.g. when the app is at /main and the user clicks on ´items 101´, the whole MainLayout is reloaded - also App.vue runs through its onAppear hooks again (see console) – and the nav is set to false again.
The goal is to not influence the MainLayout not its nested nav menu at all.
I wonder, why those reloads happen? MainLayout's onBeforeRoute checks against meta.layoutKey which does not change. But then I also observe that the pinia store gets loaded again, and the actiavteMenu var is set up false again...
Does anybody see my error(s)?

Error: Minified React error #321 on Wordpress custom block plugin

Whenever I write a useEffect() inside a component function of my block plugin, the edit page goes blank and the console logs the message:
react_devtools_backend.js:4026 Error: Minified React error #321; visit https://reactjs.org/docs/error-decoder.html?invariant=321 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
at Object.it (react-dom.min.js?ver=17.0.1:9:43163)
at e.useState (react.min.js?ver=17.0.1:9:10899)
at Prompt (Prompt.js:5:35)
at N (element.min.js?ver=3dfdc75a0abf30f057df44e9a39abe5b:2:9552)
at U (element.min.js?ver=3dfdc75a0abf30f057df44e9a39abe5b:2:10502)
at N (element.min.js?ver=3dfdc75a0abf30f057df44e9a39abe5b:2:9284)
at lr (blocks.min.js?ver=658a51e7220626e26a92a46af5c2e489:3:111294)
at blocks.min.js?ver=658a51e7220626e26a92a46af5c2e489:3:137935
at xn (blocks.min.js?ver=658a51e7220626e26a92a46af5c2e489:3:138073)
at blocks.min.js?ver=658a51e7220626e26a92a46af5c2e489:3:139086
The component:
import React, { useState, useEffect } from "react";
import axios from "axios";
function Prompt(props) {
const [data, setData] = useState({ hits: [] });
useEffect(() => {
const fetchData = async () => {
const result = await axios(
"http://my-site-test.local/wp-json/wp/v2/posts?_fields[]=title"
);
setData(result.data);
};
fetchData();
}, []);
console.log(data);
return (
<>
JSX...
</>
);
}
export default Prompt;
I tried to delete node_modules and reinstall to no avail…
I believe the problem is in my-plugin/src/index.js — wp.blocks.registerBlockType's 'save' property only allows static HTML to be returned (so it can be stored in the database within the content) and I was trying to insert a React component into it.
Since I want a dynamic block on the front-end, I have to load a register_block_type in my-plugin/index.php to render my component.
EDIT You actually can add React directly in the save attribute if you have specified script when registering your block in the PHP main file (or in your block.json file.

Next.js dynamic import with custom loading component: "Warning: Expected server HTML to contain a matching <em> in <div>."

I have a next.js page with dynamic imports, which worked well until now,
but since I started using the custom loading components I get the warning
Warning: Expected server HTML to contain a matching <em> in <div>.
What can I do to avoid this warning ?
Here a reduced example:
// pages/index.tsx:
import React from 'react';
import dynamic from 'next/dynamic';
import { LoadingComponent } from '../src/LoadingComponent';
// -- The custom loading fallback component
const loadingFallback = { loading: LoadingComponent };
// -- The dynamically loaded component
const DynamicallyLoadedComponent = dynamic(
() => import('../src/DynamicallyLoadedComponent'),
loadingFallback
);
// -- The main component
function Main(){
return <DynamicallyLoadedComponent />;
}
export default Main;
// src/DynamicallyLoadedComponent.tsx:
export default function DynamicallyLoadedComponent(){
return <div>ready loaded, final page content.</div>;
};
// src/LoadingComponent.tsx:
export const LoadingComponent = () => {
return <em>... loading ...</em>;
}
Apparently you can not extract the part { loading: LoadingComponent } into a variable, so it has to be written
inline, e.g.:
const DynamicallyLoadedComponent = dynamic(
() => import('../src/DynamicallyLoadedComponent'),
{ loading: LoadingComponent } // <-- needs to be written like this, a variable can't be used
);
Maybe this is because Next.js analyses the code in a kind of "pre-compiling" step and expects to find exactly this pattern (?).
Probably this is the same requirement as mentioned under Basic usage regarding the dynamic import itself:
Note: In import('path/to/component'), the path must be explicitly written.
It can't be a template string nor a variable.
Furthermore the import() has to be inside the dynamic() call for Next.js to be able to match
webpack bundles / module ids to the specific dynamic() call and preload them before rendering.
dynamic() can't be used inside of React rendering as it needs to be marked in the top level
of the module for preloading to work, similar to React.lazy.

window not defined while import AgoraRTC from 'agora-rtc-sdk-ng' in nextjs

I get an error window is undefined while importing agora.io.
Kindly import it like shown below
const AgoraRTC = (await import('agora-rtc-sdk-ng')).default
}
AGORA uses "window" object which is a client side object and Next JS renders on server side. So You need to use "dynamic" for client side rendering. Via Dynamic you can import the file on client side so it will work.
You should copy your whole Agora code and put it in a file.
For example in my case I put it in "../components/agoraclientside.js"
So in my index.js file I wrote :
import dynamic from "next/dynamic";
const ClientSideControls = dynamic(
() => {
return import("../components/agoraclientside");
},
{ ssr: false }
);
Then in your return function use <ClientSideControls />

Resources