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

Expo Router Adapter / CJS Support #837

Open
izakfilmalter opened this issue Jan 1, 2025 · 18 comments
Open

Expo Router Adapter / CJS Support #837

izakfilmalter opened this issue Jan 1, 2025 · 18 comments
Labels
feature New feature or request

Comments

@izakfilmalter
Copy link

izakfilmalter commented Jan 1, 2025

I have stubbed out an adapter that would work for Expo Router:

import { Array, Option, pipe, Record } from 'effect'
import { router, type UnknownOutputParams, useGlobalSearchParams, useSegments } from 'expo-router'
import {
  type unstable_AdapterInterface,
  unstable_createAdapterProvider,
} from 'nuqs/dist/adapters/custom'

export function useGlobalQueryParams<TParams extends UnknownOutputParams = UnknownOutputParams>() {
  const params = useGlobalSearchParams<TParams>()
  const segments = useSegments()
  const urlParams = segments
    .filter((segment) => segment.startsWith('[') && segment.endsWith(']'))
    .map((segment) => segment.slice(1, -1))

  return Object.fromEntries(
    Object.entries(params).filter(([key]) => !urlParams.includes(key)),
  ) as TParams
}

function useNuqsExpoRouterAdapter(): unstable_AdapterInterface {
  const params = useGlobalQueryParams<Record<string, string>>()

  const updateUrl = (searchParams: URLSearchParams) => {
    const newParams = Object.entries(Object.fromEntries(searchParams))
    const oldParams = pipe(params, Record.toEntries)

    const paramsToRemove = pipe(
      oldParams,
      Array.filter(([oldKey]) =>
        pipe(
          newParams,
          Array.findFirst(([newKey]) => oldKey === newKey),
          Option.isNone,
        ),
      ),
      Array.map(([key]) => [key, undefined]),
    )

    router.setParams({
      ...Object.fromEntries(paramsToRemove),
      ...Object.fromEntries(newParams),
    })
  }

  return {
    searchParams: new URLSearchParams(params),
    updateUrl,
    rateLimitFactor: 2,
    getSearchParamsSnapshot: () => new URLSearchParams(params),
  }
}

export const NuqsExpoAdapter = unstable_createAdapterProvider(useNuqsExpoRouterAdapter)

The problem I am running into is expo uses the metro bundler. Metro doesn't support ESM yet, so everything has to be CJS. Is there away we can add CJS support back to this package?

@izakfilmalter izakfilmalter added the bug Something isn't working label Jan 1, 2025
@izakfilmalter
Copy link
Author

I did a quick and dirty cjs build of nuqs and it seems to work. I am gonna do some testing wiht Expo and wire everything up.

@franky47
Copy link
Member

franky47 commented Jan 1, 2025

Thanks for the contribution!

Unfortunately, adding CJS support back (it was removed in nuqs v2) is not planned, it's time for the ecosystem to move forward.

What's blocking Metro from allowing ESM, do you know? It feels like efforts should be directed towards this goal instead of keeping CJS alive (a format deprecated since Node 18), that would allow modern libraries running in Expo/Metro.

@izakfilmalter
Copy link
Author

Ya I'm fully with you. Metra has lagged behind for a long time. That's a beast to change and people are trying but you have to deal with Facebook. Do you mind if I publish a cjs fork for everyone using expo?

@izakfilmalter
Copy link
Author

Let me poke at it a bit more this morning. I might be able to get around it. Will let you know.

@izakfilmalter
Copy link
Author

Ok, did some bable config hacking. Got it working without cjs build. Running into issues on native now:
image

Basically location doesn't exist on native. Native needs to kinda run like server does. Gonna poke around and see what I can do.

@izakfilmalter
Copy link
Author

Ok, seems like on dev you have some code that isn't released yet that will get around the above issue. Testing that now.

@izakfilmalter
Copy link
Author

izakfilmalter commented Jan 1, 2025

Ok here is how I have this working for me. I built the next branch. I have the following adapter:

import { Array, Option, pipe, Record } from 'effect'
import { router, type UnknownOutputParams, useGlobalSearchParams, useSegments } from 'expo-router'
import {
  type unstable_AdapterInterface,
  unstable_createAdapterProvider,
} from 'nuqs/dist/adapters/custom'
import { useCallback } from 'react'

export function useGlobalQueryParams<TParams extends UnknownOutputParams = UnknownOutputParams>() {
  const params = useGlobalSearchParams<TParams>()
  const segments = useSegments()
  const urlParams = segments
    .filter((segment) => segment.startsWith('[') && segment.endsWith(']'))
    .map((segment) => segment.slice(1, -1))

  return Object.fromEntries(
    Object.entries(params).filter(([key]) => !urlParams.includes(key)),
  ) as TParams
}

function useNuqsExpoRouterAdapter(): unstable_AdapterInterface {
  const params = useGlobalQueryParams<Record<string, string>>()

  const updateUrl = useCallback(
    (searchParams: URLSearchParams) => {
      const newParams = Object.entries(Object.fromEntries(searchParams))
      const oldParams = pipe(params, Record.toEntries)

      const paramsToRemove = pipe(
        oldParams,
        Array.filter(([oldKey]) =>
          pipe(
            newParams,
            Array.findFirst(([newKey]) => oldKey === newKey),
            Option.isNone,
          ),
        ),
      )

      router.setParams({
        ...Object.fromEntries(paramsToRemove),
        ...Object.fromEntries(newParams),
      })
    },
    [params],
  )

  return {
    searchParams: new URLSearchParams(params),
    updateUrl,
    rateLimitFactor: 2,
    getSearchParamsSnapshot: () => new URLSearchParams(params),
  }
}

export const NuqsExpoAdapter = unstable_createAdapterProvider(useNuqsExpoRouterAdapter)

I followed the following post: expo/expo#30323 (comment) and add this to my babel.config.ts

    overrides: [
      {
        test: [/nuqs/],
        plugins: [
          'babel-plugin-transform-import-meta',
          'module:@reactioncommerce/babel-remove-es-create-require',
        ],
      },
    ],

I then had to import from nuqs/dist/* to get imports to resolve. Works on both native and web for expo router.

@franky47 what the timeline for releasing support for getSearchParamsSnapshot?

@franky47
Copy link
Member

franky47 commented Jan 1, 2025

I then had to import from nuqs/dist/* to get imports to resolve

Was that a TypeScript or a runtime issue? If runtime, I assume it's the Expo/Metro resolver that doesn't support package.json "exports" fields.

I have a few things I want to test on the React Router adapters before releasing 2.3.0 in GA (hopefully early next week when I'm back home from holidays), but there's [email protected] which you should be able to depend on right away.

@izakfilmalter
Copy link
Author

Metro spits out the following:

 WARN  Attempted to import the module "/node_modules/nuqs/dist/adapters/custom" which is not listed in the "exports" of "/node_modules/nuqs" under the requested subpath "./dist/adapters/custom". Falling back to file-based resolution. Consider updating the call site or asking the package maintainer(s) to expose this API.

@franky47
Copy link
Member

franky47 commented Jan 1, 2025

Hum that's weird, because we do define this export:

"./adapters/custom": {
"types": "./dist/adapters/custom.d.ts",
"import": "./dist/adapters/custom.js",
"require": "./esm-only.cjs"
},

Do you get the same error when importing useQueryState (or anything else) from nuqs in app code?

@izakfilmalter
Copy link
Author

Have to do this: import { useQueryState } from 'nuqs/dist'.

@franky47
Copy link
Member

franky47 commented Jan 1, 2025

@izakfilmalter
Copy link
Author

That breaks the babel hack to get around no cjs support.

@franky47 franky47 added feature New feature or request and removed bug Something isn't working labels Jan 1, 2025
@izakfilmalter
Copy link
Author

I think if you got rid of ./dist and put everything at the root of the package, it would work.

@franky47
Copy link
Member

franky47 commented Jan 1, 2025

Yeah I've been doing a lot of experiments to properly support various TypeScript configurations (see #708).

Putting everything at the top level might not be convenient in the monorepo setup, but it's worth a try.

@franky47
Copy link
Member

franky47 commented Jan 1, 2025

Here's a preview deployment with everything at the top level:

pnpm add https://pkg.pr.new/nuqs@64e3ba98bfcbbdfcc9c49741d6d2e7fa5269d869

@izakfilmalter
Copy link
Author

Seems like with that preview I run into the CJS issue again. I think importing from dist was letting me around it.

@franky47
Copy link
Member

franky47 commented Jan 1, 2025

Ok, thanks for the feedback, it also gives me weird behaviours in some TS configs, so I'll close that branch.

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

No branches or pull requests

2 participants