-
-
Notifications
You must be signed in to change notification settings - Fork 2
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
Custom useParams hook #433
Comments
I totally agree we need this. I was just thinking about this the other day. The whole number/string thing is a real downer lol. I think the majority of folks will use all number ids or all string ids. So I think it should be super simple for either of these. How about this? // Always returns numbers - no option argument
const {taskId} = useParamsInt()
// Always returns strings - no option argument
const {taskId} = useParamsStr()
// Escape hatch for a mixed bag
const {taskId} = useParamsInt()
const router = useRouter()
const projectId = router?.query.projectId as string |
@flybayer I think having more powerful useParamsCustom() which would take a prisma query like object (key -> boolean | function, where a function would be a parser function and true would just parseInt) would be quite useful, just to make it a bit more readable for some stuff. Also I'd change your example // Always returns strings - no option argument
const {id} = useParamsStr()
// Always returns numbers - no option argument
const {id} = useParamsInt()
// Always returns numbers - no option argument
const {taskId} = useParamsInt('taskId')
// Multiple in one
const {id, taskId} = useParamsInt(['id', 'taskId']) Also add the custom hook back with a bit different syntax // Custom parsers
const {id, taskId} = useParamsCustom({
id: true,
taskId: (raw) => parseInt(raw.split('-')[0], 16)
});
// Impl without last hook
const {id} = useParamsInt();
const {taskId as taskIdRaw} = useParamsStr('taskId');
const taskId = parseInt(taskIdRaw.split('-')[0], 16);
// Or
const {id} = useParamsInt();
const router = useRouter();
const taskIdRaw = router?.query.taskId as string;
const taskId = parseInt(taskIdRaw.split('-')[0], 16) |
What would this do Adding Also, anyone can create their own custom hooks like this and publish to npm, so something like this is different that something core in the CLI or server logic. |
By default we can't know what routing name the user chose
const {id} = useParamsInt();
const {fooBar} = useParamsInt('fooBar');
const {projectId, id} = useParamsInt(['projectId', 'id']); |
Oh, so I was thinking |
What would you do with invalid values that end up in the param? I don't think NaN is a contextually appropriate value to return for a URL param. Plus Most apps may want to just error out early if the route param is fails to cast to integer, but others may prefer to try to fix the value or do something custom with it. I don't think a hook we provide should ever encourage the app to try to do a lookup from a value that got truncated. Seems difficult for a single hook to try to satisfy all of those possible scenarios without adding clunkiness. |
We could just do that, seems easier than my implementation 😅
We could just return null if we get a NaN |
Returning |
Excellent points @MrLeebo Maybe the best way to "solve" this is switch ids to be strings by default, which I think we should do anyways. So then the remaining issue is that So maybe we could add |
I still think that most people would rather go with integer IDs for starters |
Not sure if this is inherited from next.js router, but I think params This might be an edge case, but what happens when param and query share the same name, e.g.
@MrLeebo I agree with you. URL params and query are basically user input and if we are using that input for the database query then it needs to be validated and sanitized. |
How about I think this is the output you intuitively expect. // router.query = {id: "123", somethingElse: "word"}
const params = useParams()
// params = {id: 123, somethingElse: "word"} We can do a regex test, and if it only has digits, attempt |
Not every number looking string should be converted to a number. For the first iteration, we could just return a string dictionary |
@anteprimorac I'm curious, when would you want a number to remain a string in this context? |
@flybayer For example if you have a list of locations and you want to filter them by postal code and you want to create a nice looking route like this |
Yeah that's a great example! What I would like the most is if Prisma would allow both string & number inputs (like Rails). But until then, I think our current best option is probably this: // Always returns numbers
const {taskId} = useParamsInt()
// Always returns strings
const {taskId} = useParamsStr() |
If you have defined that your model has an ID that has type Prisma is doing the right thing there because having a strictly typed database layer is right, and this framework can benefit from it. I thought that with Currently we have this code: const router = useRouter();
const id = parseInt(router?.query.id as string);
const [user] = useQuery(getUser, { where: { id } }); and with const { id } = useParams();
const [user] = useQuery(getUser, { where: { id: Number(id) } }); If you have more complex params, you can use a library like |
Yeah, unfortunately there's nothing we can do about the param/query conflict. That's deep inside Next.js itself. 😕 |
I was able to make a prototype for this. // utils/router.ts
import { useRouter } from 'blitz';
import { parse } from 'url';
import { getRouteRegex, getRouteMatcher } from 'next/dist/next-server/lib/router/utils';
export const useRouterParams = () => {
const router = useRouter();
const { pathname } = parse(router.asPath);
const params = getRouteMatcher(getRouteRegex(router.route))(pathname);
if (params) {
return params;
}
return {};
};
export const useRouterQuery = () => {
const router = useRouter();
const { query } = parse(router.asPath, true);
return query;
}; You can test it by dropping it into utils. Example of use: // app/users/pages/users/[id].tsx
import { useRouterParams } from 'utils/router';
export const User = () => {
const { id } = useRouterParams();
const [user] = useQuery(getUser, { where: { id: Number(id) } });
return user;
}; @flybayer do you think if this is something that could be added to blitz core? I am happy to create PR. |
Ok, here's some updated thoughts.
Essentially we just need a filter that solves each case. We can do this with one single hook: function useParams(type: 'string' | 'int' | 'array' = 'string') {
// ...
}
// Return all string params — excludes arrays
const {zipcode} = useParams()
// Return everything that passes parseInt — excludes everything else
const {taskId} = useParams('int')
// Return all array params — excludes everything else
const {slug} = useParams('array') What do you all think of this? |
I don't think that is intuitive to everyone because in that case output of that function depends on the type of param value. Also, I believe that validation, sanitization and casting of the user input should be more robust than just strings, integers and arrays and that it could be for any data handling for example forms validation. What you think about adding another hook const id = parseInt(useParam('id'), 10); // number
const slugs = useParam('slug', false); // string[]
const categories = useParam('cat', false).map(cat => parseInt(cat, 10)); // number[] @Zeko369 could you change the status of this issue to |
The thing that really bothers me about it is that it still passively just nulls out values when you request something you expect to be a number but you get something else instead. The ideal scenario, I think, would be if the type information was actually part of the route itself, so a type mismatch would simply prevent the route from matching at all. E.g. @flybayer is that something that could be a NextJS enhancement, or possibly even a blitz rule? |
@anteprimorac yeah, you're right — not super intuitive. We've come full circle — your last suggestion is basically the same as @Zeko369 very first post 😄 @MrLeebo that's something that could definitely be added to Next if their team is open to the idea. And yes, I think that probably could be added via Blitz. Might be tricky, but seems possible. Regarding validation/sanitization: I totally agree we need proper validation/sanitization, but this needs to happen inside query/mutation functions. If you use a query to fetch an item and that item doesn't exist in the db, it will either throw a not found error or return a result that indicates the same (up to the developer on this). All of the following should result in the same Not Found error:
So as long as you get the same end result, it doesn't matter how you get there in this case, right? All of the below approaches have the same result because
So I do like I think we would need to statically define the types somewhere // app/route-types.ts
export type = {
projectId: number
slug: string[]
} |
Let's try to come to a conclusion here :) Current: const router = useRouter()
const taskId = Number(router?.query.taskId)
const zipcode = router.zipcode as string
const slug = router.slug as string[] With blitz-js/blitz#574: const params = useRouterParams()
const taskId = Number(params.taskId)
const zipcode = params.zipcode as string
const slug = params.slug as string[] My final proposal for parsing & type guard: const taskId = useParamNumber('taskId') // return: number
const zipcode = useParamString('zipcode') // return: string
const slug = useParamArray('slug') // return: string[]
|
I created vercel/next.js#13534 for the type-safe routing idea, but the proposed hooks can approximate that feature by redirecting to the catch-all route if the param has a type mismatch instead of returning |
Actually, with blitz-js/blitz#574 the example should look like this: const params = useRouterParams() // every param type is string | string[]
const taskId = Array.isArray(params.taskId)
? Number(params.taskId[0])
: Number(params.taskId) // return: number
const zipcode = Array.isArray(params.zipcode)
? params.zipcode[0]
: params.zipcode // return: string
const slug = Array.isArray(params.slug)
? params.slug
: [params.slug] // return: string[] Your final proposal should resolve the I've been experimenting with support for yup validation schema, and currently, it can be used like this: type StatusFilter = 'active' | 'deleted';
const { status } = useRouterQuery({
validateSchema: new yup.object().shape({
status: yup
.string()
.oneOf<StatusFilter>(['active', 'deleted'])
.default('active')
.required(),
}),
}); Currently, I have to pass schema to every hook call, but instead of that, it would be better to define it in the scope of the page(maybe pushing it into react context), and then our hooks can pull it from it. What do you think about something like this? |
@MrLeebo, that's a nice thorough feature request! I think for now we need to stick with returning NaN from our hook because params will be empty during static prerendering. The actual error doesn't occur until runtime when we want to use that id to fetch data. If we through an error from the hook, then prerendering will fail. @anteprimorac I'm still not sold on client-side url query validation (vs server-side), but you're definitely welcome to open a new issue for that! |
@anteprimorac I unassigned you from this since blitz-js/blitz#574 doesn't solve this issue. But you're welcome to pick it back up if you want to also add the new param hooks. |
@flybayer yes, I agree that doing validation only on client-side is not good, but I would like to explore how to share the same logic for URL validation on both sides. You can assign this issue on me because I don't have permission to do it, and I will add a PR for adding new hooks for parsing single param/query. What do you think about having just one hook for params and one for queries that would handle all these parsing cases? const slug = useParam('slug') // returns: string|string[]
const taskId = useParam('id', 'number') // returns: number
const zipcode = useParam('zipcode', 'string') // returns: string
const slugs = useParam('slug', 'array') // returns: string[] |
@anteprimorac I considered that, but unless I'm missing something it's impossible to type the result of that properly. (result is always |
Oh nice! Then lets do that |
@flybayer what you think about adding the same functionality to |
What do you want and why?
As mentioned in the slack, current way of getting route params isn't really as nice as it could be
Better API (similar to prisma client)
Also if we have an object we can go overboard with TS definitions (keyof stuff) and have a lot nicer API for getting this stuff
I'd like to build this, but before I'd like to discuss maybe there's some other ideas on how to improve this
Insipiration
In a CRA project of mine
The text was updated successfully, but these errors were encountered: