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

feat(useUrlSyncedState): allow always emitting values #3996

Merged
merged 2 commits into from
Jan 22, 2025

Conversation

rphilippen
Copy link
Contributor

@rphilippen rphilippen commented Jan 16, 2025

Proposed Changes

⚠️ While this PR adds a feature, it also fixes bad behavior I introduced in #3992. Details are in the description below, but in short: I added use of useMemo, but it was completely useless, as I put the serializer in the dependencies. If you put the serializer in-line (like useTable did), it'll be invalidated on every render. The whole approach I took was flawed, so it has been rewritten.

Summary

I wrote another long PR description do explain all the details and decisions, so here's a summary:

Use Case for the features: Particularly useful for from and to (dateRange) states in the table. Ensures consistent URL parameters for sharing links, even if the initial state changes over time.

  • Added an "alwaysEmit" flag to the useUrlSyncedState hook (serialization), ensuring non-empty keys are always added to the URL, even if they match the initial state. It also adds these values during initialization.
  • initialState can now be a function, aligning with the useState hook.

Addressed issue(s): Previous PR had incorrect handling of the useUrlSyncedState hook, treating it like a rendering component and memoizing incorrectly. Just like useState only inspects the initial value on the first render, useUrlSyncedState should behave similarly.
Decision: Snapshot the serializer and deserializer on the first render to avoid frequent proxy function rebuilding. This approach aligns with useState behavior, but has limitations if serialization methods are placed inline (and use state values from the closure).

Feature: alwaysEmit

Let me start by explaining the features added to the useUrlSyncedState hook (and indirectly to useTable, as it uses it):

  • (Main) support an "alwaysEmit" flag, that will always emit non empty keys to the URL, even if the value matches the initial state.
    • As a side effect, these values are also added to the URL on initialization.
  • (Side) allow initialState to be a function yielding the state, to align with the useState hook.

I added the alwaysEmit flag solely for the from and to (== dateRange) state. So what is so special about that, that it needs this? Imagine typical use cases of a table that use the DatePicker and some time range to query in. The page using the table very likely defines …

  • presets relative to now, like "yesterday", "last week", "last month" et cetera.
  • the initial range to be one of those presets (or also a range relative to now).

Now imagine a user navigates to a page that has a table with syncWithUrl: true set. The url will not contain any parameters (related to the table), so the user is presented with the initial state, including the initial date range. For convenience, lets say that's "yesterday". Now the user sees something in the table they find odd, so they want to share the link with a colleague. They copy the link, and send it, but the receiver doesn't have time to look at it until the next day. When they open the page (for example the day after), they'll see a different time slice, since the range was not in the URL.

To state the obvious: the alwaysEmit feature ensures the state is always added to the URL (unless the value is empty/nullish), and that fixes this use case. I discussed the "API" (optional third boolean from the serializer) with @gdostie, and I think it's the best way to do it. While obscure, the use case is also very obscure. The feature is only applicable if the initial state is bound to change on a next visit to a page, which is not the case for most state values. I didn't want to add a property to the options or change the serialization API a lot just for this niche use case.

Fix: behavior

I quite messed up the hook in my previous PR (not intentionally of course), and I don't really have a good excuse for it 😅. For some reason I treated the hook code as if it was a rendering component, which is obviously wrong. I memoized things to not recalculate then on every call to the hook (= every render), but then put something in the dependencies that is likely to change every render.

To clarify a bit what is expected, and what might not be expected of the hooks, take these details in consideration:

  • useState only takes the initial value (initializer) as parameter.
    • Only on the first render is the value inspected (taken as state), or if you pass a function, it is only invoked on the first render of a component.
  • useUrlSyncedState is a proxy for useState (with an added feature), so it should behave similarly.
  • useUrlSyncedState takes an object as parameter, with two functions as parameters (serializer, deserializer).

The function arguments are unfortunate. Should the hook always call the latest version of the function passed, or the first? Note that updating the serializer affects the proxied setState, as it depends on the serializer. I went for snapshotting the serializer (and deserializer) of the "first render". To me that's logical, as useUrlSyncedState is a proxy for useState. It also saves a lot of rebuilding the same proxy function, in case somebody puts the serializer and deserializer functions inline. That causes these methods to be a new (lambda) function instance on every invocation of the hook. I put "first render" in quotes, because the proxy will be updated if you change the sync flag value, and in that case it will update the serializer reference.

Only keeping a snapshot of the serializer on the first render has consequences though. We can't prevent anybody from placing those methods inline, and using (state) values from the closure. However, that state will of course go stale quickly, as it will always see the values as they were on the first render. Note that the deserializer is only expected to be invoked on initialization anyway. Still, I decided to pass the initialState to it. This makes it easier to use a "const function", which I think is the best approach.

You will see I moved the serialization methods to not be inline in useTable. While not strictly necessary to fix the hook behavior, I did this simply to make it impossible to (accidentally) use state variables from the closure.

Tested

I tested these changes on my Admin-UI branch where I use the syncWithUrl option, and the hook also for the page's state. (The Data Health panels). On initial navigation, you can see the hook adds the date range to the URL. (And it also sets/keeps the value in there if it equals the initial value). For the rest the hook also works as expected, as also verified by the unit tests.

Potential Breaking Changes

There are no breaking changes, only additive features.

At the moment, only useTable uses the hook. In an in-review PR, Admin UI's Data Health panel also starts using the useUrlSyncedState hook. The from and to being serialized on init could be considered a behavior change, but since no UI uses the syncWithUrl option of the table (yet, to my knowledge), no current UI will be affected by that. The Data Health UI is actually relying on this flag, and it needs this feature.

Acceptance Criteria

  • The proposed changes are covered by unit tests
  • The potential breaking changes are clearly identified
  • README.md is adjusted to reflect the proposed changes (if relevant) Not Applicable.

@rphilippen rphilippen requested a review from a team as a code owner January 16, 2025 16:48
@rphilippen rphilippen requested review from gdostie and FelixBlaisThon and removed request for a team January 16, 2025 16:48
Copy link

  - also export useUrlSyncedState types
  - correct hook behavior and memoization
@rphilippen rphilippen force-pushed the feat/ua-9706-always-emit-table-from-to branch from c46a794 to d725feb Compare January 17, 2025 13:50
@gdostie gdostie merged commit a61cb13 into master Jan 22, 2025
6 checks passed
@gdostie gdostie deleted the feat/ua-9706-always-emit-table-from-to branch January 22, 2025 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants