Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
rejhgadellaa opened this issue May 24, 2022 · 5 comments
Open

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

rejhgadellaa opened this issue May 24, 2022 · 5 comments
Labels
enhancement New feature or request

Comments

@rejhgadellaa
Copy link

rejhgadellaa commented May 24, 2022

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!

@rejhgadellaa rejhgadellaa added the enhancement New feature or request label May 24, 2022
@developit
Copy link
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>
    );
  }
}

@rejhgadellaa
Copy link
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.

@developit
Copy link
Member

developit commented May 29, 2022

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

@rejhgadellaa
Copy link
Author

rejhgadellaa commented Jun 9, 2022

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!

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants