-
-
Notifications
You must be signed in to change notification settings - Fork 831
Extract RoomTileViewModel from RoomTile #5857
Conversation
although while testing, the view_room never seems to get dispatched? so it looks like this might be dead code anyway, still porting it though
Wrt to how the view model change event triggers a rerender, |
Another note is that one can notice there is quite a bit of potential overlap between the stores and the view models (and echo chamber), so even though the view model looks to be only forwarding methods and properties here, it could potentially assume some of those responsibilities. |
@@ -384,25 +239,25 @@ export default class RoomTile extends React.PureComponent<IProps, IState> { | |||
<IconizedContextMenuOptionList first> | |||
<IconizedContextMenuRadio | |||
label={_t("Use default")} | |||
active={state === ALL_MESSAGES} | |||
active={volume === ALL_MESSAGES} |
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.
Should all of these be moved to be getter in the model?
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, can do. We'll also need a setter for each.
@@ -522,11 +523,13 @@ export default class RoomSublist extends React.Component<IProps, IState> { | |||
const visibleRooms = this.state.rooms.slice(0, this.numVisibleTiles); | |||
for (const room of visibleRooms) { | |||
tiles.push(<RoomTile | |||
room={room} | |||
model={new RoomTileViewModel({ |
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.
That might be personal preferences, but I believe spreading the enumerable properties of the view model onto the component might be better
React only does shallow comparison on props for things like React.memo
. Which would force us to implement a areEqual
function every time
It also reads a bit better when going through the debugger sidebar for example
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 same goes for the props comparison with React.PureComponent
. We'd have to implement shouldComponentUpdate
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.
Not quite sure what my preference would be yet, but about shouldComponentUpdate
, I believe you'd need either way when there are complex nested objects inside the model, such as room
.
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, spreading the view model properties was also my first though. I haven't come up with a good way to trigger the component to rerender though this way. The parent would have to rerender basically when a child view model emits change
. And what triggers the parent to rerender if not forceUpdate
or some state variable? Might be better to just put that in the child component then and not also rerender all the sibling components. forceUpdate
has the advantage of not needing to implement shouldComponentUpdate
...
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 be happy to learn about other possibilities here 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 actually went with spreading the view model properties over the component state (so it can update itself by listening to the view model)
Thank you for creating that PR and putting your thoughts in code I like the approach and decoupling the logic from the views feel really nice. Deriving every single thing from state and creating reusable getters with semantic names is also going to be a huge help in my opinion We briefly discussed how to author components once the logic is decoupled and from what I can see in this PR there is still a bit of logic mainly there to handle user input and interactions with the DOM. I don't see a clear reason to be so opiniated one way or another for how people write their components leaving the door open to hooks or classes |
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.
Overall it seems sane - just a couple nitpicks about classes and representation. Docs would also be good :)
public componentWillUnmount() { | ||
this.props.model.off("change", this.rerender); | ||
this.props.model.off("ensureVisible", this.scrollIntoView); | ||
this.props.model.destroy(); |
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.
it's a little surprising that the model gets destroyed here, but I suppose it's the right thing given not much else will know when is best to destroy 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.
Indeed, in a larger example, the parent view model would destroy the child view model when needed (e.g. upon navigation, ...), and the child view model being cleared would cause a re-render and the component to unmount. Given there is no parent view model here yet, I reversed things.
this.props.model.off("change", this.rerender); | ||
this.props.model.off("ensureVisible", this.scrollIntoView); |
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.
we could probably make an abstract class which does a lot of this lifecycle management, making the components smaller/easier to write. Could even make the class take a T
to make this.props.model
magically work.
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.
Have tried to do this fwiw
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.
Overall, the core design pattern here makes sense to me, and seems like a great path forward, thanks for working on this example! 😄
I agree with @turt2live that it would be great to have some longer docs explaining the pattern and some of the design nuances.
@@ -522,11 +523,13 @@ export default class RoomSublist extends React.Component<IProps, IState> { | |||
const visibleRooms = this.state.rooms.slice(0, this.numVisibleTiles); | |||
for (const room of visibleRooms) { | |||
tiles.push(<RoomTile | |||
room={room} | |||
model={new RoomTileViewModel({ |
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.
Not quite sure what my preference would be yet, but about shouldComponentUpdate
, I believe you'd need either way when there are complex nested objects inside the model, such as room
.
export default class RoomTile extends ModelView<RoomTileViewModel, IState> { | ||
private roomTileRef = createRef<HTMLDivElement>(); | ||
|
||
constructor(props: IProps<RoomTileViewModel>) { |
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.
is there a way to declare a Props type inside the ModelView class already taking the passed in view model type into account?
E.g. something like:
type Props = IProps<V>
in ModelView and then here just do RoomTile.Props
instead of IProps<RoomTileViewModel>
or something?
This probably doesn't make any sense, haven't done typescript in a while...
Just asking because I would have to export IProps if I were to extract ModelView to its own file.
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 sure I'm grokking exactly what you're asking but I think you're asking about ComponentProps<typeof XYZ>
(React.ComponentProps)
Nope don't think this is what you are after, you could use Parameters<ModelView["constructor"]>[0]
as the type or something like that to save having to import it again, the above you could wrap in a nicer to read generic type
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.
Interesting, could you give an example of how this could be wrapped?
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.
actually I think I messed that up or have since lost it, /me looks at @turt2live for inspiration
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.
most IDEs will infer the types for people if you just leave it off the constructor, so:
constructor(props) {
super(props); // it now knows that props is IProps<T>
}
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.
also, imo, given we don't use props
but instead this.props
in constructors, I'm not too worried about types - we're just fulfilling the contract, which doesn't need explicit type declarations.
The other option would be to make the props the view model, so people can <Component {...model} />
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.
Looks great to me overall, thanks for writing up the overview, it's very helpful. 😄
Co-authored-by: J. Ryan Stinnett <[email protected]>
First draft of an example how extracting logic from the views into view models can decouple them from the js-sdk, and generally simplify them.
the
room
property on the view model would ideally not be public, but this would have required to make all the avatar components work without a room, which is a larger task.The update mechanism isn't perfect yet. Upon re-rendering the parent view, the
RoomSublist
, it will recreate all the view models. These would ideally live in aRoomSublistViewModel
, but again, I wanted to limit the scope of the PR. Recreating the view model while using the same key will swap the view model on an existingRoomTile
, so we deal with this incomponentDidUpdate
by destroying the old view model and attaching to the new one. We'd need to come up with a better way to handle there not yet being a parent view model while doing this conversion one component at a time.So, if there was a
RoomSublistViewModel
, it would have atiles
ortileViewModels
property which contains all the sub view models. If anything is added or removed from that list,RoomSublistViewModel
will emit an update which causesRoomSublist
to rerender and pick up the changes in the tiles list.The concept what can cause re-renders changes somewhat here. Basically components can only trigger re-renders within their own component based on their own view model, but not within their children as is common with React. Each view model emits a change event to trigger a rerender in their component at their level.
Note that some of the boilerplate code in
RoomTile
could be extracted into a base component class/function specific to work with a view model.This change is marked as an internal change (Task), so will not be included in the changelog.