Giter VIP home page Giter VIP logo

solid-router's Introduction

Solid Router

Solid Router npm Version

Version 0.10.0 requires Solid v1.8.4 or later.

A router lets you change your view based on the URL in the browser. This allows your "single-page" application to simulate a traditional multipage site. To use Solid Router, you specify components called Routes that depend on the value of the URL (the "path"), and the router handles the mechanism of swapping them in and out.

Solid Router is a universal router for SolidJS - it works whether you're rendering on the client or on the server. It was inspired by and combines paradigms of React Router and the Ember Router. Routes can be defined directly in your app's template using JSX, but you can also pass your route configuration directly as an object. It also supports nested routing, so navigation can change a part of a component, rather than completely replacing it.

It supports all of Solid's SSR methods and has Solid's transitions baked in, so use it freely with suspense, resources, and lazy components. Solid Router also allows you to define a load function that loads parallel to the routes (render-as-you-fetch).

Getting Started

Set Up the Router

> npm i @solidjs/router

Install @solidjs/router, then start your application by rendering the router component

import { render } from "solid-js/web";
import { Router } from "@solidjs/router";

render(
  () => <Router />,
  document.getElementById("app")
);

This sets up a Router that will match on the url to display the desired page

Configure Your Routes

Solid Router allows you to configure your routes using JSX:

  1. Add each route to a <Router> using the Route component, specifying a path and a component to render when the user navigates to that path.
import { render } from "solid-js/web";
import { Router, Route } from "@solidjs/router";

import Home from "./pages/Home";
import Users from "./pages/Users";

render(() => (
  <Router>
    <Route path="/users" component={Users} />
    <Route path="/" component={Home} />
  </Router>
), document.getElementById("app"));
  1. Provide a root level layout

This will always be there and won't update on page change. It is the ideal place to put top level navigation and Context Providers

import { render } from "solid-js/web";
import { Router, Route } from "@solidjs/router";

import Home from "./pages/Home";
import Users from "./pages/Users";

const App = props => (
  <>
    <h1>My Site with lots of pages</h1>
    {props.children}
  </>
)

render(() => (
  <Router root={App}>
    <Route path="/users" component={Users} />
    <Route path="/" component={Home} />
  </Router>
), document.getElementById("app"));
  1. Create a CatchAll Route (404 page)

We can create catchall routes for pages not found at any nested level of the router. We use * and optionally the name of a parameter to retrieve the rest of the path.

import { render } from "solid-js/web";
import { Router, Route } from "@solidjs/router";

import Home from "./pages/Home";
import Users from "./pages/Users";
import NotFound from "./pages/404";

const App = props => (
  <>
    <h1>My Site with lots of pages</h1>
    {props.children}
  </>
)

render(() => (
  <Router root={App}>
    <Route path="/users" component={Users} />
    <Route path="/" component={Home} />
    <Route path="*404" component={NotFound} />
  </Router>
), document.getElementById("app"));
  1. Lazy-load route components

This way, the Users and Home components will only be loaded if you're navigating to /users or /, respectively.

import { lazy } from "solid-js";
import { render } from "solid-js/web";
import { Router, Route } from "@solidjs/router";

const Users = lazy(() => import("./pages/Users"));
const Home = lazy(() => import("./pages/Home"));

const App = props => (
  <>
    <h1>My Site with lots of pages</h1>
    {props.children}
  </>
)

render(() => (
  <Router root={App}>
    <Route path="/users" component={Users} />
    <Route path="/" component={Home} />
  </Router>
), document.getElementById("app"));

Create Links to Your Routes

Use an anchor tag that takes you to a route:

import { lazy } from "solid-js";
import { render } from "solid-js/web";
import { Router, Route } from "@solidjs/router";

const Users = lazy(() => import("./pages/Users"));
const Home = lazy(() => import("./pages/Home"));

const App = props => (
  <>
    <nav>
      <a href="/about">About</a>
      <a href="/">Home</a>
    </nav>
    <h1>My Site with lots of pages</h1>
    {props.children}
  </>
);

render(() => (
  <Router root={App}>
    <Route path="/users" component={Users} />
    <Route path="/" component={Home} />
  </Router>
), document.getElementById("app"));

Dynamic Routes

If you don't know the path ahead of time, you might want to treat part of the path as a flexible parameter that is passed on to the component.

import { lazy } from "solid-js";
import { render } from "solid-js/web";
import { Router, Route } from "@solidjs/router";

const Users = lazy(() => import("./pages/Users"));
const User = lazy(() => import("./pages/User"));
const Home = lazy(() => import("./pages/Home"));

render(() => (
  <Router>
    <Route path="/users" component={Users} />
    <Route path="/users/:id" component={User} />
    <Route path="/" component={Home} />
  </Router>
 ), document.getElementById("app"));

The colon indicates that id can be any string, and as long as the URL fits that pattern, the User component will show.

You can then access that id from within a route component with useParams.

Note on Animation/Transitions: Routes that share the same path match will be treated as the same route. If you want to force re-render you can wrap your component in a keyed <Show> like:

<Show when={params.something} keyed><MyComponent></Show>

Each path parameter can be validated using a MatchFilter. This allows for more complex routing descriptions than just checking the presence of a parameter.

import { lazy } from "solid-js";
import { render } from "solid-js/web";
import { Router, Route } from "@solidjs/router";
import type { SegmentValidators } from "./types";

const User = lazy(() => import("./pages/User"));

const filters: MatchFilters = {
  parent: ["mom", "dad"], // allow enum values
  id: /^\d+$/, // only allow numbers
  withHtmlExtension: (v: string) => v.length > 5 && v.endsWith(".html"), // we want an `*.html` extension
};

render(() => (
  <Router>
    <Route
      path="/users/:parent/:id/:withHtmlExtension"
      component={User}
      matchFilters={filters}
    />
  </Router>
), document.getElementById("app"));

Here, we have added the matchFilters prop. This allows us to validate the parent, id and withHtmlExtension parameters against the filters defined in filters. If the validation fails, the route will not match.

So in this example:

  • /users/mom/123/contact.html would match,
  • /users/dad/123/about.html would match,
  • /users/aunt/123/contact.html would not match as :parent is not 'mom' or 'dad',
  • /users/mom/me/contact.html would not match as :id is not a number,
  • /users/dad/123/contact would not match as :withHtmlExtension is missing .html.

Optional Parameters

Parameters can be specified as optional by adding a question mark to the end of the parameter name:

// Matches stories and stories/123 but not stories/123/comments
<Route path="/stories/:id?" component={Stories} />

Wildcard Routes

:param lets you match an arbitrary name at that point in the path. You can use * to match any end of the path:

// Matches any path that begins with foo, including foo/, foo/a/, foo/a/b/c
<Route path="foo/*" component={Foo} />

If you want to expose the wild part of the path to the component as a parameter, you can name it:

<Route path="foo/*any" component={Foo} />

Note that the wildcard token must be the last part of the path; foo/*any/bar won't create any routes.

Multiple Paths

Routes also support defining multiple paths using an array. This allows a route to remain mounted and not rerender when switching between two or more locations that it matches:

// Navigating from login to register does not cause the Login component to re-render
<Route path={["login", "register"]} component={Login} />

Nested Routes

The following two route definitions have the same result:

<Route path="/users/:id" component={User} />
<Route path="/users">
  <Route path="/:id" component={User} />
</Route>

/users/:id renders the <User/> component, and /users/ is an empty route.

Only leaf Route nodes (innermost Route components) are given a route. If you want to make the parent its own route, you have to specify it separately:

//This won't work the way you'd expect
<Route path="/users" component={Users}>
  <Route path="/:id" component={User} />
</Route>

// This works
<Route path="/users" component={Users} />
<Route path="/users/:id" component={User} />

// This also works
<Route path="/users">
  <Route path="/" component={Users} />
  <Route path="/:id" component={User} />
</Route>

You can also take advantage of nesting by using props.children passed to the route component.

function PageWrapper(props) {
  return (
    <div>
      <h1> We love our users! </h1>
      {props.children}
      <A href="/">Back Home</A>
    </div>
  );
}

<Route path="/users" component={PageWrapper}>
  <Route path="/" component={Users} />
  <Route path="/:id" component={User} />
</Route>;

The routes are still configured the same, but now the route elements will appear inside the parent element where the props.children was declared.

You can nest indefinitely - just remember that only leaf nodes will become their own routes. In this example, the only route created is /layer1/layer2, and it appears as three nested divs.

<Route
  path="/"
  component={(props) =>
    <div>
      Onion starts here {props.children}
    </div>
  }
>
  <Route
    path="layer1"
    component={(props) =>
      <div>
        Another layer {props.children}
      </div>
    }
  >
    <Route
      path="layer2"
      component={() => <div>Innermost layer</div>}
    />
  </Route>
</Route>

Load Functions

Even with smart caches it is possible that we have waterfalls both with view logic and with lazy loaded code. With load functions, we can instead start fetching the data parallel to loading the route, so we can use the data as soon as possible. The load function is called when the Route is loaded or eagerly when links are hovered.

As its only argument, the load function is passed an object that you can use to access route information:

import { lazy } from "solid-js";
import { Route } from "@solidjs/router";

const User = lazy(() => import("./pages/users/[id].js"));

// load function
function loadUser({params, location}) {
  // do loading
}

// Pass it in the route definition
<Route path="/users/:id" component={User} load={loadUser} />;
key type description
params object The route parameters (same value as calling useParams() inside the route component)
location { pathname, search, hash, query, state, key} An object that you can use to get more information about the path (corresponds to useLocation())
intent "initial", "navigate", "native", "preload" Indicates why this function is being called.
  • "initial" - the route is being initially shown (ie page load)
  • "native" - navigate originated from the browser (eg back/forward)
  • "navigate" - navigate originated from the router (eg call to navigate or anchor clicked)
  • "preload" - not navigating, just preloading (eg link hover)

A common pattern is to export the load function and data wrappers that corresponds to a route in a dedicated route.data.js file. This way, the data function can be imported without loading anything else.

import { lazy } from "solid-js";
import { Route } from "@solidjs/router";
import loadUser from "./pages/users/[id].data.js";
const User = lazy(() => import("/pages/users/[id].js"));

// In the Route definition
<Route path="/users/:id" component={User} load={loadUser} />;

The return value of the load function is passed to the page component when called at anytime other than "preload", so you can initialize things in there, or alternatively use our new Data APIs:

Data APIs

Keep in mind these are completely optional. To use but showcase the power of our load mechanism.

cache

To prevent duplicate fetching and to trigger handle refetching we provide a cache api. That takes a function and returns the same function.

const getUser = cache(async (id) => {
  return (await fetch(`/api/users${id}`)).json()
}, "users") // used as cache key + serialized arguments

It is expected that the arguments to the cache function are serializable.

This cache accomplishes the following:

  1. It does just deduping on the server for the lifetime of the request.
  2. It does preload cache in the browser which lasts 10 seconds. When a route is preloaded on hover or when load is called when entering a route it will make sure to dedupe calls.
  3. We have a reactive refetch mechanism based on key. So we can tell routes that aren't new to retrigger on action revalidation.
  4. It will serve as a back/forward cache for browser navigation up to 5 mins. Any user based navigation or link click bypasses it. Revalidation or new fetch updates the cache.

Using it with load function might look like:

import { lazy } from "solid-js";
import { Route } from "@solidjs/router";
import { getUser } from ... // the cache function

const User = lazy(() => import("./pages/users/[id].js"));

// load function
function loadUser({params, location}) {
  void getUser(params.id)
}

// Pass it in the route definition
<Route path="/users/:id" component={User} load={loadUser} />;

Inside your page component you:

// pages/users/[id].js
import { getUser } from ... // the cache function

export default function User(props) {
  const user = createAsync(() => getUser(props.params.id));
  return <h1>{user().name}</h1>;
}

Cached function has a few useful methods for getting the key that are useful for invalidation.

let id = 5;

getUser.key // returns "users"
getUser.keyFor(id) // returns "users[5]"

You can revalidate the cache using the revalidate method or you can set revalidate keys on your response from your actions. If you pass the whole key it will invalidate all the entries for the cache (ie "users" in the example above). You can also invalidate a single entry by using keyFor.

cache can be defined anywhere and then used inside your components with:

createAsync

This is light wrapper over createResource that aims to serve as stand-in for a future primitive we intend to bring to Solid core in 2.0. It is a simpler async primitive where the function tracks like createMemo and it expects a promise back that it turns into a Signal. Reading it before it is ready causes Suspense/Transitions to trigger.

const user = createAsync((currentValue) => getUser(params.id))

Using cache in createResource directly won't work properly as the fetcher is not reactive and it won't invalidate properly.

createAsyncStore

Similar to createAsync except it uses a deeply reactive store. Perfect for applying fine-grained changes to large model data that updates.

const todos = createAsyncStore(() => getTodos());

action

Actions are data mutations that can trigger invalidations and further routing. A list of prebuilt response helpers can be found below.

import { action, revalidate, redirect } from "@solidjs/router"

// anywhere
const myAction = action(async (data) => {
  await doMutation(data);
  throw redirect("/", { revalidate: getUser.keyFor(data.id) }); // throw a response to do a redirect
});

// in component
<form action={myAction} method="post" />

//or
<button type="submit" formaction={myAction}></button>

Actions only work with post requests, so make sure to put method="post" on your form.

Sometimes it might be easier to deal with typed data instead of FormData and adding additional hidden fields. For that reason Actions have a with method. That works similar to bind which applies the arguments in order.

Picture an action that deletes Todo Item:

const deleteTodo = action(async (formData: FormData) => {
  const id = Number(formData.get("id"))
  await api.deleteTodo(id)
})

<form action={deleteTodo} method="post">
  <input type="hidden" name="id" value={todo.id} />
  <button type="submit">Delete</button>
</form>

Instead with with you can write this:

const deleteUser = action(api.deleteTodo)

<form action={deleteTodo.with(todo.id)} method="post">
  <button type="submit">Delete</button>
</form>

Notes of <form> implementation and SSR

This requires stable references as you can only serialize a string as an attribute, and across SSR they'd need to match. The solution is providing a unique name.

const myAction = action(async (args) => {}, "my-action");

useAction

Instead of forms you can use actions directly by wrapping them in a useAction primitive. This is how we get the router context.

// in component
const submit = useAction(myAction)
submit(...args)

The outside of a form context you can use custom data instead of formData, and these helpers preserve types. However, even when used with server functions (in projects like SolidStart) this requires client side javascript and is not Progressive Enhancible like forms are.

useSubmission/useSubmissions

Are used to injecting the optimistic updates while actions are in flight. They either return a single Submission(latest) or all that match with an optional filter function.

type Submission<T, U> = {
  readonly input: T;
  readonly result?: U;
  readonly pending: boolean;
  readonly url: string;
  clear: () => void;
  retry: () => void;
};

const submissions = useSubmissions(action, (input) => filter(input));
const submission = useSubmission(action, (input) => filter(input));

Response Helpers

These are used to communicate router navigations from cache/actions, and can include invalidation hints. Generally these are thrown to not interfere the with the types and make it clear that function ends execution at that point.

redirect(path, options)

Redirects to the next route

const getUser = cache(() => {
  const user = await api.getCurrentUser()
  if (!user) throw redirect("/login");
  return user;
})

reload(options)

Reloads the data on the current page

const getTodo = cache(async (id: number) => {
  const todo = await fetchTodo(id);
  return todo;
}, "todo")

const updateTodo = action(async (todo: Todo) => {
  await updateTodo(todo.id, todo);
  reload({ revalidate: getTodo.keyFor(id) })
})

Config Based Routing

You don't have to use JSX to set up your routes; you can pass an array of route definitions:

import { lazy } from "solid-js";
import { render } from "solid-js/web";
import { Router } from "@solidjs/router";

const routes = [
  {
    path: "/users",
    component: lazy(() => import("/pages/users.js")),
  },
  {
    path: "/users/:id",
    component: lazy(() => import("/pages/users/[id].js")),
    children: [
      {
        path: "/",
        component: lazy(() => import("/pages/users/[id]/index.js")),
      },
      {
        path: "/settings",
        component: lazy(() => import("/pages/users/[id]/settings.js")),
      },
      {
        path: "/*all",
        component: lazy(() => import("/pages/users/[id]/[...all].js")),
      },
    ],
  },
  {
    path: "/",
    component: lazy(() => import("/pages/index.js")),
  },
  {
    path: "/*all",
    component: lazy(() => import("/pages/[...all].js")),
  },
];

render(() =>
  <Router>{routes}</Router>,
  document.getElementById("app")
);

Also you can pass a single route definition object for a single route:

import { lazy } from "solid-js";
import { render } from "solid-js/web";
import { Router } from "@solidjs/router";

const route = {
  path: "/",
  component: lazy(() => import("/pages/index.js"))
};

render(() => <Router>{route}</Router>, document.getElementById("app"));

Alternative Routers

Hash Mode Router

By default, Solid Router uses location.pathname as route path. You can simply switch to hash mode through using <HashRouter>.

import { HashRouter } from "@solidjs/router";

<HashRouter />;

Memory Mode Router

You can also use memory mode router for testing purpose.

import { MemoryRouter } from "@solidjs/router";

<MemoryRouter />;

SSR Routing

For SSR you can use the static router directly or the browser Router defaults to it on the server, just pass in the url.

import { isServer } from "solid-js/web";
import { Router } from "@solidjs/router";

<Router url={isServer ? req.url : ""} />;

Components

<Router>

This is the main Router component for the browser.

prop type description
children JSX.Element, RouteDefinition, or RouteDefinition[] The route definitions
root Component Top level layout component
base string Base url to use for matching routes
actionBase string Root url for server actions, default: /_server
preload boolean Enables/disables preloads globally, default: true
explicitLinks boolean Disables all anchors being intercepted and instead requires <A>. default: false

<A>

Like the <a> tag but supports automatic apply of base path + relative paths and active class styling (requires client side JavaScript).

The <A> tag has an active class if its href matches the current location, and inactive otherwise. Note: By default matching includes locations that are descendents (eg. href /users matches locations /users and /users/123), use the boolean end prop to prevent matching these. This is particularly useful for links to the root route / which would match everything.

prop type description
href string The path of the route to navigate to. This will be resolved relative to the route that the link is in, but you can preface it with / to refer back to the root.
noScroll boolean If true, turn off the default behavior of scrolling to the top of the new page
replace boolean If true, don't add a new entry to the browser history. (By default, the new page will be added to the browser history, so pressing the back button will take you to the previous route.)
state unknown Push this value to the history stack when navigating
inactiveClass string The class to show when the link is inactive (when the current location doesn't match the link)
activeClass string The class to show when the link is active
end boolean If true, only considers the link to be active when the curent location matches the href exactly; if false, check if the current location starts with href

<Navigate />

Solid Router provides a Navigate component that works similarly to A, but it will immediately navigate to the provided path as soon as the component is rendered. It also uses the href prop, but you have the additional option of passing a function to href that returns a path to navigate to:

function getPath({ navigate, location }) {
  // navigate is the result of calling useNavigate(); location is the result of calling useLocation().
  // You can use those to dynamically determine a path to navigate to
  return "/some-path";
}

// Navigating to /redirect will redirect you to the result of getPath
<Route path="/redirect" component={() => <Navigate href={getPath} />} />;

<Route>

The Component for defining Routes:

prop type description
path string Path partial for defining the route segment
component Component Component that will be rendered for the matched segment
matchFilters MatchFilters Additional constraints for matching against the route
children JSX.Element Nested <Route> definitions
load RouteLoadFunc Function called during preload or when the route is navigated to.

Router Primitives

Solid Router provides a number of primitives that read off the Router and Route context.

useParams

Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.

const params = useParams();

// fetch user based on the id path parameter
const [user] = createResource(() => params.id, fetchUser);

useNavigate

Retrieves method to do navigation. The method accepts a path to navigate to and an optional object with the following options:

  • resolve (boolean, default true): resolve the path against the current route
  • replace (boolean, default false): replace the history entry
  • scroll (boolean, default true): scroll to top after navigation
  • state (any, default undefined): pass custom state to location.state

Note: The state is serialized using the structured clone algorithm which does not support all object types.

const navigate = useNavigate();

if (unauthorized) {
  navigate("/login", { replace: true });
}

useLocation

Retrieves reactive location object useful for getting things like pathname

const location = useLocation();

const pathname = createMemo(() => parsePath(location.pathname));

useSearchParams

Retrieves a tuple containing a reactive object to read the current location's query parameters and a method to update them. The object is a proxy so you must access properties to subscribe to reactive updates. Note values will be strings and property names will retain their casing.

The setter method accepts an object whose entries will be merged into the current query string. Values '', undefined and null will remove the key from the resulting query string. Updates will behave just like a navigation and the setter accepts the same optional second parameter as navigate and auto-scrolling is disabled by default.

const [searchParams, setSearchParams] = useSearchParams();

return (
  <div>
    <span>Page: {searchParams.page}</span>
    <button
      onClick={() =>
        setSearchParams({ page: (parseInt(searchParams.page) || 0) + 1 })
      }
    >
      Next Page
    </button>
  </div>
);

useIsRouting

Retrieves signal that indicates whether the route is currently in a Transition. Useful for showing stale/pending state when the route resolution is Suspended during concurrent rendering.

const isRouting = useIsRouting();

return (
  <div classList={{ "grey-out": isRouting() }}>
    <MyAwesomeConent />
  </div>
);

useMatch

useMatch takes an accessor that returns the path and creates a Memo that returns match information if the current path matches the provided path. Useful for determining if a given path matches the current route.

const match = useMatch(() => props.href);

return <div classList={{ active: Boolean(match()) }} />;

useCurrentMatches

useCurrentMatches returns all the matches for the current matched route. Useful for getting all the route information.

For example if you stored breadcrumbs on your route definition you could retrieve them like so:

const matches = useCurrentMatches();
const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))

usePreloadRoute

usePreloadRoute returns a function that can be used to preload a route manual. This is what happens automatically with link hovering and similar focus based behavior, but it is available here as an API.

const preload = usePreloadRoute();

preload(`/users/settings`, { preloadData: true });

useBeforeLeave

useBeforeLeave takes a function that will be called prior to leaving a route. The function will be called with:

  • from (Location): current location (before change).
  • to (string | number}: path passed to navigate.
  • options (NavigateOptions}: options passed to navigate.
  • preventDefault (void function): call to block the route change.
  • defaultPrevented (readonly boolean): true if any previously called leave handlers called preventDefault().
  • retry (void function, force?: boolean ): call to retry the same navigation, perhaps after confirming with the user. Pass true to skip running the leave handlers again (ie force navigate without confirming).

Example usage:

useBeforeLeave((e: BeforeLeaveEventArgs) => {
  if (form.isDirty && !e.defaultPrevented) {
    // preventDefault to block immediately and prompt user async
    e.preventDefault();
    setTimeout(() => {
      if (window.confirm("Discard unsaved changes - are you sure?")) {
        // user wants to proceed anyway so retry with force=true
        e.retry(true);
      }
    }, 100);
  }
});

Migrations from 0.9.x

v0.10.0 brings some big changes to support the future of routing including Islands/Partial Hydration hybrid solutions. Most notably there is no Context API available in non-hydrating parts of the application.

The biggest changes are around removed APIs that need to be replaced.

<Outlet>, <Routes>, useRoutes

This is no longer used and instead will use props.children passed from into the page components for outlets. This keeps the outlet directly passed from its page and avoids oddness of trying to use context across Islands boundaries. Nested <Routes> components inherently cause waterfalls and are <Outlets> themselves so they have the same concerns.

Keep in mind no <Routes> means the <Router> API is different. The <Router> acts as the <Routes> component and its children can only be <Route> components. Your top-level layout should go in the root prop of the router as shown above

element prop removed from Route

Related without Outlet component it has to be passed in manually. At which point the element prop has less value. Removing the second way to define route components to reduce confusion and edge cases.

data functions & useRouteData

These have been replaced by a load mechanism. This allows link hover preloads (as the load function can be run as much as wanted without worry about reactivity). It support deduping/cache APIs which give more control over how things are cached. It also addresses TS issues with getting the right types in the Component without typeof checks.

That being said you can reproduce the old pattern largely by turning off preloads at the router level and then injecting your own Context:

import { lazy } from "solid-js";
import { Route } from "@solidjs/router";

const User = lazy(() => import("./pages/users/[id].js"));

// load function
function loadUser({params, location}) {
  const [user] = createResource(() => params.id, fetchUser);
  return user;
}

// Pass it in the route definition
<Router preload={false}>
  <Route path="/users/:id" component={User} load={loadUser} />
</Router>

And then in your component taking the page props and putting them in a Context.

function User(props) {
  <UserContext.Provider value={props.data}>
    {/* my component content  */}
  </UserContext.Provider>
}

// Somewhere else
function UserDetails() {
  const user = useContext(UserContext)
  // render stuff
}

SPAs in Deployed Environments

When deploying applications that use a client side router that does not rely on Server Side Rendering you need to handle redirects to your index page so that loading from other URLs does not cause your CDN or Hosting to return not found for pages that aren't actually there.

Each provider has a different way of doing this. For example on Netlify you create a _redirects file that contains:

/*   /index.html   200

On Vercel you add a rewrites section to your vercel.json:

{
  "rewrites": [
    {
      "source": "/(.*)",
      "destination": "/index.html"
    }
  ]
}

solid-router's People

Contributors

albertsabate avatar aminya avatar amoutonbrady avatar andrewrosss avatar birkskyum avatar brendan-csel avatar brendonovich avatar brenelz avatar btakita avatar cliffordfajardo avatar csarnataro avatar danieltroger avatar davedbase avatar femincan avatar high1 avatar jchatard avatar jeski-bright avatar jpdutoit avatar jutanium avatar marvin-j97 avatar nksaraf avatar oedotme avatar orenelbaum avatar rturnq avatar rvlzzr avatar ryansolid avatar sanichkotikov avatar schniz avatar thetarnav avatar yhdgms1 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

solid-router's Issues

support conditional Route

I'd like to render one Route or another depending on the data from a store.

Is it supported somehow?

The active class triggers for `NavLink`s using the `/` path, regardless of what path you visit.

Summary: The active class triggers for NavLinks using the / path, regardless of what path you visit.

Steps to reproduce:

  1. Run the following code
  2. Navigate to the "list" page

Expected result: Only the list NavLink has an active class
Actual result: Boththe NavLinks have an active class

import { MountableElement, render } from "solid-js/web";
import { Router, useRoutes, NavLink } from "solid-app-router";

const routes = [
	{
		path: "/",
		component: () => <div>Home</div>
	},
	{
		path: "/list",
		component: () => <div>List</div>,
	},
];

function App() {
	const Routes = useRoutes(routes);
	return (
		<>
			<NavLink href="/">
				Home
			</NavLink>
			<NavLink href="/list">
				List
			</NavLink>
			<Routes />
		</>
	);
}

render(
	() => (
		<Router>
			<App />
		</Router>
	),
	document.getElementById("app") as MountableElement
);

Commentary
I get the logic behind this, in that if you are visiting product/<some id here> you might want to match against a /product NavLink. But I think there still should be a way to get an active class on your root directory without it always having to be active.

Also, thanks for all the hard work you guys have done on this router, and solidjs in general!

Document how to read routing data in a component

Take for example the route "/users/:id/orders?order=popular".

How does one read the following inside the component which is routed to?:

  • the route parameters (:id)
  • the query parameters (order=popular)

I've been trying to read through the source code and at first glance it looks like these ought to be set as props on the component, but it's not proving to a very productive exercise.

Would you be able to provide a little more documentation regarding the basic usage?

useMatch is not consistence with classList

 <Link
      href="/about"
      classList={{
        "bg-blue-100 hover:bg-blue-100": Boolean(useMatch(() => "/about")()),
      }}
      className="rounded-full hover:bg-gray-100 px-2 py-1"
    >
     

      <span className="text-sm font-bold">About Us</span>
    </Link>

Refresh on /about path do not apply classList

Expose RouteProps to public

Allows Route component to be extended or encapsulated. i.e. if I want an authenticated route, a good pattern might be...

type AuthenticatedRouteProp = RouteProp & { isAuthed: boolean };
const AuthenticatedRoute: Component<RouteProp> = (props) => {
  return (
    { props.isAuthed ? <Route {...props} /> : <Navigate href={'/login'} /> }
  );
} ;

which means that RouteProp needs to be exposed

Autowire routes based on a predefined folder

It would be interesting to have a feature like this:

    <Router autowire={true} root={process.env.PUBLIC_URL}>
      <Nav />
      <Route class="view" />
    </Router>

Which takes a predefined (hard-coded) folder, let's say "pages" (since I've seen it on every solid projects. It will parse this folder and get path.jsx/tsx with its path.data.js/ts and generate the routes

Uncaught Error: Make sure your app is wrapped in a <Router />

When I use Dynamic to switch between "internal" components in my "main page component" using a signal, <Link>s fail with the error in the title.

Example

(Also, on a related note, is it a bad idea to store the "internal state component" in a signal? If it is, how else am I going to create components with dynamic data (when I, for example, receive data from a websocket and want to display that data in a new component by passing it as props which is then shown using <Dynamic/>)?)

Adding a title property to the route config to update the document title on navigation

Was reading some articles recently around SPA's and the issue of a document title when pages change. I thought there might be an opportunity to add something into the router config to capture a page title (potentially combined with a "template" title for building a title of site name - page name for example)

I was thinking something along the lines of having a router config property (and route component prop) of "title", which is then used during the navigation function to set the document title. The "template" could be a top level config item (or even a property of the main Router object) which sets the template string and has some sort of placeholder that is replaced with the actual title to be updated.

I'm happy to look at getting a PR together to see if it could work, but just wanted to see if it's something that would be liked/supported.

More than 2 levels of nesting not work

It seems can not use more than 2 levels of nesting.
Is it a bug or design limit?

https://codesandbox.io/s/solid-router-nesting-thlvg?file=/src/main.tsx

import { Router,  Route,  Routes,  Link,  useParams,  Outlet} from 'solid-app-router'
import { render } from 'solid-js/web'

function AppFrame() {
  return (
    <>
      <h1>AppFrame</h1>
      <Outlet />
    </>
  )
}

function ArticleList() {
  return (
    <>
      <h2>Article List</h2>
      <ol>
        <li>
          <Link href="/articles/1">Article #1</Link>
        </li>
        <li>
          <Link href="/articles/2">Article #2</Link>
        </li>
        <li>
          <Link href="/articles/3">Article #3</Link>
        </li>
      </ol>
      <Outlet />
    </>
  )
}

function Article() {
  const params = useParams<{ id: string }>()
  return <p>Article Content: {params.id}</p>
}

export default function App() {
  return (
    <Router>
      <p>
        <Link href="/">Home</Link>
      </p>
      <hr />
      <Routes>
        <Route path="/" element={<AppFrame />}>
          <Route path="/" element={<ArticleList />}>
            <Route path="/articles/:id" element={<Article />} />
          </Route>
        </Route>
      </Routes>
    </Router>
  )
}

render(() => <App />, document.getElementById('app'))

Support optional path params

Add support for optional params so /page and /page/unknown both match in this example:

<Route path="/page/:pathParam?" component={MyPage} />

Using mergeProps in data causing TypeError

Possibly similar to #6 - using mergeProps in data seems to cause the following error on latest solid-js and solid-app-router:

TypeError
can't define property Symbol("state-node"): Object is not extensible
getDataNodes
https://uojg1.csb.app/node_modules/solid-js/dist/solid.js:1131:23
get
https://uojg1.csb.app/node_modules/solid-js/dist/solid.js:1158:33
Data/</<
tab1.tsx:12:18

   9 |       <button type="button" onClick={increment}>
  10 |         {count()}
  11 |       </button>
> 12 |       <code>{JSON.stringify(props, null, 2)}</code>
     |                  ^
  13 |     </>
  14 |   );
  15 | }

Here is the forked sandbox from #6 where I just updated the versions to latest solid-js and solid-app-router: https://codesandbox.io/s/silent-voice-uojg1

Removing the use of mergeProps allows it to work without error.

Add named routes

The official router for Vue.js has a feature called "Named routes". It makes it possible to give the routes a separate name to refer to them instead of using the actual route. The benefit of this is that you can rename the route inside the config file but not where it has been referenced.

This feature would be a great addition.

Symbolic useData

As discussed in Discord, the current numeric input to useData is brittle (even with the negative-number extension of #69). Using a component at different depths of the hierarchy makes it difficult to find the right ancestor data.

The general agreement from the thread was to introduce a context-like API. I believe the plan is to pass in the same function that was given as a data attribute to the Route. For example, <Route data={UserData}> woudl get accessed like so:

const data = useData(UserData);

Type-wise, this could work as follows:

function useData<T>(dataFunc: () => T): T | undefined;
  // returns undefined if no ancestor route uses this dataFunc

There was also discussion about useDataLoader but that seemed more specific to Resources (maybe a Solid Start thing?).

A related question is whether this should replace the existing numeric model (breaking change), or augment it as a second option (more code). I think at least we should preserve the common case, where useData() returns the current route's data, though this version will be better for typing even in that case.

Document path pattern syntax

It looks like the syntax supported for path patterns is the same as React Router, but it'd be helpful to at least say "supports :param and *rest parameters", and perhaps for those unfamiliar, clarify that :param matches a single component between slashes, while * must be the final thing (and looking at the code, I think prefixed by a slash?).

I found this hard to figure out without reading the code, because you also mention Ember Router which I'm not familiar with, so I figured there might be some additional pattern syntax.

Admittedly I couldn't find this well documented in React Router either, but I found brief mention here and here.

Allow sub path '*all' to match top level '*all' instead.

Common use case is to show completely empty not found page, if route doesn't match, without any specific sub path elements, currently this is not possible without redirects which change current path or some position absolute styling hacks.

In this example I wish to show top level Not Found page and not sub level one when no other routes match.
Example

const User = () => (
  <div class="user-page">
    <div>User page</div>
    <Outlet />
  </div>
);
...
<Routes>
  <Route path="/" element={<div>Home</div>} />
  <Route path="/users" component={User}>
    <Route path="/" element={<div>User</div>} />
    <Route path="/sub" element={<div>UserSub</div>} />
    <Route
      path="/*all"
      element={
        <div>
          Foward this to top level "Not Found" page somehow
          while keeping current path.
        </div>
      }
    />
  </Route>
  <Route path="/*all" element={<div>Not Found</div>} />
</Routes>;

Improve documentation for useLocation

The parseQuery in the example for useLocation is a bit misleading IMHO. Mention that location.query returns a Proxy that can be used to access the query parameters, e.g.

const location = useLocation();

return <p>{location.query.code}</p>;

Also mention whether query parameters are downcased or not, so whether ?confirmationCode=123 has to be accessed as location.query.confirmationcode or location.query.confirmationCode.

route no working in production build

Hi,
When im in development, button is navigating to another url.
but when i run npm run build
and i do npm run serve
and i click the button to navigate its not working only the url is change.

TIA

Using a <Link> component to navigate to the current route does not update data

It seems routing to the current page, but using a different pattern than the one you are currently on, does not update the data associated with that route. An example of when this might be done is on a product page, linking to another related product.

Steps to reproduce:

  1. Run the given code on a server
  2. Visit https://127.0.0.1/foo/1
  3. Click on "Go to page 2"

Expected result: The title reads "You are on page 2"
Actual result: The title reads "You are on page 1", even though the URLhas updated

import { createSignal, Accessor } from 'solid-js';
import { MountableElement, render } from 'solid-js/web';
import { Router, Routes, Route, Link, useData } from 'solid-app-router';
import { Params } from 'solid-app-router';

function FooData({ params }: { params: Params}): Accessor<number| undefined> {
	return createSignal(parseInt(params.id))[0];
}

function Foo() {
	const routingData = useData() as Accessor<number| undefined>;

	return <>
		<h1>You are on page {routingData()}</h1>
		<Link href={`/foo/1`}>Go to page 1</Link>
		<br/>
		<Link href={`/foo/2`}>Go to page 2</Link>
	</>;
}

render(
	() => (
		<Router>
			<Routes>
				<Route path="/foo/:id" element={<Foo />} data={FooData} />
			</Routes>
		</Router>
	),
	document.getElementById('app') as MountableElement
);

(Screenshot of step 2)
image
(Screenshot of step 3)
image

Route to path ="/" is not working in electron.js

td,dr

need Hashrouter equivalent for solid.

I was trying the routing in a basic electron app and got stuck with this.

The path="/" works fine in browsers but I cannot GET that page in my electron window.

STEPS TO REPRODUCE

app.tsx

import { Link ,Route, Routes } from "solid-app-router";

export const App = () => {
  const Nav = () => {
    return (
      <>
      <div style={{display:"flex"}}>
      <Link style={{"margin-right":"10px"}} href="/">homepage</Link>
      <Link style={{"margin-right":"10px"}} href="/newcomponent">new component</Link>
      </div> 
      </>
    );
  };
  const Homepage = () => {
    return (
      <>
        <Nav />
        <div>homepage component.</div>
      </>
    );
  };
  const Component1 = () => {
    return (
      <>
        <Nav />
        <div>new component.</div>
      </>
    );
  };
  return (
    <>
      {/* <Nav />  */} 
      <Routes>
        <Route path="/" element={<Homepage />} />
        <Route path="/newcomponent" element={<Component1 />} />
      </Routes>
    </>
  );
};

index.tsx

import { render } from "solid-js/web";
import { Router } from "solid-app-router";
import { App } from "./app.main";

render(
  () => (
    <Router>
      <App />
    </Router>
  ),
  document.getElementById("app")
);
`

EXPECTED OUTPUT

This code was supposed to show Homepage component on loading the window.

OUTPUT -

Both components were not loaded. I have to make the Nav component load on every page to route between pages.
The default path="/" is not getting loaded.

THIS CODE WORKS AS EXPECTED IN BROWSERS.

TECHSTACK

  • solid.js ^1.2.6
  • solid-app-router ^0.1.14
  • electron ^16.0.5
for creating a local solid-electron instance

use - https://github.com/DhansAL/solidjs-electronForge-TS-template

Expose createPathMatcher to public

Expose createPathMatcher function. useMatch only works with current path, so you have no options if you want for example compare previous path and see if it matches.

Serve + Nested Routing results in Error

I'm currently using serve with the following config to redirect all routes back to index.html:

{
  "rewrites": [
    { "source": "/**", "destination": "/index.html" },
    { "source": "/**/**", "destination": "/index.html" }
  ]
}

My Routes are defined as follows in Solid:

const AppRoutes = () => {
  return (
    <Routes>
      <Route
        path="/"
        element={
          <div>
            <Link href={'/app/2'}>Link</Link>
          </div>
        }
      />
      <Route path={'/app'}>
      <Route path={'/'} element={<p>Hi</p>}></Route>
        <Route path={'/*all'} element={<p>Hello</p>}></Route>
      </Route>
    </Routes>
  );
};

Going directly in the browser to localhost:3000/app results in the correct

Hi

being displayed. Clicking the link if I go to localhost:3000 correctly displays

Hello

.

However, if I decide to hit localhost:3000/app/2 in my address bar, I get the following errors:

[Error] SyntaxError: Unexpected token '<'
	(anonymous function) (runtime.js:1)
[Error] SyntaxError: Unexpected token '<'
	(anonymous function) (solid-js.js:1)
[Error] SyntaxError: Unexpected token '<'
	(anonymous function) (regexparam.js:1)
[Error] SyntaxError: Unexpected token '<'
	(anonymous function) (rturnq.js:1)
[Error] SyntaxError: Unexpected token '<'
	(anonymous function) (main.js:1)
[Error] Did not parse stylesheet at 'http://localhost:3000/app/main.css' because non CSS MIME types are not allowed in strict mode.

I'm using the latest solid/solid-router and webpack

Any chance there's a known solution to this and I'm just being dumb?

Top level paths always matches even if nested paths were declared first

I am not sure if this is a bug or just me not understanding how routes priority works.
Reproduction

<Route path="/" component={User}>
  <Route path="/users" element={<div>Should match this</div>} />
</Route>
<Route path="/users" element={<div>But matches this</div>} />

I discovered this while trying to use <Navigate> and redirect from /|/users to /users/default-route

Change `base` does not work

function App() {
  const [base, setBase] = createSignal('old');
  return <Router base={base()}>
    <button onclick={() => setBase('new')}>Change base</button>
    <Link href="/about">about</Link>
    <Routes />
  </Router>
}

After change base to new then call useNavigator()('/about') still navigate to /old/about

Expose the entire router state in data function

I guess it would be more of a discussion than an issue, but while working on the real world example rewritten with solid app router, I encountered a situation where I'd like to access the router.location and listen to the change within the data function.

I thought about just using the useRouter and access router.location but it turns out we can't access this context in data functions. Digging a bit into the source code, I figured you manually generate getters for query and params to be passed into the data function props.

Is there a reason we just don't feed the whole state proxy into the data function here:

https://github.com/ryansolid/solid-app-router/blob/bb251041c245ed2247cbb1885f16acf981c11e70/src/index.tsx#L205-L212

Like so:

return levels[i].handler.data!(state); 

I did patch it locally and it seems to work and since the proxy already exist I don't think we pay any perf price. That being said, I guess we are exposing more than the user could ever need.. And I would understand if you'd prefer discarding that issue.

Go to previous route

Hi.

For example, as a user I can navigate to some edit page from different routes, and after changes is applied, I need to navigate back to previous route. Is there any way to do this?

If I remember right, in react-router there was a state that can be anything (for example state: { prev: location.pathname }). But state here is just a string or null. And looks like it doesn't work (or I don't understand something). navigate('/users/edit', { state: location.pathname }) after this, I've still got null state on edit page.

Changing URL directly got 404 not found

Hello I have some problem when directly change the URL in URL bar or refresh page and it got "404 Not Found" but when I have changed URL by clicking on <Link> then it show the component normally

It's only affected to sub-route such as "/users", "/2", "/1" but the route "/" is perfectly fine.

Before refresh page
image

After refresh page
image

My project environment

  • snowpack.js ^3.7.1
  • solid.js ^1.0.0
  • solid-app-router: ^0.0.51

*Edit
This is my snowpack config file

 * @type {import('snowpack').SnowpackConfig}
 */
const config = {
  mount: {
    public: "/",
    src: "/assets"
  },
  packageOptions: {
    installTypes: true,
    NODE_ENV: true
  },
  devOptions: {
    out: "dist",
    open: "none",
    bundle: true
  },
  buildOptions: {
    clean: true,
    out: "dist"
  },
  plugins: ["@snowpack/plugin-typescript", "@snowpack/plugin-babel", "@snowpack/plugin-postcss"],
  optimize: {
    bundle: true,
    minify: true,
    target: "es2020",
    treeshake: true,
    splitting: true
  }
};

module.exports = config;

Error in `normalize(path)` function when `path` is `undefined`

When I run my app in development mode (with vite-plugin-solid) I run into an issue where normalize function (found in utils.js) receives undefined as parameter. (This happens before path becomes / or any other string)

The code in normalize function doesn't check if path is a string and is trying to use replace method which causes runtime error.

I don't see this error when I serve a built application, so it might be related to solid-refresh

Here's my App with the routing tree:

import { Suspense, lazy } from 'solid-js'
import { Router, Routes, Route } from 'solid-app-router'

import AppHeader from './components/AppHeader'
import AppFooter from './components/AppFooter'

import './app.css'

const Home = lazy(() => import('./screens/home/Home'))

const ScreenOne = lazy(() => import('./screens/screen-one/ScreenOne'))
const ViewOne = lazy(() => import('./screens/screen-one/ViewOne'))
const ViewTwo = lazy(() => import('./screens/screen-one/ViewTwo'))
const ViewThree = lazy(() => import('./screens/screen-one/ViewThree'))

const ScreenTwo = lazy(() => import('./screens/screen-two/ScreenTwo'))
const ScreenThree = lazy(() => import('./screens/screen-three/ScreenThree'))


const NotFound = () => {
  return (
    <main>
      <h2>404</h2>
      <p>Path not found.</p>
    </main>
  )
}

const App = () => {
  return (
    <div class="app">
      <Router>
        <AppHeader />
        <Suspense fallback="Loading...">
          <Routes>
            <Route path="/screen-one" element={ <ScreenOne /> }>
              <Route path="/view-one" element={ <ViewOne /> } />
              <Route path="/view-two" element={ <ViewTwo /> } />
              <Route path="/view-three" element={ <ViewThree /> } />
              <Route path="/" element={ <ViewOne /> } />
            </Route>
            <Route path="/screen-two" element={ <ScreenTwo /> } />
            <Route path="/screen-three" element={ <ScreenThree /> } />
            <Route path="/" element={ <Home /> } />
            <Route path="/*all" element={ <NotFound /> } />
          </Routes>
        </Suspense>
        <AppFooter />
      </Router>
    </div>
  )
}

export default App

AppHeader component contains Link components:

import { Link } from 'solid-app-router'

const AppHeader = () => {
  return (
    <header class="app-header">
      <h1><Link href="/">Home</Link></h1>
      <nav class="app-navigation">
        <ul>
          <li><Link href="/screen-one">Screen One</Link></li>
          <li><Link href="/screen-two">Screen Two</Link></li>
          <li><Link href="/screen-three">Screen Three</Link></li>
        </ul>
      </nav>
    </header>
  )
}

export default AppHeader

Attached is a debugging screenshot:
Screenshot from 2021-12-06 07-52-00

Allow useMatch take RouteDefinition object instead of path.

Basically I want to know which route is currently active outside of <Routes/>, so I can for example change nav bar styles on specific page, currently this is hard to do correctly. So my proposed solution is to allow useMatch take RouteDefinition object so this works:

const LIST_ROUTE = {
  path: '/list',
  component: List,
  children: [
    { path: '/' },
    { path: '/apples' },
    { path: '/oranges' },
    { path: '/banana' },
  ],
}

const routes: RouteDefinition[] = [
  {
    path: '/',
    component: Home,
  },
  LIST_ROUTE,
]

export const App = () => {
  const Routes = useRoutes(routes)
  const isListRoute = useMatch(LIST_ROUTE)

  return (
    <div>
      <Routes />
      <BottomNavBar hide={isListRoute()} />
    </div>
  )
}

Alternative today is to use

const isListRoute = useMatch(() => '/list/*') // Matches everything and is not a correct solution
// or way more complicated
const isListRoute = useMatch(() => '/list/')
const isListAppleRoute = useMatch(() => '/list/apple') 
const isListOrangeRoute = useMatch(() => '/list/orange') 
const isListBananaRoute = useMatch(() => '/list/banana') 
const isListRouteActive= createMemo(() => isListRoute() && isListAppleRoute() && isListOrangeRoute() && isListBananaRoute())

There might be some other way to solve this like having <Routes /> fire change event with matched route id.

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'then')

    at routing.js:231
    at untrack (solid.js:401)
    at navigateFromRoute (routing.js:192)
    at routing.js:246
    at Object.fn (RouteGuard.tsx:52)
    at runComputation (solid.js:593)
    at updateComputation (solid.js:578)
    at runTop (solid.js:654)
    at runUserEffects (solid.js:753)
    at solid.js:714

RouteGaurd line 52 is just a navigate('/login')

I am using the latest solid-app-router and solid (v1.3.1)

Which points to https://github.com/solidjs/solid-app-router/blob/ea0edc85502225ab62085014e9fb309df351634f/src/routing.ts#L338

//From dev tools
if (resolvedTo !== current || nextState !== state()) {
                if (isServer) {
                    if (output) {
                        output.url = resolvedTo;
                    }
                    setSource({ value: resolvedTo, replace, scroll, state: nextState });
                }
                else {
                    const len = referrers.push({ value: current, replace, scroll, state });
                    start(() => {
                        setReference(resolvedTo);
                        setState(nextState);
                    }).then(() => { // Here is the error
                        if (referrers.length === len) {
                            navigateEnd({
                                value: resolvedTo,
                                state: nextState
                            });
                        }
                    });
                }
            }

default page when parent is click

Hi is this possible?
From
image
To
image

I want to achieve is when i click the parent link. then the first(or any) child should be the default page to show.

Thanks for help.

MergeProps in RouteDefinitions data result in Proxy error

Good day, library maintainers!

I was playing around with this router (and solid in general) and wanted to say thank you for this awesome library, and ecosystem you are working on. This is a breath of fresh air and I am definitely looking forward to incorporate Solid into my work!

I looked into the source code for this router and found out how to use data object. But I found out that if you are using mergeProps in the data property in route definitions it's breaking with Proxy error, but if I just return new object (without mergeProps) it's working as intended.

I am not sure it's a solid-app-router bug or bug in the main library, but I didn't encounter this before, while using solid without this router.

Error:
'ownKeys' on proxy: trap result did not include 'Symbol(state-proxy)'

Repro link

I might've misunderstood something with mergeProps so it might not be a bug at all.

Vite, ssr => Error [ERR_REQUIRE_ESM]

Trying to setup ssr with vite. Created server.ts as described in solid/vite docs/examples but at

render = (await vite.ssrLoadModule('/src/entry-server.tsx')).render; // renderToStringAsync inside

got error:

Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /home/rootfs/var/www/thesite/thesite/node_modules/solid-app-router/dist/index.js
require() of ES modules is not supported.
require() of /home/rootfs/var/www/thesite/thesite/node_modules/solid-app-router/dist/index.js from /home/rootfs/var/www/thesite/thesite/node_modules/vite/dist/node/chunks/dep-11db14da.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.
Instead rename index.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from /home/rootfs/var/www/thesite/thesite/node_modules/solid-app-router/package.json.

    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1089:13)
    at Module.load (internal/modules/cjs/loader.js:937:32)
    at Function.Module._load (internal/modules/cjs/loader.js:778:12)
    at Module.require (internal/modules/cjs/loader.js:961:19)
    at require (internal/modules/cjs/helpers.js:92:18)
    at nodeRequire (/home/rootfs/var/www/thesite/thesite/node_modules/vite/dist/node/chunks/dep-11db14da.js:73337:17)
    at ssrImport (/home/rootfs/var/www/thesite/thesite/node_modules/vite/dist/node/chunks/dep-11db14da.js:73290:20)
    at eval (/src/AppDocument.tsx:18:31)
    at instantiateModule (/home/rootfs/var/www/thesite/thesite/node_modules/vite/dist/node/chunks/dep-11db14da.js:73323:166)

Could you help with it?

Thank you!

Doc: remove useRoutes hook from the README JSX example

It seems the JSX example in the README contains the useRoutes hook when it shouldn't be needed for it and will cause confusion for incoming new users :)

...
import { Router, Routes, Route, Link } from "solid-app-router";
...

function App() {
  const Routes = useRoutes(routes); <--- To remove
  return (
    <>
      <h1>Awesome Site</h1>
      <Link class="nav" href="/">
        Home
      </Link>
      <Link class="nav" href="/users">
        Users
      </Link>
      <Routes>
        <Route path="/users" element={<Users />} />
        <Route path="/users/:id" element={<User />}>
          <Route path="/" element={<UserHome />} />
          <Route path="/settings" element={<UserSettings />} />
          <Route path="/*all" element={<UserNotFound />} />
        </Route>
        <Route path="/" element={<Home />} />
        <Route path="/*all" element={<NotFound />} />
      </Routes>
    </>
  );
}

SSR with router

Hi all, I apologize in advance for the translation.

I am trying to start writing in this wonderful framework. I have set up a project build for server side rendering. Everything works fine for me. But I decided to use this router to move forward. Client-side works fine, but after connecting the library, server-side stopped building.

ERROR in ./node_modules/solid-app-router/dist/index.js 503:15-23
export 'template' (imported as 'template') was not found in 'solid-js/web' (possible exports: Assets, Dynamic, ErrorBoundary, For, HydrationScript, Index, Match, NoHydration, Portal, Show, Suspense, SuspenseList, Switch, createComponent, escape, generateHydrationScript, getHydrationKey, isServer, mergeProps, pipeToNodeWritable, pipeToWritable, renderToString, renderToStringAsync, resolveSSRNode, spread, ssr, ssrBoolean, ssrClassList, ssrHydrationKey, ssrSpread, ssrStyle)
@ ./src/shared/App.tsx 14:0-42 79:27-33
@ ./src/server/index.tsx 10:0-35 34:39-42

ERROR in ./node_modules/solid-app-router/dist/index.js 652:4-10
export 'insert' (imported as 'insert') was not found in 'solid-js/web' (possible exports: Assets, Dynamic, ErrorBoundary, For, HydrationScript, Index, Match, NoHydration, Portal, Show, Suspense, SuspenseList, Switch, createComponent, escape, generateHydrationScript, getHydrationKey, isServer, mergeProps, pipeToNodeWritable, pipeToWritable, renderToString, renderToStringAsync, resolveSSRNode, spread, ssr, ssrBoolean, ssrClassList, ssrHydrationKey, ssrSpread, ssrStyle)
@ ./src/shared/App.tsx 14:0-42 79:27-33
@ ./src/server/index.tsx 10:0-35 34:39-42

ERROR in ./node_modules/solid-app-router/dist/index.js 654:4-10
export 'effect' (imported as 'effect') was not found in 'solid-js/web' (possible exports: Assets, Dynamic, ErrorBoundary, For, HydrationScript, Index, Match, NoHydration, Portal, Show, Suspense, SuspenseList, Switch, createComponent, escape, generateHydrationScript, getHydrationKey, isServer, mergeProps, pipeToNodeWritable, pipeToWritable, renderToString, renderToStringAsync, resolveSSRNode, spread, ssr, ssrBoolean, ssrClassList, ssrHydrationKey, ssrSpread, ssrStyle)
@ ./src/shared/App.tsx 14:0-42 79:27-33
@ ./src/server/index.tsx 10:0-35 34:39-42

ERROR in ./node_modules/solid-app-router/dist/index.js 654:17-29
export 'setAttribute' (imported as 'setAttribute') was not found in 'solid-js/web' (possible exports: Assets, Dynamic, ErrorBoundary, For, HydrationScript, Index, Match, NoHydration, Portal, Show, Suspense, SuspenseList, Switch, createComponent, escape, generateHydrationScript, getHydrationKey, isServer, mergeProps, pipeToNodeWritable, pipeToWritable, renderToString, renderToStringAsync, resolveSSRNode, spread, ssr, ssrBoolean, ssrClassList, ssrHydrationKey, ssrSpread, ssrStyle)
@ ./src/shared/App.tsx 14:0-42 79:27-33
@ ./src/server/index.tsx 10:0-35 34:39-42

ERROR in ./node_modules/solid-app-router/dist/index.js 720:0-14
export 'delegateEvents' (imported as 'delegateEvents') was not found in 'solid-js/web' (possible exports: Assets, Dynamic, ErrorBoundary, For, HydrationScript, Index, Match, NoHydration, Portal, Show, Suspense, SuspenseList, Switch, createComponent, escape, generateHydrationScript, getHydrationKey, isServer, mergeProps, pipeToNodeWritable, pipeToWritable, renderToString, renderToStringAsync, resolveSSRNode, spread, ssr, ssrBoolean, ssrClassList, ssrHydrationKey, ssrSpread, ssrStyle)
@ ./src/shared/App.tsx 14:0-42 79:27-33
@ ./src/server/index.tsx 10:0-35 34:39-42

Suspense & Solid app router together will break the suspense fallback.

Suspense & Solid app router together will break the suspense fallback if you click on a link where a resource is loading.
The page will be blocked instead until the resource is done loading.
I'm guessing this isn't expected behavior.

Example:

//App.js
export default () => {
  const Routes = useRoutes(routes);

  <Suspense fallback={<p>Loading Page...</p>}>
    <Routes />
  </Suspense>
};

//Dashboard.js
export default () => {
	const [data] = createResource(() => {
		return new Promise((resolve) => {
			setTimeout(() => {
				resolve('DATA LOADED');
			}, 3000);
		});
	});

	return <div>Dashboard Page {data()}</div>;
};

Data fetcher function is executed multiple times

I am trying out solidjs with solid-start. Here's the repo: https://github.com/HriBB/solid-started

I have two pages:

Homepage

// src/pages/index.data.ts
import { createResource } from 'solid-js'

export type Data = {
  page: string
  simulateFetch: boolean
  sleep: number
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

const simulateFetch = async () => {
  console.log('fetch home data')

  const ms = 1000
  await sleep(ms)
  return {
    page: 'home',
    simulateFetch: true,
    sleep: ms,
  }
}

export default function HomeData() {
  const [data] = createResource(simulateFetch)
  return data
}

// src/pages/index.tsx
import { Resource } from 'solid-js'
import { useData } from 'solid-app-router'

import { Data } from './index.data'
import { Hero } from '~/components/home/hero'
import { CallToAction } from '~/components/home/call-to-action'
import { GettingStarted } from '~/components/home/getting-started'
import { LoremIpsum } from '~/components/home/lorem-ipsum'
import { Pricing } from '~/components/home/pricing'

export default function Home() {
  const data = useData<Resource<Data>>()
  return (
    <div class="leading-normal tracking-normal text-white gradient">
      <div text="center" p="t-20">
        {JSON.stringify(data(), null, 2)}
      </div>
      <Hero />
      <LoremIpsum />
      <GettingStarted />
      <Pricing />
      <CallToAction />
    </div>
  )
}

Blog

// src/pages/blog.data.ts
import { createResource } from 'solid-js'

export type Data = {
  page: string
  simulateFetch: boolean
  sleep: number
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

const simulateFetch = async () => {
  console.log('fetch blog data')

  const ms = 1000
  await sleep(ms)
  return {
    page: 'blog',
    simulateFetch: true,
    sleep: ms,
  }
}

export default function BlogData() {
  const [data] = createResource(simulateFetch)
  return data
}

// src/pages/blog.tsx
import { Resource } from 'solid-js'
import { useData } from 'solid-app-router'

import { PostList } from '~/components/post'
import { Content } from '~/components/ui'
import { Data } from './blog.data'

export default function Blog() {
  const data = useData<Resource<Data>>()
  return (
    <Content>
      {JSON.stringify(data(), null, 2)}
    </Content>
  )
}

When I navigate back and forth between home and blog, simulateFetch function is execute multiple times. Here's the console.log:

client.ts:22 [vite] connecting...
client.ts:52 [vite] connected.
index.data.ts:12 fetch home data
blog.data.ts:12 fetch blog data
blog.data.ts:12 fetch blog data
blog.data.ts:12 fetch blog data
index.data.ts:12 fetch home data
index.data.ts:12 fetch home data
blog.data.ts:12 fetch blog data
blog.data.ts:12 fetch blog data
index.data.ts:12 fetch home data
index.data.ts:12 fetch home data
...

Is this a bug or am I doing something wrong?

Default value of `href`

Could you add an option for empty links. Links that could be used as buttons, for example.
Then usually don't need href. But a link must have href, so there's a trick with javascript:void(0).

Input

<Link>To the moon</Link>

Output

<a href="javascript:void(0)">To the moon</a>

The idea is to set href to javascript:void(0) as a default value.

useLocation and URL fragment?

I'm trying to create Facebook manual authentication flow using solid.js featuring solid-app-router. For response_type=token, Facebook API redirect url is called having access token data in URL Fragment. More info can be found here.

How can I parse URL Fragment using solid-app-router hooks? I suppose useLocation should return location with hash as a URL Fragment, right? At least my simple code returns hash as an empty string.

For url localhost:3000/auth-callback/?#access_token=<lots_of_characters> and the Component

import { Component } from "solid-js";
import { useLocation } from "solid-app-router";

const AuthCallback: Component = () => {
  const location = useLocation();

  return (
    <div>{JSON.stringify(location)}</div>
  );
};

export default AuthCallback;

the output is: {"pathname":"/auth-callback","search":"","hash":"","state":null,"key":"","query":{}}

Using `useNavigate()` threw an error

When call the useNavigate function, useRouter() cannot find a router context.

Uncaught Error: Make sure your app is wrapped in a <Router /> arises, but the app is certainly wrapped in a Router component.

Here is some of the code:

// index.tsx
import { render } from 'solid-js/web'
import { Router, useRoutes } from 'solid-app-router'
import { Component, lazy } from 'solid-js'

const routes = [
 {
   path: '/',
   component: lazy(() => import('./pages/Home')),
   children: [
     {
       path: '/',
       component: lazy(() => import('./pages/scripts/ScriptList')),
     },
     {
       path: '/script/add',
       component: lazy(() => import('./pages/scripts/ScriptAdd')),
     },
   ],
 },
]

const App: Component = () => {
 const Routes = useRoutes(routes)
 return <Routes />
}

render(
 () => (
   <Router>
     <App />
   </Router>
 ),
 document.getElementById('root')
)

// other.tsx
import { useNavigate } from 'solid-app-router'

const fn = () => {
 const navigate = useNavigate()
 navigate('/script/add')
}

Is anything wrong in this?

Document how components in nested routes work

This one threw me off for a while until I understood how it works. In the routes definition, when a route has children, its component must contain a Route component for those children to be rendered.

For example;

import { Route, Router } from 'solid-app-router';
import { lazy } from 'solid-js';
import { render } from "solid-js/web";

const routes = [
  {
    path: '/my-page',
    component: lazy(() => import('./pages/my-page')),
  },
  {
    path: '/my-page/:slug',
    component: Route, // <-- this is what I was missing
    children: [
      {
        path: '/',
        component: lazy(() => import('./pages/my-page/[slug]')),
      },
      {
        path: '/sub-page',
        component: lazy(() => import('./pages/my-page/[slug]/sub-page')),
      },
    ],
  },
];

const App = () => (
  <Router routes={routes}>
    <Route />
  </Router>
);

render(App, document.getElementById('app'));

In the example I've used Route as the actual component but it's enough for the route's component to contain a <Route />.

It's obvious in retrospect but after using so many different router implementations over the years I wasn't sure if it was automagically injected or just how flexible nesting is.

Note: I know there's no formal documentation yet, so this is just a tip for when you get to it!

Route Loads before the lazy Route Component Finishes fetching.

The path changes instantly but the Route component is still fetching, because of that, a flash of the blank page occurs.
How can I achieve a blocking way of fetching the Route Component, so when navigating to another lazy page it doesn't show a blank page but stays on the current one, and maybe shows a progress bar/line like on youtube by using a "hook" useIsFetching(route?), I Looked into Data functions but I think it doesn't suit this use case because I just want to load a Route Component and no data alongside it.

Example https://codesandbox.io/s/solid-router-185j1

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.