-
-
Notifications
You must be signed in to change notification settings - Fork 830
Conversation
import { RoomMember } from "../../../models/rooms/RoomMember"; | ||
import { ThreePIDInvite } from "../../../models/rooms/ThreePIDInvite"; | ||
|
||
export interface IMemberService { |
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.
Our code style says not to use I-prefixes on interfaces
useEffect(() => { | ||
viewModel.load() | ||
return () => { | ||
viewModel.unload() |
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.
Why can't this load/unload be done by the useMemberListViewModel
hook? or at least have a useModel(viewModel)
hook.
load(): void | ||
unload(): void | ||
loadMembers(searchQuery: string): Promise<Record<"joined" | "invited", RoomMember[]>>; | ||
setOnMemberListUpdated(callback: (reload: boolean) => void): void; | ||
setOnPresemceUpdated(callback: (userId: string) => void): void; | ||
getThreePIDInvites(): ThreePIDInvite[]; | ||
shouldShowInvite(): boolean; | ||
showPresence(): boolean |
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.
doc comments needed here, what's the diff between load and loadMembers given one implies this concerns only members based on its name
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.
Yea some poor naming clashes there, will update and document properly. load/unload were meant as general ViewModel lifecycle functions, loadMembers
is a relic of the original implementation.
Hi, since I've been transitioning Element Call to an MVVM architecture recently I thought I'd provide some feedback. This initiative is really cool to see, but in my view there are also some missed opportunities here:
I imagine there are benefits to keeping everything contained inside React, like familiarity and integration with existing hooks, but I do want to at least share how pleasant the experience with observables has been in Element Call! For instance, here is an observable deciding which speaker to put in the spotlight. Previously this logic was spread out across multiple files, and you had to follow the dynamics of some React components to understand it. Then to bind those observables to React, we're just calling hooks from the observable-hooks package. Hope my perspective is useful in some way 👋 |
Thanks for your feedback @robintown ! React/Web isn't traditionally my wheelhouse(I come from mobile and it's been a few years since i've written React). What led me toward using hooks for the viewModel was just that it seemed like a React-y way to combine logic and state. Having continued to play with this example, I've tied myself in knots a little trying to get my use callbacks/effects/dependencies correct and it worries me a bit what it would be like with a more complicated ViewModel. This might just be due to my inexperience, but I can't help but think it would be simpler with a plain class. As you said this would be more the norm for ViewModels, so I think re-working with a class might be a good next steps for this. I've used observables plenty coming from mobile to similar effect, with the ViewModels presentation state. It's also something that has tended to divide opinion on the teams I've worked in the past so I'd be interested to get a sense of how others feel. |
Thanks so much for doing this. It's really illustrative of how the view the split can look. Yeah, having the view model as a hook will obviously be limiting if we do want to switch frameworks, and the hook logic can be hard to get your head around. That said, it could save a lot of boilerplate code since the hooks can automatically integrate seamlessly with the components (eg. setup/teardown on mount/unmount). How would we test the View itself? I'm wondering about the viewmodel being passed in as a prop to the view so that tests could do this. It would make it more awkward to use though. |
…ted as a class with reactive state and exposed via a hook.
I have changed to a ViewModel written as a class and that uses rxjs to represent the presentation state. As @robintown suggested I found this much easier to handle the flow of data and it was way quicker to write and debug. The viewModel is still exposed as a hook and I used @dbkr as it is just a simple interface exposed we could find a way to test the view by providing a mock implementaiton of that hook interface. Or as we do on mobile, test the and viewmodel as unit together and then I think you only need to use the MockMemberService which is already mocked for the unit tests of the ViewModel. Will try add some component tests shortly. |
Yeah, the little layer of glue between rxjs and react seems okay and avoids complete dependency on react. Testing the two together might be easiest if we can just mock out the service, although I assume we want to split them out sometimes as that's part of the advantage of MVVM. As long as we can mock the viewmodel though, that's fine. |
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.
Some thoughts and questions:
}; | ||
} | ||
const MemberList: React.FC<IProps> = (propsIn: IProps) => { | ||
const viewModel = useMemberListViewModel(propsIn.roomId); |
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'd opt for dependency injection here i.e you pass the vm as prop. Otherwise how would you render this component with a mock/different vm?
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.
Yup, makes sense thanks. Was going to do something like this next so I could create component tests.
onClose(): void; | ||
onSearchQueryChanged: (query: string) => void; | ||
} | ||
|
||
interface IState { | ||
loading: boolean; | ||
filteredJoinedMembers: Array<RoomMember>; | ||
filteredInvitedMembers: Array<RoomMember | MatrixEvent>; | ||
canInvite: boolean; | ||
truncateAtJoined: number; | ||
truncateAtInvited: number; | ||
onInviteButtonClick(roomId: string): void; | ||
onThreePIDInviteClick(eventId: string): void; |
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.
The view should only depend on the view model so these should be moved to the vm.
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.
For callbacks specifically that are only relevant to the parent component (so if we are forwarding to the VM it will probably just be proxied to the parent) I think maybe it's open to interpretation of the MVVM implementation, I've seen done both ways. Might depend on the design of the overall navigation. I'm happy to go with whatever we align on though.
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'm not convinced that rx-js
provides any value here. Data from the MemberService
is first converted into an observable and then immediately converted into a react state. All this complexity is worth it if we do want to move away from react but otherwise YAGNI. Also remember that this glue needs to be repeated for each view.
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.
rx-js
was used here to ease the implementation of the ViewModel by providing a reactive presentation state(so that the UI updates when the presentation state does). The view models's role is generally to transform data and notify downstream of it's updates, which tends to fit well reactive libraries like this.
So it's not an attempt to move away from react, just a pragmatic decision to simplify the implementation of the ViewModel( I found it much easier to implement with like this than with a hook).
Have you an alternative in mind @MidhunSureshR?
showPresence={this.showPresence} | ||
/> | ||
); | ||
return <MemberTile key={m.userId} member={m} showPresence={props.showPresence} />; |
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 imagine eventually we'd refactor MemberTile
to also use MVVM. How would the code look then?
I imagine :
return <MemberTile viewmodel={?} />;
Where would the view model come from in this case?
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'm guessing you're considering this as the member tile contains things like the shield, requiring dynamic e2e status?
Two approaches I can think of:
- Nested view models
- Just consider it another "non-smart" functional component and it's the responsibility of the MemberListView to provide the e2e data. If we found a need to re-use that state across view models, then use some form of composition.
The purpose of this PR is to:
The PR is large as it's difficult to demonstrate an architecture and the benefits without a pretty complete example. It's functioning but not complete(there are more improvements to make and more things to test). If we are happy to move ahead with this as an approach we would apply it as a series of separate PRs(We do actually want to make UX improvements to the MemberList that are not included int he scope of this example).
MVVM
Summary
Better separation of concerns
MemberList.tsx
). This means this logic has to be understood and tested all together as one unit.MemberService
wraps our business logic(from the js-sdk) and returns the data model which is make up of simple data types. In this PR I've added to some models that already existed undermodels/
.MemberList
contains just UI code(Functional Component) . It takes only simple data types and functions to send back commands.MemberListViewModel
contains just presentation logic and state.IMemberService
interface so that for the defined data model inputs and users commants we assert the presentation logic works by checking the model outputted for the UI. Similarly this also makes it easier to test the UI. We could again use the the Mocked Service and the ViewModel to output the different valid states for a particular screen, then rendering output and asserting it's validity using snapshot/screenshot component tests. Screentshot tests like this should be predictable(i.e. for this data the ui should look like this). E.g. Maybe in removing js-sdk from the avatar UI components, by separating business and presentation logic from the UI we can isolate some of the bugs that are causing tests flakiness.IMemberListService
to pass to the ViewModel.Related Material
Uncle bob's clean architecture is a complementary resource on how to organise a codebase, it's come up in conversation so just linking here in case anybody is not familiar or wants a refresh.