feat(useUrlSyncedState): allow always emitting values #3996
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Proposed Changes
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
andto
(dateRange) states in the table. Ensures consistent URL parameters for sharing links, even if the initial state changes over time.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 theuseState
hook.Addressed issue(s): Previous PR had incorrect handling of the
useUrlSyncedState
hook, treating it like a rendering component and memoizing incorrectly. Just likeuseState
only inspects the initial value on the first render,useUrlSyncedState
should behave similarly.Decision: Snapshot the
serializer
anddeserializer
on the first render to avoid frequent proxy function rebuilding. This approach aligns withuseState
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 touseTable
, as it uses it):initialState
to be a function yielding the state, to align with theuseState
hook.I added the
alwaysEmit
flag solely for thefrom
andto
(==dateRange
) state. So what is so special about that, that it needs this? Imagine typical use cases of a table that use theDatePicker
and some time range to query in. The page using the table very likely defines …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 thirdboolean
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.useUrlSyncedState
is a proxy foruseState
(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, asuseUrlSyncedState
is a proxy foruseState
. 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 thesync
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 theinitialState
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 theuseUrlSyncedState
hook. The from and to being serialized on init could be considered a behavior change, but since no UI uses thesyncWithUrl
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
README.md is adjusted to reflect the proposed changes (if relevant)Not Applicable.