-
-
Notifications
You must be signed in to change notification settings - Fork 654
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
Improve internet-connectivity state a bit; upgrade @react-native-community/netinfo. #4940
Conversation
e9a7c9f
to
370ef21
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @chrisbobbe ! These changes will be good. Comments below, mainly on the implementation of useHasStayedTrueForMs
.
| NetInfoWimaxState | ||
| NetInfoVpnState | ||
| NetInfoOtherState; | ||
declare export type NetInfoState = NetInfoDisconnectedStates | NetInfoConnectedStates; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat! This type is pretty detailed.
// | ||
// (Several of those parameters are configurable -- timeout durations, | ||
// URL, etc.) | ||
netInfoState.isInternetReachable, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
online state: Switch to `isInternetReachable`, from `connectionType`.
This is hopefully more accurate about what we actually want to know:
"can the app expect to reach the Internet?" Not just "is there some
network connection?".
Cool.
One thing happening here is that our old logic was equivalent to using the isConnected
property (except that with @react-native-community/netinfo
v6 that property started being null
sometimes instead of false
.) So using that would have been a bit cleaner for us to do in the first place, plus with the v6 upgrade it gives us the distinction between null
and false
.
Then switching from (effectively) isConnected
to isInternetReachable
makes it false or possibly null in some cases where it previously would have been true. That's the result of asking the more on-point question.
src/common/OfflineNotice.js
Outdated
useEffect(() => { | ||
if (value) { | ||
timeoutId.current = setTimeout(() => { | ||
setResult(true); | ||
}, duration); | ||
} else { | ||
clearTimeout(timeoutId.current); | ||
timeoutId.current = null; | ||
setResult(false); | ||
} | ||
}, [value, duration]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. Won't this clobber the old timeout? That is, if we have:
- Render with value true.
- Render again with value true, before that timeout completes.
then we'll forget about the old timeout and set a new one.
I guess the original timeout will still end up firing… but because timeoutId.current
won't know about it anymore, it'll end up firing even if the value then changes to false.
Oh, and I guess this is a latent bug as it stands, because it only triggers if the effect gets rerun. Which only happens if value
or duration
changes, and currently we never change duration
. Still, good to fix the latent bug in case we reuse this -- plus I suspect the code will be somewhat easier to reason through in a fixed version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Separately: we should clear this timeout if the component unmounts, so we don't have a leaked timeout trying to belatedly call setResult
.
useEffect
supports this -- have its callback return the cleanup function:
https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, and I guess this is a latent bug as it stands, because it only triggers if the effect gets rerun.
Ah, yeah, I think I was sort of counting on this fact for the implementation. I think I should use usePrevious(value)
in there somewhere for explicitness (and so it doesn't require callers to never change duration
); I'll try that.
370ef21
to
28856f2
Compare
Thanks for the review! Revision pushed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Partial thoughts below.
src/common/OfflineNotice.js
Outdated
useEffect(() => { | ||
const startTimer = () => { | ||
timeoutId.current = setTimeout(() => { | ||
setResult(true); | ||
}, duration); | ||
}; | ||
|
||
const resetTimer = () => { | ||
clearTimeout(timeoutId.current); | ||
timeoutId.current = null; | ||
setResult(false); | ||
}; | ||
|
||
if (prevValue !== true && value) { | ||
startTimer(); | ||
} else if (prevValue === true && !value) { | ||
resetTimer(); | ||
} | ||
|
||
return resetTimer; | ||
}, [prevValue, value, duration]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. What happens if duration
changes (while value
doesn't)?
I think the consequence of that will be:
- This effect gets invoked again.
- … But, before any of the logic inside the effect gets to run, the last effect's teardown callback is invoked. That's
resetTimer
. So the existing timeout gets cleared.
So I think this still ends up clobbering the old timeout.
This logic does seem tricky to pin down. I'll stare at this a bit and see if I can come up with a clean way to do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it's not too bad that we clear the old timeout if duration
does change -- certainly that's no longer quite the timeout we want anyway, because duration
is what determines the desired timing. Though this still isn't really ideal behavior in that case: for example if duration
keeps changing back and forth, we'll never get around to realizing that the value has been true for well over duration
.
But also, because we're depending on prevValue
as well as value
here, if we simply get a rerender (with no duration
change):
- First render, with
value
true (andprevValue
null.) - Second render, with
value
still true, but nowprevValue
also true.
then the effect will get rerun, and we'll cancel the timeout.
I guess that can't happen more than once in a row, but still not the desired behavior.
28856f2
to
f81449f
Compare
I've just pushed a revision that takes a bit of a different approach to the issue of Also added a couple of tweaks atop the changes to |
Thanks, those changes look good! Should I squash as I see appropriate, or would you prefer to do that @gnprice? |
…lers. The version we're on now (5.9.10) is much later than 3.2.1, which it looks like was what informed the types written down in AppEventHandlers. One nice-looking thing we get is `isInternetReachable`, which should actually tell us whether the Internet is reachable (not just whether there's an active network connection).
And update the libdef by transferring over the changes made in the two TypeScript files it's based on. This handles the only breaking change announced: > - useNetinfo: When the connection state is unknown, the > `isConnected` and `isInternetReachable` properties are now set > to `null` rather than `false`. This allow you to easily detect > the initial "unknown" state before the state is detected and set > to a boolean.
This is hopefully more accurate about what we actually want to know: "can the app expect to reach the Internet?" Not just "is there some network connection?". Getting this right is already important for a nice user experience. But it'll be more critical if we adapt our request retry-and-timeout logic to be aware of the Internet reachability state, which is reasonable to want to do. In 6.0.0, `isInternetReachable` started being `boolean | null`; see the comment about when it's `null`. Since `null` is a meaningful third state, adjust the Redux plumbing to allow `null` for `state.session.isOnline` so we can do something distinct in the UI.
As Greg pointed out a few weeks ago [1], `usePrevious` has had a "funny quirk where on the first call, it takes the "previous" value to be the current one." Instead, let the caller decide what the value should be on the first render, or just have it be `null` if the caller is fine with that. [1] zulip#4914 (comment)
Mostly to get the summary line back down to a line, pushing a detail out into the prose body. Also update the reference to the upstream source we based this on, now that we've improved it in a way that changes its interface.
The main motivation for this is that it makes the types behave more uniformly between the case where `initValue` is or isn't provided: this way, the value we actually use for it is always the value passed, rather than sometimes being that and sometimes of a different type. That in turn will let us tighten the types so that when we have a real initial value to provide, the return type becomes simply `T` rather than `T | null` or `T | void`.
This way, in cases where we have an initial value that's actually of the main value's type `T` (like false or zero), we can see that the return value is always of type `T`, rather than have the type-checker think that it might be null or undefined.
And log what we know from NetInfo to Sentry.
f81449f
to
88b0a71
Compare
Cool -- going ahead and merging.
Yeah, so if I'd written the whole branch myself, then I'd squash at least the first three of these: and maybe all four. If you'd written the whole branch, then I expect you'd do something like that too. They basically make one change (the first commit, which you wrote), and then revise it (the subsequent commits, which I added.) But there's nothing broken about the first commit, so it's not essential that the changes need to be squashed in. I regularly merge branches that have a sequence like this, when there's something that I tweak at the same time as I merge. In those cases the separate commits are helpful for showing what I changed and why -- basically the same reason that I left them unsquashed when pushing this revision the other day. This commit sequence seems clear enough, so in the interest of not spending more time than necessary in logistics, I'm happy just merging 🙂 (If one of the commits was broken in some way -- either in our tests, or from a user perspective -- then I'd definitely want to squash in any fixes. That's the one real hard rule of commit sequences for us, that we always aim to not have a step that's broken.) |
This is our first use of `react-test-renderer`. It piggy-backs on our incorporation of Jest's "modern" fake-timer implementation in PRs zulip#4754 and zulip#4931. That was handy! I haven't yet found any test cases that fail with our implementation. (And I'd been hoping to, to debug an unexpected error!) But I did try pasting in an earlier iteration of the hook's implementation, from zulip#4940, that Greg had found bugs in by reading the code. Many of these tests failed on that buggy implementation, which is a good sign. Might as well keep these new tests, then, if they're not an unreasonable maintenance burden.
This is our first use of `react-test-renderer`. It piggy-backs on our incorporation of Jest's "modern" fake-timer implementation in PRs zulip#4754 and zulip#4931. That was handy! I haven't yet found any test cases that fail with our implementation. (And I'd been hoping to, to debug an unexpected error!) But I did try pasting in an earlier iteration of the hook's implementation, from zulip#4940, that Greg had found bugs in by reading the code. Many of these tests failed on that buggy implementation, which is a good sign. Might as well keep these new tests, then, if they're not an unreasonable maintenance burden.
This is our first use of `react-test-renderer`. It piggy-backs on our incorporation of Jest's "modern" fake-timer implementation in PRs zulip#4754 and zulip#4931. That was handy! I haven't yet found any test cases that fail with our implementation. (And I'd been hoping to, to debug an unexpected error!) But I did try pasting in an earlier iteration of the hook's implementation, from zulip#4940, that Greg had found bugs in by reading the code. Many of these tests failed on that buggy implementation, which is a good sign. Might as well keep these new tests, then, if they're not an unreasonable maintenance burden.
This is our first use of `react-test-renderer`. It piggy-backs on our incorporation of Jest's "modern" fake-timer implementation in PRs zulip#4754 and zulip#4931. That was handy! I haven't yet found any test cases that fail with our implementation. (And I'd been hoping to, to debug an unexpected error!) But I did try pasting in an earlier iteration of the hook's implementation, from zulip#4940, that Greg had found bugs in by reading the code. Many of these tests failed on that buggy implementation, which is a good sign. Might as well keep these new tests, then, if they're not an unreasonable maintenance burden.
Now, we're specifically using a bit of state that says whether we can expect to reach the Internet. And we show a notice if it's taken unexpectedly long to determine the state (16 seconds).
Getting this state right is good in general, for user experience. But it'd also become more critical if we adapt our request retry-and-timeout logic to act on the Internet reachability state, which would be reasonable to do.