#invoke navigate() after function call but before signal update propagates to ui

44 messages · Page 1 of 1 (latest)

sullen badge
#

i have this function ```tsx
const handleJoinGroup = () => {
const g = group();
const user = session.user();
if (!g || !user || !user.username || isMember()) return;

void joinGroup(z, group()?.id ?? '', user.id, user.username);
navigate(`/group/${g.id}`);

};


in this case, the joinGroup function will result in a signal update, which then results in a ui update

i want the user to navigates away before the ui update, otherwise they see a flash of a view for their new state before the nav happens. this is *fine* but not great ux. i've tried wrapping this in a startTransition, but i don't see much happening

one option is to navigate before the joinGroup call. this is fine because joingroup is very fast, but it feels wrong to send the user off before the operation to actually grant them access there is begun. does anyone have thoughts?
soft jetty
#

does setTimeout work?

sullen badge
#

it could, but i was hoping there was a more native way to handle somethign like this

soft jetty
sullen badge
#

well actually setTimeout wouldn't work

#

that wouldn't really do much for me i don't think

soft jetty
#

So the goal is to update without triggering the UI update?

sullen badge
#

the goal is to invoke joinGroup and navigate before the next render, so basically, yea

#

by the time joinGroup finishes, the ui will have gotten the signal updates

#

and as such, navigate comes in too late

#

i can't put navigate before joinGroup with any sort of confidence, because then i may send the user to their next page before the app state is totally updated

cedar mango
#

Is joinGroup a promise?

sullen badge
#

Yea

#

But it resolves very fast

#

There is the option to just track with a signal if the user clicked the submit button, and if so, force the page to keep the original view. But I feel like there’s a got to be a better solution

cedar mango
#

It resolves very fast but you could just use the .then part of it to navigate after it's done just in case

elder pendant
sullen badge
#

Yea even in the .then it’s already updated in the ui

#

Maybe the issue is that the navigation isn’t instant?

elder pendant
#

is there some await-ing before navigate gets called?

#

synchronous updates must still happen in the same task

#

at least until async context lands in browser

sullen badge
#

no

#

joinGroup returns a void promise, which i don't await, and then navigate is called next, nothing in between

#

the signal that updates as a result of joinGroup is a memo

cedar mango
#

Maybe you can make use of onBeforeLeave?

cedar mango
#

Oops sorry it's useBeforeLeave*

elder pendant
#

which means, navigate gets called first

#

the fact that it's an async function suggests that there's some updates across microtasks

#

better if you could reproduce it in the playground

sullen badge
#

might be tough in playground, working on a repro rn though

elder pendant
#

not necessarily have to be playground, stackblitz, csb, etc

sullen badge
#

yea there's something weird on my end going on. the minimal-ish repro i could make it only happens if i wrap the navigate in a setTimeout

#

in that the nav happens before the next frame, so i'm not sure i'll have to look into my code a little more

#

here's my whole component, maybe i'm messing up the conditional rendering in some way?

export default function JoinGroupPage() {
  const session = useUser();
  const z = useZero();
  const navigate = useNavigate();
  const params = useParams<Params>();

  const group = useQuery(() =>
    getTopLevelGroupDetailsByInviteLink(z, params.invitationId),
  );

  const guardedGroupWithMembers = useQuery(() =>
    getGroupMembersWhereUserIsAMember(
      z,
      group()?.id ?? '',
      session.user()?.id ?? '',
    ),
  );

  const isMember = () => guardedGroupWithMembers() !== undefined;

  const handleJoinGroup = () => {
    const g = group();
    const user = session.user();
    if (!g || !user || !user.username || isMember()) return;

    // TODO: fix new state flash
    void joinGroup(z, group()?.id ?? '', user.id, user.username);
    navigate(`/group/${g.id}`);
  };

  return (
    <Show when={session.user() && group()}>
      {(group) => (
        ...
          <StyledCard class="max-w-md w-full">
            <StyledCardHeader>
              <StyledCardTitle>
                <Show when={!isMember()} fallback={"Can't join "}>
                  Join{' '}
                </Show>
                '{group().title}'
              </StyledCardTitle>
              <StyledCardDescription>
                <Show
                  when={!isMember()}
                  fallback={"you're already a member of"}
                >
                  you&apos;ve been invited to join
                </Show>{' '}
                '{group().title}'
              </StyledCardDescription>
            </StyledCardHeader>
            <StyledCardContent class="flex justify-center">...</StyledCardContent>
          </StyledCard>
      )}
    </Show>
  );
}
thorny prawn
#

Just some observations:

What I'm not seeing is

  1. a Suspense boundary
  2. whether or not joinGroup does anything that would trigger a Suspense boundary

With a Suspense boundary in place a transition should start() paint- holding, performing the next render in the background.

With something like

start(() => {
  return joinGroup(z, group()?.id ?? '', user.id, user.username).then(() =>
    navigate(`/group/${g.id}`)
  );
});

I would expect the navigation to start before the nearest suspense boundary has a chance to swap in the updated render.

sullen badge
#

There's no suspense boundary, but none of the signals i'm accessing are async (ie. createResource/createAsync)

#

or at least as far as I can tell

sullen badge
#

update: i think batch is what i was looking for