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

Retrieve query params from URL to use in Mesop #340

Closed
richard-to opened this issue Jun 1, 2024 · 9 comments · Fixed by #676
Closed

Retrieve query params from URL to use in Mesop #340

richard-to opened this issue Jun 1, 2024 · 9 comments · Fixed by #676
Labels
feature request New feature / API

Comments

@richard-to
Copy link
Collaborator

Right now it's not possible to get the query params from the URL.

Even request.args from flask would not work in this case since Mesop gets called via the /ui endpoint so the initial query params wouldn't be available in the flask request.

@wwwillchen
Copy link
Collaborator

Yep, I agree we should support some kind of get/set query params.

@wwwillchen
Copy link
Collaborator

Some ideas on potential APIs

Option 1: Declarative API (preferred)

We can use the fact that we have State class which are already serialized back-and-forth between the client and server and use a part of the State class as a query parameter.

@me.stateclass(query_params=["prompt"])
class State:
  prompt: str
  some_thing: Message

This would automatically set the value of prompt to the value of the query parameter "prompt" on page load.

If you mutated the value of prompt, then it would automatically update the query parameter.

Option 2: Imperative API

You could read query parameter as part of an on_load hook see #318

def on_load(e: LoadEvent):
    me.state(State).prompt = e.query_params["p"]

You could also set the query parameter in an event handler

def click(e):
   me.state(State).prompt = e.key
   me.set_query_param("p", me.key)

Thoughts

I think Option 1 is nicer because it avoids the need to manually synchronize between state attributes and query parameter, but Option 2 is more flexible (although I can't think of a case where this is actually beneficial).

@richard-to WDYT?

@richard-to
Copy link
Collaborator Author

1

There's two things about option 1 that give some slight pause:

  1. In the future I think there could be a case where we have an option where we don't pass state back and forth at all. I think this would be an config option though for people with very large state. I think in most cases, we'd end up with passing the state that has changed. So I think we'd just need a way to handle the case where we don't pass state back and forth at all. For that seems solvable by passing just the query param state then.

  2. I'm curious why want to allow users to update the query param like this? I feel like it should be readonly. And that if we wanted to update the query param it should be through me.navigate (which we will need to update to handle query params). What are the use cases for updating the query params?

2

I like this option a bit better since it more clearly delineates the query params from state. So I don't mind manually setting state here. It also gives us another use case for on load. And also keeps a consistent pattern for setting the initial state dynamically based on the user.

I think for updating query params in the event handler, maybe change that to using me.navigate. Although I'm curious your reasoning for allowing updating of query params with forcing navigation.

It's possible that the query param is not stored in the state but used to load specific data for a state variable.

if e.query_params["data_source"] == "X":
  state.data = load_data_X()
else:
  state.data = load_data_default()

@wwwillchen
Copy link
Collaborator

I think basically my view is that query params are a type of state. If you go back to the reactive UI paradigm of: UI is a function of state (i.e. f(state) = ui), then I think query param is just one kind of state.

For example, if you pay close attention to many of Search's features, they serialize their state into the URL. For example if you go to this movie search query it not only encodes which movie is selected but also the other movies in the carousel, so you get that same animation/transition in when you load the page.

I think this is a fairly complex example which we probably don't need to support in Mesop any time soon, but I think eventually it'll be convenient to serialize/deserialize complex types (beyond strings) in query params. If you think about it, we could only support string in stateclass and make app developers serialize and deserialize by hand, but this would be a pain.

The other aspect of this is that keeping it automatically sync'ed means that it's much easier to create apps where you can share a URL and the user will automatically get the same state as you were on. For example, if you have a prompt filled into a textarea, it becomes very convenient to have this populate in the URL and you can share this with a teammate. Doing this by hand is tedious and app developers will probably forget to do this.

For the load_data example, I think you can treat these as two separate state properties. One of them (probably shorter, e.g. file path) is the query param, and you use that state property to load a much larger value (e.g. actual file contents) into a second state property, which isn't a query param.

I think Option #2 is the conventional approach and it's basically Streamlit's API, but I feel like Option #1 is actually more consistent with Mesop's overall approach to state management, which is to provide strong type safety, incl. for complex types.

Let me know what you think or we can chat more in-person - it's quite an involved topic :)

@richard-to
Copy link
Collaborator Author

Thanks for the explanation of the use case where someone may want to update the query params. That makes sense. I actually didn't know Google Search did that.

So I think you've convinced me that it makes sense to be able to update the query params without refreshing.

So two usages looks like:

  1. The simple use case of 1/1 query param to state
  2. Encoding the state into the query params
  3. Perhaps some combination

I guess my main question is do we want to support encoding the state into query params as a core functionality? Or do you feel that is more implementation related?

Additionally, do you have any concerns about max length for query strings? I took a cursory look but I'm not sure if the info I was seeing out of date or not. Seems like most browsers can handle somewhat large amounts of data. But there's still a limit.


So given that, an option 3 could be to create a subclass of state for query param state. I think this makes sense since we can already have multiple state classes in mesop. So if someone wants to use query param state, then they can create a separate state class.

This makes it clearer what is query param state and what is not. This way we don't need to annotate the state class. Also it avoids the issue of multiple state classes in the same app using query params that may overlap. I guess someone could still use multiple @me.queryparamstate classes. So maybe doesn't solve it completely.

@me.queryparamstate
class QueryParamState:
  param1: str
  param2: str

def on_click(e: me.ClickEvent):
  state = me.state(QueryParamState):
  state.param1 = 2

@wwwillchen
Copy link
Collaborator

I guess my main question is do we want to support encoding the state into query params as a core functionality? Or do you feel that is more implementation related?

I think encoding state into query params should be exposed as an API. After looking at your suggestion with @me.queryparamstate, I think we could something like this:

@me.stateclass(mode="query_param")
class State:
   ...

By default, if you just use me.stateclass (no parens) then it's the same as mode="default" (which is serializing the state back-and-forth between the server and client).

In the future, we can have different modes like "in-memory" (i.e. if it's a singleton server, then you could just rely on the server to hold it in-memory) or something like "fs", "redis" if you want to store it in an external storage system (it probably wouldn't be a string, because you'd need more configuration, but the idea is that it's a unified stateclass API and how you want to store it is something you can easily switch between as a Mesop app developer.

Additionally, do you have any concerns about max length for query strings? I took a cursory look but I'm not sure if the info I was seeing out of date or not. Seems like most browsers can handle somewhat large amounts of data. But there's still a limit.

I think really big query strings can be problematic, but I think we can point people to keeping your stateclass relatively small if you're storing it as a query param. I think the suggestion of having separate stateclass for query param is good because it'll encourage people to be deliberate about what they store in query param.

I guess someone could still use multiple @me.queryparamstate classes. So maybe doesn't solve it completely.

Yeah, it's possible there will be conflicts, we could use a hash of the module+class name and use that to prefix each attribute. I think to keep it simple though (and have prettier query param names) we can just use the attribute name and print a warning if there's a duplicate query param name. In general, I'd guess for most apps (since people are building fairly small single-page apps for mesop), they will use one state class at most for query param.

@richard-to
Copy link
Collaborator Author

I like that idea of adding a mode parameter to the stateclass decorator. That's a good point about using with other modes. That's an interesting idea.

@richard-to
Copy link
Collaborator Author

One other nice mode could be session storage using the built in Flask session object. This would allow temporary persistence of state even after reloads. This could be nice for certain use cases where you want certain settings to persist that may not be suitable in the query params.

@wwwillchen
Copy link
Collaborator

Recap:
me.query_params (as a dict)
drop & warn if you pass query params in the first arg of me.navigate
{"new_param": 1, **me.query_params}

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

Successfully merging a pull request may close this issue.

2 participants