preactjs/wmr

Router: Easier access to route() in class components

rejhgadellaa opened this issue · 5 comments

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!

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>
    );
  }
}

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.

@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;

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!

@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>
  );
}