- 
          
- 
                Notifications
    You must be signed in to change notification settings 
- Fork 676
msglist: Convert MessageList to a function component with Hooks #5524
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
Conversation
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, this is a really nice improvement!! Small comments below.
        
          
                src/reactUtils.js
              
                Outdated
          
        
      | const NODE_ENV = process.env.NODE_ENV; | ||
|  | ||
| /** A unique value for private use by useComputeOnce. */ | ||
| const useComputeOnce_sentinel = Object.freeze({}); | 
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.
You could use, e.g., Symbol('first render') (doc) if you want the value to contain a hint about what it means.
        
          
                src/reactUtils.js
              
                Outdated
          
        
      | if (ref.current === useComputeOnce_sentinel) { | ||
| ref.current = calculateValue(); | ||
| } | ||
| // $FlowIgnore[incompatible-cast]: value must have come from create() | 
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.
nit: I think you mean calculateValue(), not create()?
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, yeah -- that was the name in a previous draft.
        
          
                src/reactUtils.js
              
                Outdated
          
        
      | * the case where it does. | ||
| */ | ||
| export function useComputeOnce<T>(calculateValue: () => T): T { | ||
| const ref = React.useRef(useComputeOnce_sentinel); | 
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.
This implementation with useRef looks good.
An alternative that you might have considered is to pass a function to useState, for "Lazy initial state", then just never call the resulting setFoo function.
I don't prefer one way over the other; possibly useRef does the job more transparently.
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.
Yeah, I think either of those is a good solution. There might be a reason to prefer one over the other if one knew more about React internals, but I don't know of such a reason.
        
          
                src/webview/MessageList.js
              
                Outdated
          
        
      | webviewRef = React.createRef<React$ElementRef<typeof WebView>>(); | ||
| sendInboundEventsIsReady: boolean = false; | ||
| unsentInboundEvents: WebViewInboundEvent[] = []; | ||
| const webviewRef = React.useRef<React$ElementRef<typeof WebView>>(); | 
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, I think both the current React doc and the nice new beta doc for useRef aren't perfectly clear about what happens when you don't pass an initialValue argument to useRef.
If it means .current begins its life as undefined, there could theoretically be a problem when it runs into the !== null check in sendInboundEvents.
I would explicitly pass null, as we do in useUncontrolledInput, for example:
const ref = useRef<React$ElementRef<typeof TextInput> | null>(null);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.
Possibly React.createRef() (what we used before this commit) initialized .current to null. I say that because of the one word "back" in the doc:
React will assign the
currentproperty with the DOM element when the component mounts, and assign it back tonullwhen it unmounts
| I'll return to revise this sometime soon. In the meantime I just merged the two docs commits at the start of the branch, as: because I realized I wanted to point to that doc in a chat thread and wanted the updated version. | 
6e44878    to
    2effc11      
    Compare
  
    | OK, revision pushed! The main change in this revision is that now that I better understand how MessageList interacts with the theme (after all the discussion around #5527), and now that MessageList is future-proof for the theme changing -- it'll have the glitch where the user gets scrolled to the bottom, but will display a coherent color scheme rather than black-on-dark text -- I wanted to preserve that future-proofness. That meant that instead of the behavior of  | 
| (I also manually tested that if you cherry-pick the dropped final commit of #5527, open a message list while in auto-light mode, then go to system settings and switch to dark mode: (a) the version with  | 
| Hmm, novel failure in CI! That comes after spending several minutes apparently successfully doing much of the build. … And I reproduce locally, and I reproduce locally at main. I'll investigate a bit further and probably start a chat thread. In the meantime, seems independent of this PR. [edit: chat thread; fix at #5535.] | 
This fixes a build failure which started happening today due to an operational mistake in React Native release management: facebook/react-native#35204 Happily Gradle gives us a way to more precisely pin down what we want it to do when finding dependencies, which makes the build robust to this and any similar issue. I've sent the same fix upstream for the template app: facebook/react-native#35208 See also where we spotted the issue in CI: zulip#5524 (comment) and discussion in chat: https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/build.20failure.3A.20libfbjni.2Eso.20duplicated/near/1459787
2effc11    to
    32b5de4      
    Compare
  
    | (I rebased this atop main after #5535 went in; CI is passing again.) | 
In docs/architecture/react.md we say we derive all our class components from React.PureComponent, with one exception, namely MessageList. From a grep, there are a handful of other components where we use simply React.Component instead. I'm not sure the difference has much practical effect on those components, but adjust them to follow our pattern for consistency. None of these components need special behavior in this respect: they don't implement shouldComponentUpdate, and just like for other components in our app the values we pass for their props are never mutated, only replaced with new values.
The API this is referring to is actually the modern context API, just the class-component form of it. The "legacy context API" is something else, involving a static property `contextTypes`. We haven't used that one for anything since commit 30e4d19, PR zulip#4199, back in 2020-07. Also mention what's now the most common alternative to withGetText: the useContext hook.
We got this `Props` type properly type-checked with ac97c91 and the series around that.
No need to pass it around separately where we're already passing the whole props object.
Just to simplify a bit the top level of this class declaration. Also delete the redundant type annotation, and narrow the lint-ignore to the particular rule it's meant for.
By having it take a single props object, rather than some props-like values as positional arguments.
From some experimentation, it turns out that re-rendering a WebView element is perfectly fine, just as one would hope if WebView is to be a well-behaved React component type. What isn't fine is re-rendering it with a *different `html` value*. Doing that will cause the page to be reloaded, so that any UI state inside it gets reset -- most notably, the user gets scrolled right back to their starting point (i.e. the first unread, or else the very end). Which is pretty reasonable on the WebView's part. So that means that the way our current logic succeeds at avoiding that problem isn't that it avoids re-rendering the WebView (though it does do that); it's that it avoids re-rendering the MessageList, and thereby avoids ever computing a new `html` in the first place. As a result, it's perfectly fine to have another component interposed here. Even if this new component gets re-rendered for some arbitrary reason, it'll use the `html` value it got from its parent, namely MessageList -- which we continue to take care not to re-render, so the `html` value still never changes.
The code already behaved correctly, because when we look at this property we only ever test it for truthiness, so void is just as good as false. But it wasn't really well-typed; apparently Flow looked the other way.
Most of this is a routine class-to-Hooks conversion. The interesting part is the lifecycle method `shouldComponentUpdate`; the part where it tracks changes to props turns into an effect, and the part where it blocks (or tries to) re-renders turns into forcibly memoizing the bulk of the old render method using a ref. As a bonus, this fixes a longstanding latent bug: React could have at any time chosen to go ahead and re-render the component even though we have a `shouldComponentUpdate` that always returns false. (The React docs stress that there's no guarantee on that score, that the method exists only for performance optimization.) That would cause us to recompute `html`, potentially taking some time. And if the resulting value had changed since the initial render, it'd cause the state in the WebView, including scroll position, to reset. With React Hooks, we can use refs to ensure that we never compute `html` a second time, and never change its value, just as we intend. (With a bit of future-proofing to ensure we continue displaying something coherent if the theme changes, which zulip#5533 will enable.)
This just represents inlining the definition of `usePrevious`, and then simplifying. We'll make use of this ref in the next commit.
32b5de4    to
    a44dd53      
    Compare
  
    | Thanks, LGTM! Merged. Very glad to have this done. | 
A followup to #5523, as foreshadowed at #5523 (comment) . This is the next step toward the message-list changes I'll want to make for #5364.
The tricky part here is the way we have MessageList seize the baton from React when it comes to updating the UI to reflect changes in data, and go take care of those updates by other means.
With a class component for MessageList, we've accomplished this by implementing
shouldComponentUpdateto prevent re-renders. This has always been a bit of a latent bug, because that method's docs disclaim any guarantee of always preventing a re-render. In this branch, we:shouldComponentUpdateto reflect changes that have already happened;This PR concentrates on the
MessageListInnercomponent-type that does most of the work; it doesn't touch the several layers of wrappers which build up around it the actualMessageListcomponent-type. I plan to tackle those for the next PR in the series.