S
SolidJS2d ago
jack

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

i have this function
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}`);
};
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?
22 Replies
TaQuanMinhLong
does setTimeout work?
jack
jackOP2d ago
it could, but i was hoping there was a more native way to handle somethign like this
TaQuanMinhLong
:Worry_Think:
jack
jackOP2d ago
well actually setTimeout wouldn't work that wouldn't really do much for me i don't think
TaQuanMinhLong
So the goal is to update without triggering the UI update? :Worry_Think:
jack
jackOP2d ago
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
REEEEE
REEEEE2d ago
Is joinGroup a promise?
jack
jackOP2d ago
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
REEEEE
REEEEE2d ago
It resolves very fast but you could just use the .then part of it to navigate after it's done just in case
mdynnl
mdynnl2d ago
this should not matter. transition batches along sync (signal) => async (resources)
jack
jackOP2d ago
Yea even in the .then it’s already updated in the ui Maybe the issue is that the navigation isn’t instant?
mdynnl
mdynnl2d ago
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
jack
jackOP2d ago
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
REEEEE
REEEEE2d ago
Maybe you can make use of onBeforeLeave?
mdynnl
mdynnl2d ago
same problem . 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
jack
jackOP2d ago
might be tough in playground, working on a repro rn though
mdynnl
mdynnl2d ago
not necessarily have to be playground, stackblitz, csb, etc
jack
jackOPthis hour
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>
);
}
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>
);
}
peerreynders
peerreynders23h ago
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}`)
);
});
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.
Chrome for Developers
Paint Holding - reducing the flash of white on same-origin navigati...
A quick overview of paint holding. A Chrome feature for reducing the flash of white on same-origin navigations
jack
jackOP18h ago
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

Did you find this page helpful?