React Hooks with Redux & MVVM architecture
Asked Answered
G

1

6

I am trying to update my React application to use MVVM as an architecture pattern. I initially built it out with Hooks and Redux. I've come to find out that every resource I've found uses classes in MVVM. Furthermore, I can't find anything that uses Redux to manage the state. I keep seeing MobX come up, but I am trying to use Redux since I've already got it working and understand it better.

Here is the best resource I've found so far: https://medium.cobeisfresh.com/level-up-your-react-architecture-with-mvvm-a471979e3f21, though ... it's using classes and mobX.

MVVM Architecture (as I've come to understand it)

Model

Holds the state, in my example that would be Redux.

ViewModel

Serves as the middleman between the Model and the ViewController. It handles business logic and has the ability to update the Model.

ViewController

Layer above the view that invokes behaviors derived from the model in response to user actions. Also updates data to be displayed on the view.

View

Dummy component that only displays the data passed in via props or otherwise and renders accordingly.


I have tried breaking the logic out below into something that resembles MVVM and I've been unsuccessful in that endeavor. This component is quite straightforward, it simply reads (<useSelector />) the state from Redux in a hook and spits out the information in the return statement. The second file is where the output of the first component appears on a screen.

import React from 'react';
import styled from 'styled-components';
import { useSelector } from 'react-redux';
import Skeleton from 'react-loading-skeleton';
import styles, { H300, PSpan } from '../../../ui/styles/variables';
import ProfileImage from '../../ProfileImage/ProfileImage';
const { colors, borders, effects } = styles;

const UserWidget = () => {
  const loading = useSelector((state) => state.auth.loading);
  const user = useSelector((state) => state.auth.user);

  if (loading) {
    return (
      <Content>
        <CustomSkeleton circle height={100} width={100} />
        <CustomSkeleton count={4} height={15} width={130} />
      </Content>
    );
  }

  return (
    <Content className="WIDGET">
      <ProfileImage email={user.email} />
      <Username>{user.username}</Username>
      <UserGlobalStats>
        <H300>117,311</H300>
        <PSpan>Points</PSpan>
        <H300>339</H300>
        <PSpan>Words Learned</PSpan>
      </UserGlobalStats>
    </Content>
  );
};

export default UserWidget;
import React from 'react';
import CalendarWidget from '../../../components/_widgets/CalendarWidget/CalendarWidget';
import ModuleHeader from '../../../components/Module/Header/ModuleHeader';
import ModuleBody from '../../../components/Module/Body/ModuleBody';
import ModProgressWidget from '../../../components/_widgets/ModProgressWgt/ModProgressWgt';
import ButtonBlock from '../../../components/_widgets/ButtonBlock/ButtonBlock';
import { Page, LColumn, RColumn } from '../../../ui/styles/layouts';
import UserWidget from '../../../components/_widgets/UserWidget/UserWidget';

const OverviewPage = () => {

  return (
    <Page>
      <LColumn>
        <UserWidget />     <------ Here
        <CalendarWidget />
      </LColumn>
      <ModuleHeader difficulty="Beginners" language="French" />
      <ModuleBody layout="overview" />
      <RColumn>
        <ModProgressWidget />
        <ButtonBlock />
      </RColumn>
    </Page>
  );
};

export default OverviewPage;

Any help would be appreciated. I really don't want to gut and uproot everything I've been working on to include some different technology. I've seen cryptic answers saying that hooks can be handled through MVVM. Surely redux can be too right?

Gnat answered 1/7, 2020 at 20:38 Comment(2)
I have been thinking applying MVVM pattern into large-scaled React-based JavaScript application. By looking the example UserWidget component, there is no need to create ViewModel for it. Because it does not hold any business login that you need to process and pass into the UserWidget. Do you have any fat component that possibly tangled with domain related code, not only receiving some data and show it to the component? I'd like to pitch in where I can be.Axillary
What I meant above business logic* sorry for the typo.Axillary
S
2

I have been looking at this topic recently (May 2023) and I think I can more or less provide a valid answer.

In a more traditional MVVM approach, outside of React, you'll find that MVVM is closely related to data binding with the Observable pattern, like in .NET

But it is also common in Android, and I could not find a link, but something similar could be said about iOS. I don't have much experience in the last two technologies, but I'm concluding this from some research I have been doing.

I think this is the reason why the OP mentions that examples are found using only MobX. But the beauty (and curse) of react is that is very flexible and we can make it fit the MVVM architectural approach with Hooks and Redux, and not necessarily having to adopt an Observable pattern, I think this is totally valid.

Let's first bring a visual aid to better grasp the idea.

  +---------------------------+
  |          View             |
  +---------------------------+
             |   ^
             v   |
  +---------------------------+
  |        ViewModel          |
  +---------------------------+
             |   ^
             v   |
  +---------------------------+
  |          Model            |
  +---------------------------+

A very loose definition of the above:

  • View: Responsible for Presentation and Presentation Logic
  • ViewModel: Responsible for communication between View and Model, and business logic.
  • Model: State content, or data access

The main thing to note is the separation of concerns.

Now, how can we use hooks and Redux while following an MVVM architecture?

The Model

// store.js
import { createStore } from 'redux';
import rootReducer from './reducers';

const store = createStore(rootReducer);

export { store };
// reducers.js
const initialState = {
  counter: 0,
};

const rootReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        counter: state.counter + 1,
      };
    case 'DECREMENT':
      return {
        ...state,
        counter: state.counter - 1,
      };
    default:
      return state;
  }
};

export { rootReducer };

The ViewModel

// useCounterViewModel.js
import { useSelector, useDispatch } from 'react-redux';

const useCounterViewModel = () => {
  const counter = useSelector((state) => state.counter);
  const dispatch = useDispatch();

  const increment = () => {
    dispatch({ type: 'INCREMENT' });
  };

  const decrement = () => {
    dispatch({ type: 'DECREMENT' });
  };

  return {
    counter,
    increment,
    decrement,
  };
};

export { useCounterViewModel } ;

// CounterViewModel.js
import React from 'react';
import useCounterViewModel from './useCounterViewModel';

const CounterViewModel = () => {
  const viewModel = useCounterViewModel();

  return (
    <View
     counter={viewModel.counter}
     increment={viewModel.increment}
     decrement={viewModel.decrement}
    />
  );
};

export { CounterViewModel };

The View

// CounterView.js
import React from 'react';

const CounterView = ({counter, increment, decrement}) => {
  return (
    <div>
      <h1>Counter: {counter}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
};

export { CounterView };

There is a lot going on in this example, but conceptually it fits the MVVM architecture.

One final note is that if you are going to be using a pattern, you should be explicit about the responsibility of each entity. So if we are talking about MVVM, then the patterns you create and their names should reflect the Model-View-ViewModel concepts. It should be really clear what is a View, what is ViewModel, and what is a Model, even if each one is composed of smaller pieces.

  • The Model is composed of the Redux (store + reducer)
  • The ViewModel is composed of a Container Component and a Hook (business logic)
  • The View is just a view, pretty dumb (could also contain UI logic)
Sussman answered 26/5, 2023 at 17:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.