How to call `useNavigate` outside of router

I am trying to create my own "page viewer" where I will be able to navigate between pages within the site; Currently I am using the Solid Router. But I want to be able to call the "window" to navigate from outside of the router. But I can't do that. I get the following error when trying to call useNavigate
Uncaught Error: <A> and 'use' router primitives can be only used inside a Route.
Uncaught Error: <A> and 'use' router primitives can be only used inside a Route.
I want to be able to do the following outside of the router: 1. Go back 2. Go forwards 3. Reload 4. Navigate 5. Clear navigation history So far I know for going forwards and backwards I can use history.back. But I cant refresh using location.reload because the window is a dialog so reloading is going to close the dialog. And the other options I don't really know what I can do. I tried to get the Navigate function by using the Navigate component and then taking the function from the parameter passed in the href e.g.
let navigate : Navigate | undefined;

const doStuff = ({n: Navigate}) => {
navigate = n;
return "/404"
}

return (
<Router>
<Route path={"/"} component={() => <Navigate href={doStuff}></Navigate>}></Route>
</Router>
let navigate : Navigate | undefined;

const doStuff = ({n: Navigate}) => {
navigate = n;
return "/404"
}

return (
<Router>
<Route path={"/"} component={() => <Navigate href={doStuff}></Navigate>}></Route>
</Router>
But that still causes errors and is ugly code. Any suggestions on what I can do? Thanks
No description
32 Replies
bigmistqke
bigmistqke2mo ago
Any specific reason why you want it outside the router? Most code i have seen has the router at the root.
eatingtheham
eatingtheham2mo ago
The router isnt at the root because my "site" is being run with tauri. So because it is a desktop app, I dont want to navigate the whole page. Just a section of it.
peerreynders
peerreynders2mo ago
If this was my problem I'd look into crafting a Router root component that subscribes to a NavigationSubject that code outside of Router can dispatch navigation events to.
eatingtheham
eatingtheham2mo ago
How would I do the subscribing and observing?? Did some research and think I can use createContext but I am not too sure.
peerreynders
peerreynders2mo ago
Something a lot more basic than that, your own vanilla JavaScript module. Here's an example that I used: https://github.com/peerreynders/nitro-sse-counter/blob/6f27d5d7bd9e119ca2fc1feb7cbe70f9308b84b6/src/client/lib/sinks.js When you create a Sinks<T> instance you can hand around it's send property (i.e. it's not method) to anyone who wants to dispatch T values, while you can pass around the add property, so that anyone can be notified of those values. So the root component would add itself listening for navigation events send (sent) from the outside. TC39: “There is an interest in an Observable but no one wants to deal with which one” So everybody keeps creating their own …
GitHub
nitro-sse-counter/src/client/lib/sinks.js at 6f27d5d7bd9e119ca2fc1f...
SSE POC with UnJS Nitro and a TS JSDoc frontend. Contribute to peerreynders/nitro-sse-counter development by creating an account on GitHub.
GitHub
notes/meetings/2017-05/may-25.md at 57e6c4ef05d868129733ce31f200d68...
TC39 meeting notes. Contribute to tc39/notes development by creating an account on GitHub.
eatingtheham
eatingtheham2mo ago
Hmm. This seems a bit complex for me right now... Thanks though. The whole process of observing is what I don't understand I think.
Brendonovich
Brendonovich2mo ago
may i ask why running in tauri impacts whether the router is at the root? making your whole app be controlled by the one router seems like it'd be easier
eatingtheham
eatingtheham2mo ago
Yeh. I could have it be multiple pages where I do navigation. But the design of my app is that there is a page-viewer which would load different pages. And it will have its own navigation. Then I would be able to close the viewer to access the homescreen. It is a bit complicated but that is what I thought would be cool to make i guess. I could just make my own router sort of. Just don't know how to "attach" methods to a component to call stuff for it. (Just a way I could be doing the routing)
Brendonovich
Brendonovich2mo ago
At a first glance I'd implement that with /home and /page/{page id} routes And then under /page/{page id} you can have all the page viewer's routes
eatingtheham
eatingtheham2mo ago
That is still involving multiple pages right?
eatingtheham
eatingtheham2mo ago
Here is my current layout
No description
Brendonovich
Brendonovich2mo ago
i assume each page has its own id, you'd just swap out the page id in the url and be viewing a different page Oh interesting so it's more of a modal view than a dedicated route?
eatingtheham
eatingtheham2mo ago
Yeh. But wouldn't that mean for each page I need to recreate the sidebar and stuff Yeh it is
Brendonovich
Brendonovich2mo ago
Nah the sidebar and things would live in a layout that exists at /page yeah this makes things a bit more interesting
eatingtheham
eatingtheham2mo ago
And /home i guess too.
Brendonovich
Brendonovich2mo ago
yeah in that case the layout can be at /
eatingtheham
eatingtheham2mo ago
ok... I still don't want to create multiple paths. Because at the home screen is a bunch of data that is taken from a config file. And I don't want to reload that information.
Brendonovich
Brendonovich2mo ago
if u fetch the data in a shared layout and the layout doesn't unmount then the information doesn't have to reload but the modal-ness of this makes it interesting i'm guessing the base view contains all the pages and then clicking on one opens the page viewer modal?
eatingtheham
eatingtheham2mo ago
Yeh. And the sidebar directs to pages as well. And... some pages navigate to other pages lol
Brendonovich
Brendonovich2mo ago
but the modal always appears on top of the existing route rather than completely replacing it yeah?
eatingtheham
eatingtheham2mo ago
yeah I am using a dialog to open and close it
Brendonovich
Brendonovich2mo ago
yeah that's a really intricate design, i think for that i'd store the dialog state in search params and roll my own mini router since it just doesn't fit with normal route nesting
eatingtheham
eatingtheham2mo ago
Yeh a "mini router" that is what I was thinking to make
Brendonovich
Brendonovich2mo ago
a json object in search params and some switch/match cases would do the trick
eatingtheham
eatingtheham2mo ago
why in the search params? I could store it as a global variable right...?
Brendonovich
Brendonovich2mo ago
yeah u could, but search params are also global variables... i just like keeping stuff in the url where possible web dev mentality
eatingtheham
eatingtheham2mo ago
Yeh that makes sense. And if it were a website that is how I would do it as well. But considering that this is a site with no url bar then it shouldnt be needed
Brendonovich
Brendonovich2mo ago
actually even for tauri apps counting on url state can be pretty useful for if u need to restore a user's session - just swap out the url once you've got state outside the url you need extra ways of persisting and loading it
eatingtheham
eatingtheham2mo ago
True. I don't want the state to be saved if the app just launches. But definatly when I restore the window after it came from the tray But im not sure if tauri saves the window state if it goes to the tray Electron does I think... so idk I might try creating a mini router component with data from a global signal Ok @Brendonovich, I have made it. Havent tested navigation yet. But have something... might not be the best way to do it though.
import {Accessor, Component, createMemo, createSignal, ValidComponent} from "solid-js";
import SolidJSX from "solid-js/types/jsx";
import {Dynamic} from "solid-js/web";

function RouterViewer(props: { element: ValidComponent, props?: {}}) {
return (
<Dynamic component={props.element} {...props}/>
)
}

interface RouterComponent {
element: ValidComponent;
props?: Record<string, any>;
}

interface MiniRouterProps {
paths: Record<string, RouterComponent>,
invalid?: ValidComponent,
currentPath?: string,
}
import {Accessor, Component, createMemo, createSignal, ValidComponent} from "solid-js";
import SolidJSX from "solid-js/types/jsx";
import {Dynamic} from "solid-js/web";

function RouterViewer(props: { element: ValidComponent, props?: {}}) {
return (
<Dynamic component={props.element} {...props}/>
)
}

interface RouterComponent {
element: ValidComponent;
props?: Record<string, any>;
}

interface MiniRouterProps {
paths: Record<string, RouterComponent>,
invalid?: ValidComponent,
currentPath?: string,
}
class MiniRouter {
router: SolidJSX.JSX.Element;
paths: Record<string, RouterComponent>;
currentPath: {set: (value: string) => void, get: Accessor<string>};
currentPathProps?: Record<string, any>;
history: {
past: { [p: string]: RouterComponent }[],
future: { [p: string]: RouterComponent }[],
push: (path: string) => void,
};
navigate: (path: string) => void;
forwards: () => void;
backwards: () => void;

constructor(props: MiniRouterProps) {
this.paths = props.paths;

this.paths["404"] = { element: props.invalid ?? (() => <div></div>)};

const [getCurrentPath, setCurrentPath] = createSignal<string>(props.currentPath ?? "404");

this.currentPath = {set: setCurrentPath, get: getCurrentPath};

const currentElement = createMemo(() => {
let x = (this.paths[this.currentPath.get()] ?? this.paths["404"]);
x.props = this.currentPathProps;
return x;
});

this.history = {
past: [],
future: [],
push: (path: string) => {
this.history.past.push({[this.currentPath.get()]: currentElement()});
this.currentPath.set(path);
}
}
class MiniRouter {
router: SolidJSX.JSX.Element;
paths: Record<string, RouterComponent>;
currentPath: {set: (value: string) => void, get: Accessor<string>};
currentPathProps?: Record<string, any>;
history: {
past: { [p: string]: RouterComponent }[],
future: { [p: string]: RouterComponent }[],
push: (path: string) => void,
};
navigate: (path: string) => void;
forwards: () => void;
backwards: () => void;

constructor(props: MiniRouterProps) {
this.paths = props.paths;

this.paths["404"] = { element: props.invalid ?? (() => <div></div>)};

const [getCurrentPath, setCurrentPath] = createSignal<string>(props.currentPath ?? "404");

this.currentPath = {set: setCurrentPath, get: getCurrentPath};

const currentElement = createMemo(() => {
let x = (this.paths[this.currentPath.get()] ?? this.paths["404"]);
x.props = this.currentPathProps;
return x;
});

this.history = {
past: [],
future: [],
push: (path: string) => {
this.history.past.push({[this.currentPath.get()]: currentElement()});
this.currentPath.set(path);
}
}
this.router = <RouterViewer element={currentElement().element} props={currentElement().props}></RouterViewer>;

this.navigate = (path: string, props?: {}) => {
console.log("navigate")
this.history.push(path);
this.currentPathProps = props;
this.history.future = [];
}

this.backwards = () => {
if (this.history.past.length > 0) {
let x : {[p: string] : RouterComponent} = this.history.past.pop()!;
this.history.future.push({[this.currentPath.get()]: currentElement()});

this.currentPathProps = x[0]?.props;
this.currentPath.set(Object.keys(x)[0]);
}
}

this.forwards = () => {
if (this.history.future.length > 0) {
let x : {[p: string] : RouterComponent} = this.history.future.pop()!;
this.history.past.push({[this.currentPath.get()]: currentElement()});

this.currentPathProps = x[0]?.props;
this.currentPath.set(Object.keys(x)[0]);
}
}
}
}

function CreateMiniRouterPath(path: string, element: ValidComponent, props?: {}) {
return {[path]: {element, props}};
}

export {MiniRouter, CreateMiniRouterPath};
this.router = <RouterViewer element={currentElement().element} props={currentElement().props}></RouterViewer>;

this.navigate = (path: string, props?: {}) => {
console.log("navigate")
this.history.push(path);
this.currentPathProps = props;
this.history.future = [];
}

this.backwards = () => {
if (this.history.past.length > 0) {
let x : {[p: string] : RouterComponent} = this.history.past.pop()!;
this.history.future.push({[this.currentPath.get()]: currentElement()});

this.currentPathProps = x[0]?.props;
this.currentPath.set(Object.keys(x)[0]);
}
}

this.forwards = () => {
if (this.history.future.length > 0) {
let x : {[p: string] : RouterComponent} = this.history.future.pop()!;
this.history.past.push({[this.currentPath.get()]: currentElement()});

this.currentPathProps = x[0]?.props;
this.currentPath.set(Object.keys(x)[0]);
}
}
}
}

function CreateMiniRouterPath(path: string, element: ValidComponent, props?: {}) {
return {[path]: {element, props}};
}

export {MiniRouter, CreateMiniRouterPath};
Then to use it I did the following
function PageViewer(props: PageViewerProps) {
const router = new MiniRouter({
paths: {
...CreateMiniRouterPath("/settings", SettingsPage),
...CreateMiniRouterPath("/install", InstallPage)
}, invalid: InvalidPage, currentPath: "/settings"});


return (
<div>
<router.router/>
</div>
)
}
function PageViewer(props: PageViewerProps) {
const router = new MiniRouter({
paths: {
...CreateMiniRouterPath("/settings", SettingsPage),
...CreateMiniRouterPath("/install", InstallPage)
}, invalid: InvalidPage, currentPath: "/settings"});


return (
<div>
<router.router/>
</div>
)
}
Sorry bit long. If you want it somewhere else I can do that Made a couple modifications. Now it navigates... Updated the code with the modifications
eatingtheham
eatingtheham2mo ago
Here is the solidjs playground for it: https://playground.solidjs.com/anonymous/097424a8-e664-4785-95f0-456fff6aad3a Note: There is a bug because I can't export solids JSX.Element type in the playground
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
peerreynders
peerreynders2mo ago
peerreynders
StackBlitz
solid-router external navigation - StackBlitz
external.ts holds a link object that flipper.ts sends NavigationEvents to which are forwarded to the root component in app.tsx
eatingtheham
eatingtheham2mo ago
Thanks might take a look at it later. I got something to work for now though (code above ^^)
Want results from more Discord servers?
Add your server
More Posts