Note: This is the second part of our series on the MVVM architecture in the frontend. If you haven’t read the first part yet, we highly recommend
The connection between the UI and UiLogic layers is critical for ensuring seamless interaction while maintaining isolation. This connection must ensure:
Encapsulation: The logic and state changes of one component don’t affect others.
Reactivity: The UI updates dynamically when the state in the UiLogic changes.
Reusability: A single component can serve as a reusable environment for various UiLogic implementations.
To achieve this, we’ll create a reusable LogicProvider component. It will encapsulate the UiLogic and provide the necessary bridge to the UI.
interface IUiLogicProvider<UI_LOGIC> extends PropsWithChildren {
Ui: FC<any & { uiLogic: UI_LOGIC}>;
UiLogic: IBaseUiLogic<UI_LOGIC>;
restProps?: Record<string, any>;
}
function UiLogicProvider<UI_LOGIC>(props: IUiLogicProvider<UI_LOGIC>) {
const { Ui, UiLogic, restProps, children } = props;
const uiLogic = UiLogic.useUiLogic();
const allProps = {
...restProps,
uiLogic,
};
return <Ui {...allProps}>{children}</Ui>;
}
In this basic component we’re getting UiLogic instance and based on the interface convention for our UI logic we call our ui logic to run and pass the result to the Ui.
Notes and Optimization Tips
Encapsulation: The UiLogicProvider encapsulates all the logic-specific state updates and re-renders, ensuring other components remain unaffected.
Automatic Memoization: By wrapping this provider with memo, you avoid the need to explicitly use React.memo or similar techniques for each child component as you know your UI component should just be rerendered by changes of the Uilogic not rerendering of the parents. This ensures optimal performance without extra effort in an automatic way.
For example:
export default memo(UiLogicProvider, (prevProps, currProps) => {
if (
prevProps.restProps !== currProps.restProps ||
prevProps.children !== currProps.children
)
return false;
return true;
});
- This component is the most basic one. You can get the base concept of it and use it with other ideas and techniques as well. The main concept is one layer will handle calling the Ui logic to avoid other components getting affected by this component.
Last part
The last part will be the easiest one. Just from required parent component connect Ui and Ui logic with our provider.
function App() {
return (
<div>
<UiLogicProvider Ui={ButtonComponent} UiLogic={new CounterUILogic()} />
<UiLogicProvider Ui={ButtonComponent} Uilogic={new CancelButtonLogic()} />
</div>
);
}
Transitioning to MVVM Architecture in Bridge Pattern
In the MVVM (Model-ViewModel-View) architecture:
- Model: Handles the business logic and manipulates stored data.
- ViewModel (VM): Focuses on UI logic and connects the Model to the View.
- View: Manages the UI rendering and binds to the ViewModel via an interface, typically called IVM, which acts as the Bridge.
This architecture builds on the Bridge Pattern we discussed earlier, but introduces a Model layer to separate business logic, ensuring a clean and scalable foundation for large-scale applications.
Mapping Bridge Pattern to MVVM
Here’s how the Bridge Pattern concepts translate into MVVM:
| Bridge Pattern | MVVM | Role |
|---|---|---|
| Abstraction Layer | View | Reusable UI components that bind to the ViewModel. |
| Bridge | IVM Interface | Connects the View to the ViewModel. |
| Implementation Layer | ViewModel (VM) | Handles UI logic and mediates between View and Model. |
| - | Model | Business logic and data manipulation. |
Till now we had all our logic inside of the Implementation layer part if we separate business logic into separate classes or functions and we name it as a model. So the only remains in Implementation is just real UI logic and just with one connection with the model we can send all data to UI and handle UI logic as well.
In this case we’ll have a clean foundation architecture based on MVVM for our large scale application that is reusable, maintainable and testable.
Now in MVVM (Model-ViewModel-View) our Model is responsible for business logic, ViewModel is responsible for UI logic and connecting model to view and then we’ll have our reusable View part that connects to ViewModel with one interface as the IVM which plays our bridge.
Now lets review the diagram but this time in MVVM architecture.

Now you can see in the above diagram that View is replaced with an abstraction layer, IVM replaced with Bridge and VM replaced with an implementation layer. Also we add Model as a layer which is responsible for business logics and manipulation of stored data.
Now we can translate our code example to MVVM shape. For this reason we need to just replace the UI name with View and UiLogic with VM. That’s it now you have a base MVVM approach architecture in your react.
Rewriting the example in MVVM
View (UI)
function AppButton(props: ButtonProps) {
const { vm } = props;
return (
<button disabled={vm.props.disabled} onClick={vm.onClick}>
{vm.props.title}
</button>
);
}
IVM and IProps (Bridge)
interface ButtonVM {
props: {
title: string;
disabled: boolean;
}
onClick(e: MouseEvent): void
}
interface ButtonProps {
vm: ButtonVM;
... // rest of props comes from the parent
}
VM (UI Logic)
CounterVM implements IBaseVM<ButtonVM> {
useVM(): ButtonVM {
const [count, setCount] = useState(0);
return {
props: {
disabled: false,
title: `Current counter is: ${count}`,
},
onClick: () => setCount((prev) => prev + 1),
};
}
}
Final Conclusion
With this journey, we’ve wrapped up building a clean MVVM architecture in a React application, ensuring it aligns with the best practices of performance and software architecture.
Separation of Concerns: Each layer has a distinct responsibility with using Model, ViewModel (VM), View.
Bridge Pattern Integration: The IVM interface serves as the Bridge, providing a clean abstraction between the View and the ViewModel, making the architecture flexible and reusable.
Reusability and Testability: Models, ViewModels, and Views are independently reusable and testable, ensuring long-term maintainability and reducing the cost of changes.
Performance Optimization: With the use of a UiLogicProvider, we’ve ensured that re-renders are isolated to only the relevant components.
By encapsulating the ViewModel logic, we’ve also enabled automatic memoization, avoiding unnecessary re-renders across the application.
Scalability: The modular design makes the architecture inherently scalable, suitable for large-scale applications with complex requirements.
Congratulations on completing these two articles with me! I hope you found them helpful. If you did, please let me know in the comments and feel free to like and share to help more people discover and benefit from these ideas. I’d also love to hear your suggestions or ideas about coding architecture and approaches that have worked well for you!
SOCIAL SHARE CARD GENERATOR