react-mvvm-hooks v0.2.4
Getting Started
This package is a toolset that will help you to use MVVM with
React hooks
The pattern
In this project we are using a pattern based on MVVM + Factory.
The base
The structure is divided in 4 layers: Factory > View > ViewModel > Model
Model
This is where the data begins, where the data comes from. In this case we are
using UseCases to reach the data. It can be through API requests,
cache, localstorage, etc
ViewModel
This is our business logic and state is, it owns the Model. All the handlers
and states have to be here. So if you need to access some data using a
UseCase, it must be here.
View
The View is our layout, it's the visual layer of the application. It owns the
ViewModel and it CAN'T have any logic. This is important. All the the logic
must be in the ViewModel, the View will watch the ViewModel and react
accordingly.
Factory
The Factory is used to create a instance of the View and connects it to the
ViewModel. To do that you need to inject the ViewElement, viewModelFactory
and args into the ViewFactoryComponent as props.
Folder Structure
To keep things organized you must follow some kinda pattern for you view folder. The suggestion is this:
src
|--components // shared components, not views
|--view
| |--Subscription
| | |--components // components that will be used only for this View
| | | |--Input
| | | | |--index.tsx
| | | | |--[...]
| | |--view.tsx
| | |--viewModel.ts
| | |--types.ts
| | |--index.ts // this one is only necessary if you want to create the FactoryComponent
| | |--[...]How to use it
You can follow this example in the examples/injecting-views in this
repository.
The first step is Create a View inside the view folder. Let's call it
HomePage (Folder: view > HomePage).
Attention to the typing, this is very important.
1. types.ts file
This file is used only to separate the types/interfaces from the actual
View, so create the types.ts inside the HomePage folder:
import { View } from 'react-mvvm-hooks';
export interface HomePageState extends View.State {
count: number;
handleClickCountButton(): void;
}2. viewModel.ts
The ViewModel is where the business logic is, and it's very important to keep
all the heavy code inside.
import { useCallback, useState } from 'react';
import { ViewModel } from 'react-mvvm-hooks';
import { InnerButtonView } from '../InnerButton/view';
import { useInnerButtonViewModel } from '../InnerButton/viewModel';
import { HomePageState } from './types';
export const useHomePageViewModel: ViewModel.Hook<HomePageState> = () => {
const [count, setCount] = useState(0);
const handleClickCountButton = useCallback(() => {
setCount(count + 1);
}, [setCount, count]);
return {
count,
handleClickCountButton,
};
};The ViewModel is a hook, so remember always to prefix it with use. You can
set all the states here. Try to avoid this file to be too big, if necessary
create another hook insider of the HomePage folder to help abstracting some
logic.
view.tsx
This is where our layout goes. Simple as that, only layout stuff, no logic. Get
the state and handlers from props and use it.
import { View } from 'react-mvvm-hooks';
import { HomePageState } from './types';
export const HomePageView: View.Component<HomePageState> = ({
count,
handleClickCountButton,
}) => {
return (
<div
style={{
height: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div>
<button onClick={handleClickCountButton}>Count: {count}</button>
</div>
</div>
);
};Connecting with ViewFactory (src/main.tsx)
Then we have it! Our View is ready, now let's connect it to the ViewModel.
import React from 'react';
import ReactDOM from 'react-dom/client';
import { HomePageView } from './view/HomePage/view';
import { useHomePageViewModel } from './view/HomePage/viewModel';
import { FactoryHelpers } from 'react-mvvm-hooks';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<FactoryHelpers.ViewFactory
ViewElement={HomePageView}
useViewModel={useHomePageViewModel}
/>
</React.StrictMode>,
);We use the FactoryHelpers.ViewFactory that is a generic component to connect
them.
Custom ViewFactory (/HomePage/index.tsx)
If you want to create your Custom ViewFactory, it's also possible.
import { FactoryHelpers } from 'react-mvvm-hooks';
import { HomePageState } from './types';
import { useHomePageViewModel } from './viewModel';
import { HomePageView } from './view';
export const HomePageViewFactory: React.FC = () => {
const props = useHomePageViewModel();
return <HomePageView {...props} />;
};The FactoryHelpers also has a ViewFactory creator:
FactoryHelpers.create<>(). The only thing you need to do is type it:
import { FactoryHelpers } from 'react-mvvm-hooks';
import { HomePageState } from './types';
export const HomePageViewFactory = FactoryHelpers.create<HomePageState>();Injecting a View inside another View
Sometimes you need to add more functionalities and may want to split in different views. For instance toolbar that is going to be displayed in some pages with different logic inside it.
For this example we're going to keep things simple: Add a seconde Counter
inside the first View
Create your new View in our case InnerButton
Inside the view/InnerButton it has the same structure as the other one, so it
goes:
/InnerButton/types.ts
import { View } from 'react-mvvm-hooks';
export interface InnerButtonState extends View.State {
count: number;
handleClickCountButton(): void;
}
export interface InnerButtonArgs extends View.Args {
initialCount: number;
}Now we added the Args. It's needed when you want to inject something in the
ViewModel hook.
/InnerButton/viewModel.ts
import { useCallback, useState } from 'react';
import { ViewModel } from 'react-mvvm-hooks';
import { InnerButtonArgs, InnerButtonState } from './types';
export const useInnerButtonViewModel: ViewModel.Hook<
InnerButtonState,
InnerButtonArgs
> = ({ initialCount }) => {
const [count, setCount] = useState(initialCount);
const handleClickCountButton = useCallback(() => {
setCount(count + 1);
}, [setCount, count]);
return {
count,
handleClickCountButton,
};
};Pay attention to the type generics, now we have the type ViewModel.Hook with
2 generics: State and Hooks
/InnerButton/view.tsx
import { View } from 'react-mvvm-hooks';
import { InnerButtonState } from './types';
export const InnerButtonView: View.Component<InnerButtonState> = ({
count,
handleClickCountButton,
}) => {
return (
<button onClick={handleClickCountButton}>Inner Count: {count}</button>
);
};Connecing one View to another
To do that we need to go back to the HomePage view and modify somethings:
/HomePage/type.ts
import { View } from 'react-mvvm-hooks';
import { InnerButtonArgs, InnerButtonState } from '../InnerButton/types';
export interface HomePageState extends View.State {
count: number;
handleClickCountButton(): void;
innerButton: View.Reference<InnerButtonState, InnerButtonArgs>;
}We used the interface View.Reference so the state returned from
homePageViewModel knows what to return to the view.
/HomePage/viewModel.ts
import { useCallback, useState } from 'react';
import { ViewModel } from 'react-mvvm-hooks';
import { InnerButtonView } from '../InnerButton/view';
import { useInnerButtonViewModel } from '../InnerButton/viewModel';
import { HomePageState } from './types';
export const useHomePageViewModel: ViewModel.Hook<HomePageState> = () => {
const [count, setCount] = useState(0);
const handleClickCountButton = useCallback(() => {
setCount(count + 1);
}, [setCount, count]);
return {
count,
handleClickCountButton,
innerButton: {
args: { initialCount: 10 },
ViewElement: InnerButtonView,
useViewModel: useInnerButtonViewModel,
},
};
};We added to the return the innerButton which was typed in the type.ts
file.
/HomePage/view.ts
import { FactoryHelpers, View } from 'react-mvvm-hooks';
import { InnerButtonFactory } from '../InnerButton';
import { HomePageState } from './types';
export const HomePageView: View.Component<HomePageState> = ({
count,
handleClickCountButton,
innerButton,
}) => {
return (
<div
style={{
height: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div>
<button onClick={handleClickCountButton}>Count: {count}</button>
</div>
<div>
<FactoryHelpers.ViewFactory {...innerButton} />
</div>
</div>
);
};DO NOT import directly the InnerButtonView, always use
FactoryHelpers.ViewFactory in this case, it's going to avoid circular
dependency.