Skip to content

Router: Easier access to route() in class components #930

Open

Description

@rejhgadellaa

Is your feature request related to a problem? Please describe.

I want to switch to preact-iso's router but find it's very cumbersome to get a hold of its route() function in class components (as I can't use useRoute()).

I've managed to do it, but it's not pretty:

import { Component } from 'preact';
import { Router } from 'preact-iso';

class C extends Component {
  render() {
    return (
      <Router.Provider.ctx.Consumer>
        { ctx => (
          <div onClick={ event => ctx.route('/some/place') }>Click me!</div>
        )}
      </Router.Provider.ctx.Consumer>
    );
  }
}

Describe the solution you'd like

In preact-router, I could just do:

import { route } from 'preact-router'

and then use route() pretty much anywhere.

My guess is that preact-iso probably switched to useRoute() for a reason and that the import { route } isn't entirely 'compatible' anymore, but I would appreciate it if there was a better way to get a hold of it :)

Thanks!

Activity

  1. developit commented on May 25, 2022

    @developit
    Member

    There are a few ways to do this.

    1. Prefer HTML links

    Links are automatically intercepted and will invoke route() on any Router with a matching or default route. In any case where you can use an HTML link, it's usually preferable to do so for accessibility reasons:

    <a href="/some/place">Click me!</a>

    2. Classes can use context too!

    The static .context property allows class components to declare a dependency on a "new" Context (one created via createContext). This lets you access the router context on your class component:

    import { Component } from 'preact';
    import { LocationProvider } from 'preact-iso';
    
    class C extends Component {
      static context = LocationProvider.ctx;
    
      render() {
        return (
          <div onClick={ event => this.context.route('/some/place') }>Click me!</div>
        );
      }
    }

    3. Hack: use the hook to access context

    Use the provided useLocation hook to access context, then access it via a ref from your class component:

    import { Component, createRef } from 'preact';
    import { forwardRef } from 'preact-forwardref';
    import { useLocation } from 'preact-iso';
    
    const WithLocation = forwardRef(({ ref }) => {
      ref.current = useLocation();
    });
    
    class C extends Component {
      router = createRef();
    
      render() {
        return (
          <>
            <WithLocation ref={this.router} />
            <div onClick={ event => this.router.current.route('/some/place') }>Click me!</div>
          </>
        );
      }
    }

    4. Hack: Use hooks inside your class component

    Hooks work inside class components in Preact, it's just not something we document because it might change at some point in the future. However, as it currently stands, you can use hooks inside of a class (and I believe also from within a class constructor!):

    import { Component } from 'preact';
    import { useLocation } from 'preact-iso';
    
    class C extends Component {
      render() {
        const { route } = useLocation();
        return (
          <div onClick={ event => route('/some/place') }>Click me!</div>
        );
      }
    }
  2. rejhgadellaa commented on May 25, 2022

    @rejhgadellaa
    Author

    Thanks for the detailed response!

    1. Yeah I agree that <a /> is prefered but there are some cases where I need to route() after the user clicks something, I do some work, etc. But absolutely, yes.

    2. Doesn't seem to work for me. I tried your example and somehow this.context is an object with only ["__cC0","__cC1"] as keys. No route.

    I must be doing something wrong as the static get context() isn't even being called, it seems :(

    For completeness, this is render() in index.js:

    <div id="app">
      <LocationProvider>
        <ErrorBoundary>
    
          <Router onRouteChange={this._onRouteChange}>
            <Home path="/" />
          </Router>
    
        </ErrorBoundary>
      </LocationProvider>
    </div>

    And then, simplified, what I have in Home:

    import { Component } from 'preact';
    import { LocationProvider } from 'preact-iso';
    
    export default class Home extends Component {
    
      static get context() {
        return LocationProvider.ctx;
      }
    
      render() {
        return (
          <div onClick={ event => console.log( Object.keys(this.context) ) }>Click me!</div>
        );
      }
    }
    1. Thought about doing something like this but thought I'd file a feature request before resorting to hacks :)

    2. This actually works. But yeah, it's a hack.

  3. developit commented on May 29, 2022

    @developit
    Member

    @rejhgadellaa ah! sorry, I messed up the example for #2 - it should be contextType (singular):

    class C extends Component {
      static get contextType() { return LocationProvider.ctx; }
    
      render() {
        // ...
      }
    }

    or, if you'd like to avoid the getter:

    class C extends Component {
      render() {
        // ...
      }
    }
    
    C.contextType = LocationProvider.ctx;
  4. rejhgadellaa commented on Jun 9, 2022

    @rejhgadellaa
    Author

    Sorry didn't have time to test this until now.

    It works! Thanks!

    As long as I get the context in a component residing in the <LocationProvider />, I can use method #2.

    But when I want to route() from the component that contains the <LocationProvider />, I have to fall back to a hack. The use-case here is that I want <App /> to check auth and reroute if the user is not authed.

    I used to be able to do something like (real quick-and-dirty pseudo-code):

    import { route } from 'preact-router';
    
    class C extends Component {
    
      componentDidMount() {
    
        ( async () => {
          const isAuthed = await checkAuth()
          if (!isAuthed) route('/login');
        })();
    
      }
    
      render() {
        return (
          <LocationProdiver>
            <ErrorBoundary>
              <Router />
            </ErrorBoundary>
          </LocationProdiver>
        )
      }
    
    }

    but now I have to pull some shenanigans to get a hold of route outside the <LocationProvider />

    So my feature request stands, I guess? Would be really nice if we could just import the function and use it anywhere :)

    I didn't check, but maybe I could probably do something like calling history.pushState() to trigger the route?

    Anyway, thanks for the help so far!

  5. developit commented on Jun 9, 2022

    @developit
    Member

    @rejhgadellaa my suggestion here would be to move LocationProvider out of all of your components, and have one small root component that only renders the providers for your app:

    function C() {
      return (
        <ErrorBoundary>
          <Router />
        </ErrorBoundary>
      );
    }
    
    export default function App() {
      return (
        <LocationProvider>
          <C />
        </LocationProvider>
      );
    }
  6. mike-lischke commented on Feb 25, 2025

    @mike-lischke

    Unfortunately, nothing of the given solutions is usable in a TypeScript project.

    1. Links are not every time an option.
    2. ctx in static context = LocationProvider.ctx; is unkown. Missing correct type definition.
    3. Gives a number of other TS errors (no return type, invalid ref type).
    4. Throws an error:
    Uncaught TypeError TypeError: Cannot read properties of null (reading 'context')
        at x (node_modules/preact/hooks/src/index.js:366:36)
    

    The only way to get this to work is to suppress all TS and eslint errors in this file, which is the real hack.

    This is what I use now (and suppress TS errors for Router.Provider, as well as context):

                <Router.Provider.ctx.Consumer>
                    {(context) => {
                        return (
                            <Container
                                orientation={Orientation.LeftToRight}
                                className={className}
                            >
                                <TreeGrid
                                    ...
                                    onRowSelected={(row) => {
                                        const data = row.getData() as ISection;
                                        // eslint-disable-next-line @typescript-eslint/no-unsafe-call
                                        context.route(data.urlPath);
                                    }}
                                />
                            </Container>
                        );
                    }}
                </Router.Provider.ctx.Consumer>
  7. pallosp commented on May 27, 2025

    @pallosp

    Option 2 works when type checking is disabled. I filed preactjs/preact-iso#88 for the missing declaration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions