nandorojo / solito

🧍‍♂️ React Native + Next.js, unified.
https://solito.dev
MIT License
3.54k stars 181 forks source link

How to get the current route? #235

Closed nikkwong closed 1 year ago

nikkwong commented 2 years ago

I've seen some discussions in the past issues but still have not landed on anything concrete. I have a navigation list and would like to mark one as active based on its presence in the url route. I'm not sure how I can do that without knowing the current route. Any help here? Thanks.

nikkwong commented 2 years ago

Also.. I have some subset of components that can be mounted at different routes. Let's say they're games. A game like galaga could be mounted at /games/galaga or /admin/games/galaga. The game then has routing internally, like /games/galaga/scene-1 or /admin/games/galaga/scene-2.

At the galaga path (mounted at either /games or /admin, I'm not sure how i can relatively redirect to individual scenes in the game, since router.push only allows absolute navigation and I don't know where the user currently is in the absence of the current route information.

alidcast commented 2 years ago

was just looking for this as well, see #10

was exposed in expo-router via useHref #59

@nandorojo whether included in API or not, it'd be worth documenting reasoning and possible workarounds as this is standard navigation pattern

nandorojo commented 2 years ago

I think you can use expo router's solution, but outside of that getting the URL isn't very safe on native.

So far, I've resorted to using react context to wrap certain screens and such to note where they are. I know it's imperfect. But I'm hesitant to expose an API for this. There are unsafe ones used by Showtime's solito repo on native which you could try in user land.

nikkwong commented 2 years ago

Thank you for the responses! useHref only works in RN though, right? How could I do this on any of the react components in the packages/app dir? There needs to be.. something. Not being able to show which tab is active (in relation to its presence (or lack thereof) in the URL) is going to be tough to explain to my stakeholders.

So far, I've resorted to using react context to wrap certain screens and such to note where they are. I know it's imperfect. But I'm hesitant to expose an API for this.

It would be awesome to understand how this works.

Thank you :)

alidcast commented 2 years ago

How could I do this on any of the react components in the packages/app dir? Pass it down as props, or as suggested above, via React context

I would make components in shared app directory accept higher level property, e.g. isActive, rather than router implementation details

useHref only works in RN though, right?

yes, there's no consolidate API in Solito, given reasoning mentioned above so you'd use expo and next.js respective router APIs

nikkwong commented 2 years ago

Yeah.. makes sense. How would you do this though? I was thinking in the next/pages/admin/games/galaga.ts file you could do something like...

// next/pages/admin/games/galaga.ts
import GalagaComponentWrapper from 'app/features/games/index.tsx'
// add the current route here:
export GalagaComponentWrapper('/admin/games/galaga')

Then in the galaga component (which could be mounted in multiple places):

// app/features/games/galaga/index.tsx
export function GalagaComponentWrapper(path) {
    return function GalagaComponent() {
        const route = useRouter();
        console.log(path); // available here..
    }
}

Is this what you're suggesting? Or something else? I'm not sure if this is a good solution, and I'd have to come up with something different for RN. Maybe there is also a way to use the app/features/provider directory, but i'm not sure.

Thank you!

nikkwong commented 2 years ago

Welp.. actually not sure if this is a good solution. If there is a parameter in the next route, then you wouldn't be able to statically pass the current route in, ie.

next/pages/admin/[adminId]/games/galaga.ts
import GalagaComponentWrapper from 'app/features/games/index.tsx'
// The current route is not a static string, it's now:
export GalagaComponentWrapper('/admin/[adminId]/games/galaga')

I guess then in the component you'd have to get the current route by getting the parameter from useParam('adminId') and then passing it into that string.. just seems like a complicated hack, now. Thoughts? Sorry to bother!

alidcast commented 2 years ago

one alternative is to pass the prop when you're hooking up that component in each frameworks filesystem router

function Page() {
 const { pathname } = useRouter()  // or useHref() 
  const isActive = pathname === '/foo'
 return <SomeView isActive={isActive} />
}

I would start with that, and if that gets tiresome you can create your own application abstraction for grabbing pathnames

nikkwong commented 2 years ago

Ah ok, although it still feels like a hack and very verbose.. I think this solution would work for now, will report back. Thanks for the idea!

nikkwong commented 2 years ago

Ok, I think the best way to do this is by using the React Context API. For next, in provider/navigation/index-web.tsx we can add the route context on the root:

export const RouteContext = React.createContext<{
  path: string
  asPath: string
}>({ path: '', asPath: '' })

export const NavigationProvider = ({
  children,
}: {
  children: React.ReactElement
}) => {
  const router = useRouter()
  return (
    <RouteContext.Provider
      value={{ path: router.pathname, asPath: router.asPath }}
    >
      {children}
    </RouteContext.Provider>
  )
}

And then in a child component:

function AnyChildComponent = () => {

const TaskLayout: NextPage = ({ children, Component }) => {
  const { path, asPath } = useContext(RouteContext)
}

I will have to revisit when we shift to working on mobile to see if there are bottlenecks that occur at that stage in regards to the idea of relying on route paths.

nandorojo commented 2 years ago

For context, this has all been discussed at these places too, which might help: #10 #62 #45

I still don't have a fantastic answer, other than the following.

Context Solutions

// admin-context.tsx

import { createContext } from 'react'
export const IsAdminContext = createContext(false)

export const IsAdminProvider = ({ children }) => {
  return <IsAdminContext.Provider value={true}>{children}</IsAdminContext.Provider>
}

Now we just need a hook to know if it's an admin screen. On Native, we'll use the context, and on Web, the URL.

// use-is-admin.ts

import { IsAdminContext } from './admin-context'

export const useIsAdmin = () => useContext(IsAdminContext)
// use-is-admin.web.ts
import { useRouter } from 'next/router'

export const useIsAdmin = () => useRouter().pathname.includes('/admin')

As a final step, you'd wrap certain screens on native with IsAdminProvider, so that they know what kind of app they're in.

With rewrites

Alternatively, in <NavigationContainer />, you could add a redirect that changes /admin/user to something like /user?isAdmin=true.

Then, your use-is-admin on native could more simply be like this:

// use-is-admin.ts
import { createParam } from 'solito'

const { useParam } = createParam<{ isAdmin?: string }>()

export const useIsAdmin = () => {
  const [isAdmin] = useParam('isAdmin')

  return Boolean(isAdmin)
}
izakfilmalter commented 1 year ago

I am doing the following which is super hacky, but works. I am using this for my navigation bar to figure out if the route is active. This doesn't actually solve the core problem, but solves my problem which is just knowing if my top level nav link is active.

  1. Your Path and Screen name need to be the same, eg:
    const linking = {
      config: {
        screens: {
          Settings: '/settings',
        }
      }
    }
  2. useActiveSlug.ts

    import { useMemo } from 'react'
    import { O, pipe, RA } from '@steepleinc/fp'
    import { useNavigationState } from '@react-navigation/native'
    
    export function useActiveSlug() {
      const navigationState = useNavigationState((state) => state)
    
      const history = pipe(
        navigationState,
        O.fromNullable,
        O.map((x) => pipe(x.history, O.fromNullable)),
        O.flatten,
        O.getOrElse((): ReadonlyArray<unknown> => []),
      )
    
      return useMemo(
        () =>
          pipe(
            history as ReadonlyArray<{ key: string }>,
            RA.last,
            O.map((x) => pipe(x.key.split('-'), RA.head)),
            O.flatten,
            O.getOrElse(() => ''),
            (x) => x.toLowerCase(),
          ),
        [history],
      )
    }

    useActiveSlug.web.ts

    import { useMemo } from 'react'
    import { capitalizeFirstLetter, O, pipe, RA, Str } from '@steepleinc/fp'
    import { useRouter } from 'next/router'
    
    export function useActiveSlug() {
      const { pathname } = useRouter()
    
      return useMemo(
        () =>
          pipe(
            pathname,
            Str.split('/'),
            RA.lookup(1),
            O.getOrElse(() => ''),
            capitalizeFirstLetter,
          ),
        [pathname],
      )
    }