Page MenuHomePhabricator

[web] use context to display `Menu` action items
ClosedPublic

Authored by jacek on Mar 9 2022, 4:30 AM.
Tags
None
Referenced Files
Unknown Object (File)
Sat, Sep 21, 12:07 PM
Unknown Object (File)
Thu, Sep 19, 3:16 AM
Unknown Object (File)
Thu, Sep 19, 3:16 AM
Unknown Object (File)
Thu, Sep 19, 3:16 AM
Unknown Object (File)
Thu, Sep 19, 3:16 AM
Unknown Object (File)
Thu, Sep 19, 3:16 AM
Unknown Object (File)
Thu, Sep 19, 3:16 AM
Unknown Object (File)
Thu, Sep 19, 3:16 AM

Details

Summary

Introduce rendering menu items using function provided by menu context. Thread actions menu should now have correct position.
As we need to set correct position of the content in portal, I used getBoundingClientRect react function, which calculates position of rendered element.
The position needs to be updated window resizes.

Test Plan

In web app, click on thread actions menu. It should appear in correct place and in browser's inspector it will be in the portal.

Diff Detail

Repository
rCOMM Comm
Lint
No Lint Coverage
Unit
No Test Coverage

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes
web/components/menu.react.js
18 ↗(On Diff #10193)

In D3374 it was only moved from another file. I think it's the best to fix it here in this diff.

web/components/menu.react.js
18 ↗(On Diff #10193)

I missed, that I updated the props in D3376 - I'll fix it there,

rebase & add "readOnly" in props

Only had time for a short skim. Have one question. Looks good!

web/components/menu.react.js
32 ↗(On Diff #10377)

question: Won't this function ever only be called once? Since updatePosition's deps are []. Why not just make this dep list empty too?

This revision is now accepted and ready to land.Mar 15 2022, 6:04 AM
tomek requested changes to this revision.Mar 15 2022, 10:43 AM

Only had time for a short skim.

@benschac In that case you should make someone a blocking reviewer, so that this diff appears in someone's queue for a deeper review

web/components/menu.react.js
32 ↗(On Diff #10377)

Making so would require silencing eslint error. It's more correct to have this dependency.

42–44 ↗(On Diff #10377)

You can use a shorthand

47–54 ↗(On Diff #10377)

Is it intentional that this effect is called on every render?

79–84 ↗(On Diff #10377)

We should memoize this to avoid style to change on every render

85 ↗(On Diff #10377)

Is it correct to use this function in render? Does it have any side effects?

This revision now requires changes to proceed.Mar 15 2022, 10:43 AM

Some changes after review & rebase. More changes planned.

Rebase & use introduced function setMenuPosition from menu context to make window resizing work correctly.

tomek requested changes to this revision.Mar 21 2022, 3:02 AM

We're no longer using portals so please update the title and the summary.

It looks like the logic can be simplified significantly. Currently, we keep the state in two places: in the state isOpen - if the menu is opened, and in context useRenderMenu which menu is the last one. The need for the state arose from the fact that when closing the menu we need to check if we're not closing a menu rendered by some other component. But this can be avoided by checking by exposing a new method from context that takes a node and closes the menu if it is equal to the current menu.

So to simplify the state, we need to have closeMenu function in the context that takes a node and sets the state to null if the node is equal to the current menu. This change will allow us to have all the state in a single place and to reduce the duplication of logic in renderMenu calls.

web/components/menu.react.js
106 ↗(On Diff #10521)

We don't need to use fragment when there's only one component rendered

This revision now requires changes to proceed.Mar 21 2022, 3:02 AM
In D3377#94267, @palys-swm wrote:

We're no longer using portals so please update the title and the summary.

It looks like the logic can be simplified significantly. Currently, we keep the state in two places: in the state isOpen - if the menu is opened, and in context useRenderMenu which menu is the last one. The need for the state arose from the fact that when closing the menu, we need to check if we're not closing a menu rendered by some other component. But this can be avoided by checking by exposing a new method from context that takes a node and closes the menu if it is equal to the current menu.

So to simplify the state, we need to have closeMenu function in the context that takes a node and sets the state to null if the node is equal to the current menu. This change will allow us to have all the state in a single place and to reduce the duplication of logic in renderMenu calls.

I'm not sure if I understand you correctly, but do you suggest removing isOpen completely from the state of the component? The component receives currently onChange callback which is executed with new state as a parameter, so removing isOpen from component state is impossible, isn't it?
I agree with the idea of introducing closeMenu provided by context - it will simplify the logic in the component here, and make the code more readable.

jacek retitled this revision from [web] display `Menu` action items in portal to [web] use context to display `Menu` action items.Mar 21 2022, 11:33 AM
jacek edited the summary of this revision. (Show Details)
In D3377#94567, @def-au1t wrote:
In D3377#94267, @palys-swm wrote:

We're no longer using portals so please update the title and the summary.

It looks like the logic can be simplified significantly. Currently, we keep the state in two places: in the state isOpen - if the menu is opened, and in context useRenderMenu which menu is the last one. The need for the state arose from the fact that when closing the menu, we need to check if we're not closing a menu rendered by some other component. But this can be avoided by checking by exposing a new method from context that takes a node and closes the menu if it is equal to the current menu.

So to simplify the state, we need to have closeMenu function in the context that takes a node and sets the state to null if the node is equal to the current menu. This change will allow us to have all the state in a single place and to reduce the duplication of logic in renderMenu calls.

I'm not sure if I understand you correctly, but do you suggest removing isOpen completely from the state of the component? The component receives currently onChange callback which is executed with new state as a parameter, so removing isOpen from component state is impossible, isn't it?
I agree with the idea of introducing closeMenu provided by context - it will simplify the logic in the component here, and make the code more readable.

Yes, the idea was to remove isOpen. Good point with onChange. I think that still it might be possible to completely remove isOpen: we can create a function e.g. openMenu that calls onChange(true) and renderMenu and a function closeMenu that calls onChange(false) and closeMenu` from context - it's not as clear as effect, but has some other advantages. But we can come back to it later and start with introducing closeMenu. It is probably a good idea to create a new diff that adds this function.

tomek requested changes to this revision.Mar 22 2022, 2:19 AM
This revision now requires changes to proceed.Mar 22 2022, 2:19 AM

Simplified logic by removing isOpen state.

tomek requested changes to this revision.Mar 22 2022, 9:38 AM

Looks ok, but I have some questions inline

web/components/menu.react.js
48–50 ↗(On Diff #10593)

This might be a good idea to add a comment why it needs to be a layout effect

67–69 ↗(On Diff #10593)

Could you explain what is the intention here?

It looks like we have this effect so that the menu is closed when this component is unmounted. It is ok to have it, but it has one important issue: closeMenuCallback depends on menuActionList, so when menuActionList changes, the callback also changes and we close it. Is it intentional?

It might make sense, but also an alternative, where after menuActionList changes we replace old menu with a new one also has some advantages. Curious about the intentions here.

This revision now requires changes to proceed.Mar 22 2022, 9:38 AM
web/components/menu.react.js
67–69 ↗(On Diff #10593)

The aim was to remove listener and execute closeMenu when component is unmounted - e.g. when modal disappears.

I didn't notice, that after menuActionList changes, the menu would disappear. Right now, it never happens, as menu action list is always the same, but how could this behavior be avoided?

Changes in effects logic following Tomek's suggestions

tomek added a reviewer: ashoat.

This diff looks correct, but is complicated so it might be a good idea for @ashoat to take a look

ashoat requested changes to this revision.Mar 25 2022, 2:00 PM
ashoat added inline comments.
web/components/menu.react.js
37–39 ↗(On Diff #10678)

You can remove the { return } part and just use () => <div /> syntax, I think

43 ↗(On Diff #10678)

Is it possible for the result of buttonRef.current.getBoundingClientRect() to change, other than when the user resizes the screen?

59–66 ↗(On Diff #10678)

Can we merge this useEffect into the next one? Then possibly we can get rid of currentlyRenderedMenu

60–65 ↗(On Diff #10678)

We can avoid running the cleanup function if we don't need to, and simplify the logic a bit by early-exiting

70–73 ↗(On Diff #10678)

I'm confused about this condition. Is the intention here "when menuActionList changes, re-render it"? If so I don't think currentlyRenderedMenu is necessary

76–83 ↗(On Diff #10678)

Can you move this up to below where updatePosition is defined? I initially wrote a comment about updating on window resize when I first read updatePosition – I later deleted it after reading this. By localizing code we can make it more readable

This revision now requires changes to proceed.Mar 25 2022, 2:00 PM
web/components/menu.react.js
43 ↗(On Diff #10678)

getBoundingClientRect() value can change if menu position changes for some reason - like e.g. some styles change and the component is moved. I didn't notice such a case in our app, and the window resize operation is the only that can affect menu position.

If we'd like to handle every other case when menu button is moved (although I didn't notice any such case) we can execute updatePosition every render, but this would affect performance a bit more.

70–73 ↗(On Diff #10678)

Not exactly - the condition here is to check if the previous action list has been rendered. currentlyRenderedMenu is a value provided by Menu Context that allows to check if the component's menu is being rendered and to confirm that this component's menu is currently displayed for the user.

As we don't keep state (if it's open now) in menu component, we need currentlyRenderedMenu to check if currently displayed menu items are "our" items, which we do by comparing it in line 70.

If the condition here is false, it means that the currently rendered list (by the menu context) comes from some other menu component, or there is no menu rendered now, so we don't want to render it as we may override some other's menu.

If it's true - it means, that current component displays actions list now, and the list has changed, so we need to update it by executing renderMenu again.

simplify syntax, add early exit and move effect below updatePosition function following Ashoat's suggestions.

ashoat requested changes to this revision.Mar 28 2022, 7:48 PM
ashoat added inline comments.
web/components/menu.react.js
70–73 ↗(On Diff #10678)

This condition is doing two things and it's very confusing.

  1. What you described – if some other component wants to use the MenuContext. But is this behavior well-defined? Is it fully supported by your API? It seems like it would be a pretty broken experience, so I don't think we should be designing for it.
  2. Making sure that renderMenu isn't called twice when the menu is opened. On the first execution of the effect, prevActionListRef.current is set but renderMenu is not called. And then when menuActionList changes the effect will get executed again, and this time renderMenu is called.

This second condition is very difficult to understand, especially when we consider the effects of closing/reopening the Menu, and potentially another component also simultaneously manipulating the Menu. I'd like us to think a bit more about how we can make this logic more readable and clear.

I sent a meeting invite for tomorrow at 11:30am ET, but if it is too late no worries – we can try to follow-up over text. Curious to get @palys-swm's perspective as well.

This revision now requires changes to proceed.Mar 28 2022, 7:48 PM

Talked about this in my 1:1 with Tomek today. Here's a possible solution that I think would be more readable:

  1. Add a new property to MenuContext called currentOpenMenu: symbol, and also setCurrentOpenMenu: symbol => void
    • We want to use a Symbol because it makes it easy to generate a random unique thing that is not equal to anything else. Note that Symbol('foo') !== Symbol('foo') – details here
  2. Make each instance of the Menu component declare const ourSymbol = React.useRef(Symbol()), so each Menu component has its own symbol
  3. Make it so onClickMenuCallback just calls setCurrentOpenMenu(ourSymbol.current) and does nothing else
  4. Adjust the React.useEffect that calls renderMenu so that it calls renderMenu only if currentOpenMenu === ourSymbol.current and if prevActionListRef.current !== menuActionList (see inline)
  5. No longer need to access currentlyRenderedMenu from this component
  6. No longer need to procedurally trigger onChange – we can trigger this declaratively from a React.useEffect based on whether currentOpenMenu === ourSymbol.current
web/components/menu.react.js
79–85 ↗(On Diff #10741)

If you adjust the approach as I suggest, this would look like

Introduced changes from Ashoat suggested solution.
It seems more readable now

ashoat added inline comments.
web/components/menu.react.js
69–72

There are two reasons to put something inside a React.useMemo:

  1. The more important one, which is to make sure objects are not re-generated. In this case we have a primitive (boolean) so it does not matter, but if this useMemo returned an object or an array, then preventing it from regenerating one when the inputs do not change could reduce unnecessary render cycles deeper in the render tree
  2. Sometimes there is some expensive calculation, and React.useMemo can save the processing time by caching the answer. In this case the calculation is extremely cheap

Since neither case applies here, I don't think we should use React.useMemo

This revision is now accepted and ready to land.Mar 30 2022, 10:55 PM
tomek added inline comments.
web/components/menu.react.js
76–87 ↗(On Diff #10863)

Should we use isOurMenuOpen here?

web/components/menu.react.js
83 ↗(On Diff #10863)

menuActionList is still in this dep list, but is not actually needed. Can you please go through all of your dep lists and make sure they don't have anything they don't need to have?

removed redundant dependency and reused isOurMenuOpen where it was possible