-
Notifications
You must be signed in to change notification settings - Fork 160
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
MM-33439: Fetch invited users independently #486
Conversation
webapp/src/components/backstage/automation/invite_users_selector.tsx
Outdated
Show resolved
Hide resolved
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.
Good catch, @agarciamontoro! A few thoughts below.
webapp/src/components/backstage/automation/invite_users_selector.tsx
Outdated
Show resolved
Hide resolved
webapp/src/components/backstage/automation/invite_users_selector.tsx
Outdated
Show resolved
Hide resolved
webapp/src/components/backstage/automation/invite_users_selector.tsx
Outdated
Show resolved
Hide resolved
webapp/src/components/backstage/automation/invite_users_selector.tsx
Outdated
Show resolved
Hide resolved
@lieut-data, addressed one (1) of your comments, I replied to the other ones in place to continue the 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.
Nice catch! This is a tricky one for sure.
Not sure if the comment I gave will help or not, but let me know.
// When there are no users invited, options is UserProfile[], a plain list. When there is at least one user invited, | ||
// options contains two groups: the first with invited members, the second with non invited members. This is needed | ||
// because groups are rendered in the selector list only when there is at least one user invited. | ||
const [options, setOptions] = useState<UserProfile[] | GroupType<UserProfile>[]>([]); | ||
const [searchTerm, setSearchTerm] = useState(''); | ||
const invitedUsers = useSelector<GlobalState, UserProfile[]>((state: GlobalState) => props.userIds.map((id) => getUser(state, id))); |
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.
FYI, I believe this will cause the component to rerender everytime the store changes (even unrelated changes) because there's no way for React to know whether or not this selector's dependency inside state
has changed. Maybe a better way do do this would be to put it inside a useEffect
and state the dependency in dependency array.
Not related to this PR: we should think about cleaning up the other times we do this kind of thing in the rest of the code.
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 vaguely remember this discussion in other parts of the codebase, but not the specifics on what caused the re-renders.
I took a look at the docs and, according to this, the component will re-render only if the returned object is different (as in ===
returning false). It's true that the selector is executed with every change in the state, but the reason for the re-render in this specific case would be that invitedUsers
changes. As invitedUseres
is an array that gets created every time the selector is executed, it will definitely re-render, but it looks like we can prevent that by using a custom comparison operator.
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.
Or maybe not: reduxjs/react-redux#1654 I don't get 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.
Anyway, I tested this with the paint flashing tool and I am not able to reproduce what you say. The component does not re-render when I make changes to the store (I tried removing a user, creating a new channel, inviting the user that has the backstage open to a new channel and then kicking them, and posting messages in channels where the user is).
In none of the cases did I get a repaint, so I guess it is not being re-rendered. Maybe I'm missing something else here, of course.
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 don't think the issue is the inability to track the selector's dependencies -- Redux always calls all mapStateToProps
every time the store changes -- but the fact that this selector always returns a new array even if the underlying conditions haven't changed:
const userIds = ['userid1', 'userid2', 'userid3'];
getUser = (userId) => userId;
const call1 = userIds.map((id) => getUser(id));
const call2 = userIds.map((id) => getUser(id));
// returns false
console.log(call1 === call2);
Redux selectors are /supposed/ to be pure, so I think the pattern above will unconditionally trigger React's "render" phase whenever anything in the state changes. However, React won't actually render anything new to the DOM, since the output of this function will be the same as before. This does come at a cost, however, over skipping the render phase altogether.
I add this comment not that I think we necessarily need to touch anything here -- I see this as a low-bandwidth page whose components won't be mounted very often -- but just to share what I imagine is going on under the covers.
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.
Thank you for the insight! That's what I thought, but it's true that checking the repainting is not enough to test that the rendering phase is triggered again. Is there any easy way to test that? (just for the sake of knowing it for the future)
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.
But I still don't get why the issue I linked before is like that: even when using a custom comparison function, the reload is triggered (the useEffect hook in the example in the issue).
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, this is an interesting issue... Definitely we're good to go for this fix, but I really want to know what's going on under the hood here. Wonder if we should have webapp (or someone from our team) do a deep dive and then a bit of a presentation (a brown bag?) to the rest of the dev team
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've been wanting to do that for a while (also for the sake of my understanding, because I feel like I lack some knowledge on the low level aspects of react and redux)
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 might be mistaken, but I think you can just console.log
within the functional component. If it's called, that's React's "render" phase. But React reserves the right to render at any time, so it's not a bug to have it render more than strictly necessary.
A deep dive here would be awesome!
|
||
useEffect(() => { | ||
dispatch(getProfilesByIds(props.userIds)); | ||
}, [props.userIds]); |
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.
Inside here, for example. Turn invitedUsers
into a state hook above, and fill them inside this effect hook.
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 /think/ this is perhaps an anti-pattern, having a component juggle second-order state like this. If this is a concern, perhaps we could introduced a memoized selector instead?
webapp/src/components/backstage/automation/invite_users_selector.tsx
Outdated
Show resolved
Hide resolved
I've simplified the useEffect hook by moving all the logic to build the |
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.
Nice simplifications, @agarciamontoro!
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.
Nice, thanks @agarciamontoro !
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.
Tested and verified that invited members and non-invited members still work as expected.
Approving.
* Fetch invited users independently * Use getProfilesByIds * Add a comment clarifying the filter(u => u) idiom * Make useEffect do one thing: get the list of users
Summary
The component was filtering the invited members out of the list that is fetched when the component is mounted. This list is capped at 100 users (the default for getUsers), so if the invited member was not originally there, it was not shown in the component.
This PR fixes the bug by pre-fetching all the invited members. In particular:
This is a working fix for this dot release, but we still need something more robust for the future (for example, we don't even have pagination in the fetching of the users). I added this to my now long list of things I want to tackle as part of https://mattermost.atlassian.net/browse/MM-32861
About the release itself: I created a branch from the v1.5.1 tag, release-v1.5.2, which is the base for this PR.
Ticket Link
https://mattermost.atlassian.net/browse/MM-33439