Conversation
plans/DH-21654-routing.md
Outdated
| | `use_search_params` | Hook | Returns all search params as a dictionary | | ||
| | `use_search_param` | Hook | Returns a single search param by key | |
There was a problem hiding this comment.
this seems redundant. just returns all params seems fine to me, extra wrapper seems pointless, harder to grow from one param to multiple.
There was a problem hiding this comment.
I'm not dead set on it, but there are some reasons I added the second hook.
First of all, it helps avoid a problem with one vs multiple values for the same key. There's no great solution here, but I really want to avoid situations such as:
- Always having to grab the 0th element of a list (if only a list is the value, as is now)
- Having to always check if you return a list or a string (if we make return value dependent on how many values there are, which I considered while iterating)
It's worth noting that URLSearchParams has convenient getters (get and getAll) so I'm trying to emulate that in a natural way without setting up a whole class, which seems like overkill.
It was also specifically asked for in this form in the original request.
Now, this has gotten me thinking that I could just add the arguments into the use_search_params hook. I think I like that more, and does at least force the explicitness of the two. It might be jamming too much into one hook though.
There was a problem hiding this comment.
I think also important to note that Python language semantics are a bit different than JS. Notably, getting from an array with an index out of bounds (e.g. [][0], fetching the first element of an empty array) simply returns undefined, whereas Python raises an IndexError.
Also, are we going to have a tuple returned with a setter from the use_search_params? Like Router: https://reactrouter.com/api/hooks/useSearchParams
So say you care about the parameter q, which you're going to use as the search query string. That's pretty common, and you don't really care about other parameters or multiple parameters of q. To write that just with use_search_params you would have to write something like:
params, set_params = use_search_params()
q = params.get("q", [""])[0]
# vs
q = use_search_param("q", "")That's not tooooo bad. But what if you care about a param named foo, you just want the first value, but you want it to be None if it's unset (vs. including it in the query string)?
params, set_params = use_search_params()
q = params.get("q")[0] if params.has("q") else None
# vs
q = use_search_param("q")Note in JS, the seconds case I described there where you'd want undefined, it'd be:
[params, setParams] = useSearchParams();
q = params.get("q")So yes, the hook is a bit redundant. But also, I could see the value of a use_search_param hook.
(Side note, kind of funny they don't actually show getting the params from the URLSearchParams on https://reactrouter.com/api/hooks/useSearchParams).
There was a problem hiding this comment.
Added a setter to the hooks, so I think keeping the hooks separate is best to prevent overcomplicating the hook.
plans/DH-21654-routing.md
Outdated
| ```python | ||
| def navigate( | ||
| pathname: str | None = None, | ||
| search_params: str | SearchParams | None = None, | ||
| fragment: str | None = None, | ||
| full: bool = False, | ||
| ) -> None: |
There was a problem hiding this comment.
This doesn't feel like it fits in. Like it feels like a component but is very different to a component.
Maybe the rest of the ui.router spec needs to be contemplated to see if there's a better spot for this behaviour, as in next.js this lives under the router.
There was a problem hiding this comment.
I don't know if we need to go full router yet.
We could go with useNavigate hook which is less awkward:
https://reactrouter.com/api/hooks/useNavigate
This as is is kinda like redirect (https://reactrouter.com/api/utils/redirect) but with (I think) a better name.
There was a problem hiding this comment.
To me this would be handled similar to triggering ui.toast... I don't particularly care if it's useNavigate or redirect, up to y'all.
I will note that with the useNavigate hook, you can just call navigate("/path?foo=bar"), which isn't clear from here; you could say that path can also include the query params (since it's the first arg), but then what if you do navigate(pathname="/path?foo=bar", search_params="foo=biz")? Throw? Override? Merge? Some clarity there.
There was a problem hiding this comment.
Made the return value a tuple so switched to a hook.
Added clarification for conflicting path and search_params, etc. I think override is simplest and clearest. Merge is weird because of repeat hook possibilities and throw seems unnecessary vs the clear note.
plans/DH-21654-routing.md
Outdated
| pathname: str # The path to navigate to (`"/path"`) | ||
| search_params: str | SearchParams # Query string (`"?foo=bar"`) or SearchParams dict | ||
| fragment: str # URL fragment, e.g. `"section"` (leading `#` optional) | ||
| full: bool # If True, navigate to full path instead of relative path to this widget |
There was a problem hiding this comment.
Full seems like something humans and AI are going mess up setting correctly.
There was a problem hiding this comment.
Yeah, I don't love the name. I'll try to think of something better. It's hard to get a good name that isn't overloaded already.
There was a problem hiding this comment.
Link has relative: https://reactrouter.com/api/components/Link#relative
absolute would be fine too I think
There was a problem hiding this comment.
Renamed to absolute
There are a few type issues failing, found in #1319
plans/DH-21654-routing.md
Outdated
|
|
||
| ```python | ||
| @overload | ||
| def use_search_param(key: str, default: str = "") -> str: |
There was a problem hiding this comment.
The default should be None, e.g. it returns None if no query parameter is set. If it's set (e.g. /myPath?foo), that should return the empty string ''.
There was a problem hiding this comment.
Switched to None default. Good catch with the empty set value.
plans/DH-21654-routing.md
Outdated
| ### API | ||
|
|
||
| ```python | ||
| def use_search_params() -> dict[str, list[str]]: |
There was a problem hiding this comment.
We're not planning on returning a setter like https://reactrouter.com/api/hooks/useSearchParams does?
There was a problem hiding this comment.
This is an interesting point. It does it a little more complex in terms of the implementation plan as this is almost a navigate, with things held the same except for search params. But I can definitely see the value. I think I want to consider this for all hooks. It would even then make sense to move navigate into a hook if we did the inverse, add a tuple with all the current data to a tuple.
There was a problem hiding this comment.
In general I've switched to tuples with value, setters. The use_url_components is the exception. I think it would be weird to have a setter there because we'd have to ignore some of the provided values (scheme and netloc) so it seems like it should be more informational for that one.
plans/DH-21654-routing.md
Outdated
| | `use_search_params` | Hook | Returns all search params as a dictionary | | ||
| | `use_search_param` | Hook | Returns a single search param by key | |
There was a problem hiding this comment.
I think also important to note that Python language semantics are a bit different than JS. Notably, getting from an array with an index out of bounds (e.g. [][0], fetching the first element of an empty array) simply returns undefined, whereas Python raises an IndexError.
Also, are we going to have a tuple returned with a setter from the use_search_params? Like Router: https://reactrouter.com/api/hooks/useSearchParams
So say you care about the parameter q, which you're going to use as the search query string. That's pretty common, and you don't really care about other parameters or multiple parameters of q. To write that just with use_search_params you would have to write something like:
params, set_params = use_search_params()
q = params.get("q", [""])[0]
# vs
q = use_search_param("q", "")That's not tooooo bad. But what if you care about a param named foo, you just want the first value, but you want it to be None if it's unset (vs. including it in the query string)?
params, set_params = use_search_params()
q = params.get("q")[0] if params.has("q") else None
# vs
q = use_search_param("q")Note in JS, the seconds case I described there where you'd want undefined, it'd be:
[params, setParams] = useSearchParams();
q = params.get("q")So yes, the hook is a bit redundant. But also, I could see the value of a use_search_param hook.
(Side note, kind of funny they don't actually show getting the params from the URLSearchParams on https://reactrouter.com/api/hooks/useSearchParams).
plans/DH-21654-routing.md
Outdated
|
|
||
| | Feature | Type | Description | | ||
| | ------------------- | --------------------- | ----------------------------------------- | | ||
| | `use_pathname` | Hook | Returns current pathname | |
There was a problem hiding this comment.
In ReactRouter they don't have a usePath*, they just have useResolvedPath and useLocation. Location doesn't really exist in Python...
In general we probably should use urlsplit in Python (urlparse says to use urlsplit)...
In those tuples that are returned, they just call the path path, not pathname.
All that to say, I think this should just be use_path.
And maybe we need something similar to useLocation that just returns the SplitResult... use_split_url ? use_url_components?
There was a problem hiding this comment.
All the different structures react router allows for really makes things complicated, there is no one source of truth... for reference, I got pathname from the Path object here, and that specific parameter is what I am going for here. I don't ming the rename given the python context though. I'll add a split result hook too.
There was a problem hiding this comment.
Renamed and added the new use_url_components hook.
plans/DH-21654-routing.md
Outdated
| ```python | ||
| def navigate( | ||
| pathname: str | None = None, | ||
| search_params: str | SearchParams | None = None, | ||
| fragment: str | None = None, | ||
| full: bool = False, | ||
| ) -> None: |
There was a problem hiding this comment.
To me this would be handled similar to triggering ui.toast... I don't particularly care if it's useNavigate or redirect, up to y'all.
I will note that with the useNavigate hook, you can just call navigate("/path?foo=bar"), which isn't clear from here; you could say that path can also include the query params (since it's the first arg), but then what if you do navigate(pathname="/path?foo=bar", search_params="foo=biz")? Throw? Override? Merge? Some clarity there.
plans/DH-21654-routing.md
Outdated
| pathname: str # The path to navigate to (`"/path"`) | ||
| search_params: str | SearchParams # Query string (`"?foo=bar"`) or SearchParams dict | ||
| fragment: str # URL fragment, e.g. `"section"` (leading `#` optional) | ||
| full: bool # If True, navigate to full path instead of relative path to this widget |
There was a problem hiding this comment.
Link has relative: https://reactrouter.com/api/components/Link#relative
absolute would be fine too I think
mofojed
left a comment
There was a problem hiding this comment.
I think the biggest hangup for me is whether the hooks should return setters or not. It seems different frameworks do different things? Can you take a look at a few different frameworks and briefly summarize how each does it? React-router, next.js router, tanstack router I think were the paradigms we were looking at.
I had just looked at React-router, but honestly I don't reallllly like having a setter in each one, and then you could have people call like set_path, then set_query_params instead of encouraging them to just do one call like set_url or navigate or whatever. And then there's a bunch of read-only hooks and only one navigation/way to change the URL.
Unsure if @dsmmcken has a pref there. Or what's your pref @jnumainville
|
I personally like the getter/setter situation because of locality, allowing you to focus on what you want.
So, in summary, only react-router does the getter and setter and only for search params. |
|
My preference is closer to next.js. I do not care for the setters at all, that part belongs under ui.router to me, there's a difference between pushing and changing the url. This why I only care about this spec in full context, and it needs to be constructed as such. |
|
@jnumainville We need to flesh out a couple of real-world examples to build confidence we have the "right" API. In particular, in this plan, we don't have any "real" examples of what we're trying to achieve. We should imagine a few scenarios about how we may want to use routing (could probably get Claude to just come up with a few). In the ticket, the customer is just asking for read. Please clarify with Paul if we can get any more details from the customer, specifically if they'll want to change the URL as well from some controls. |
|
TanStack Router has push vs replace under NavigateOptions. |
|
I had Claude generate a bunch of short representative snippets then determine which hook was better for each case. A nice way to prototype, and it’s conclusions seem reasonable per example. The summary it generated: I can flesh out bigger examples (or show the examples it generated here, but there is 25) if needed, but that does seem to cover a lot of ground, and the findings were pretty definite. Tuple with setter for Now to just solidify my thoughts on the routing concerns, I do generally like TanStack router’s approach. We can provide a push/replace param to the |
|
Here is what I want to be able to do: Think about building interconnecting dashboards, and how to make that work nicely across the same and different PQs. I have a pq with 2 or 3 dashboards exported for it. I want to be able to define routes that point to dashboard. I want to have pretty-urls, where there's segments that can be used from from it, next I believe uses I want urls to be easily constructable, probably a new route for /pq// Then user can do something like: and have code that does something like @ui.component dash that uses those nice paths. ui.table( |
Add basic SPA routing related hooks
The plan contains specs for hooks as well as some more advanced routing behavior. Only hooks will be implemented in this PR. The other behaviors will be implemented in DH-21898.
A router (like the one proposed in #362) is beyond the scope of either of these tickets, but could be built on top of them in the future. This also means we don't have a way to pull out path params automatically, but the data is there.