Sign Up

Sign Up to our social questions and Answers Engine to ask questions, answer peopleโ€™s questions, and connect with other people.

Have an account? Sign In

Have an account? Sign In Now

Sign In

Login to our social questions & Answers Engine to ask questions answer peopleโ€™s questions & connect with other people.

Sign Up Here

Forgot Password?

Don't have account, Sign Up Here

Forgot Password

Lost your password? Please enter your email address. You will receive a link and will create a new password via email.

Have an account? Sign In Now

Sorry, you do not have permission to ask a question, You must login to ask a question.

Forgot Password?

Need An Account, Sign Up Here

Please type your username.

Please type your E-Mail.

Please choose an appropriate title for the post.

Please choose the appropriate section so your post can be easily searched.

Please choose suitable Keywords Ex: post, video.

Browse

Need An Account, Sign Up Here

Please briefly explain why you feel this question should be reported.

Please briefly explain why you feel this answer should be reported.

Please briefly explain why you feel this user should be reported.

Sign InSign Up

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Navigation

  • Home
  • About Us
  • Contact Us
Search
Ask A Question

Mobile menu

Close
Ask a Question
  • Home
  • About Us
  • Contact Us
Home/ Questions/Q 3013

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Latest Questions

Author
  • 61k
Author
Asked: November 26, 20242024-11-26T10:45:07+00:00 2024-11-26T10:45:07+00:00

MVVM in React ๐Ÿ”ฅ๐Ÿ”ฑ๐Ÿ”ฅ๐Ÿ”ฑ

  • 61k

This is going to be an unpopular one but here goes…

I have been finding that writing my React applications using the MVVM pattern has been preeeetty pretty nice.

Let me explain.

The Problem

I know, I know.

Functional programming is the best programming.

React is functional by design so you should never do something different.

MVVM is the pattern used by Angular ๐Ÿคฎ and Vue ๐Ÿคฎ.

Mutating variables is for plebs.

Yeah I know.

But have you ever;

  • had to migrate between an older version of React to a newer one and be a little stuck because your business logic depended on React features that had breaking changes? (e.g. class components -> functional components)

  • had performance issues tied to React's rendering cycle and struggle to address them because your business logic was tied to your presentation logic?

  • wished you could use use async/await in your components but are instead forced to thunk promises through a colossal state management digest cycle?

  • been stuck on testing React components, not exactly sure how to mock things?

  • gotten dizzy trying to think through functional components?

Yeah… Probably just me

Traffic Light

This is a super simple example but imagine a basic traffic light component.

You want the traffic light to start at green, stay green for 3000ms, transition to yellow for 500ms then red for 4000ms.

A likely conventional approach to this would be something like this:

const states = {   green:  { duration: 3000, next: 'yellow' },   yellow: { duration: 500,  next: 'red' },   red:    { duration: 4000, next: 'green' }, }  function TrafficLight(props) {   const [color, setColor] = useState('green')    setTimeout(() => setColor(states[color].next), states[color].timeout)    return <div>{color}</div> } 
Enter fullscreen mode Exit fullscreen mode

You might put the setTimeout in a useEffect to ensure you can cancel the timer but you get the point

LGTM, what's wrong here?

It's my view that the best projects are those that are accessible to engineers of all experience levels and that are architecturally resilient to lower quality contributions. After all, not everyone is a 10x developer with 20 years experience and that's okay, we can just factor that in.

For that to be the case; code needs to be plain, obvious and, unless required, avoid the use of clever framework-specific magic.

In the above example, the business logic is itself is tied to the render cycle of the framework. This has the effect of limiting the pool of contributors to “framework” engineers / React specialists.

In my travels I have seen many applications do this sort of thing, perhaps at a higher level of abstraction (embedded within state management solutions and so on), but when boiled down practically operating like this.

Alright, so what are you suggesting then?

Let's separate this problem into two halves.

  • Represent the state for the view
  • Render the state for the view

The View Model

Lets make a basic container for the view state.

const sleep = duration => new Promise(res => setTimeout(res, duration))  const states = {   green:  { duration: 3000, next: 'yellow' },   yellow: { duration: 500,  next: 'red' },   red:    { duration: 4000, next: 'green' }, }  class TrafficLightView {   @reactive color = 'green'   #active = true    async onInit() {     while(this.#active) {       await sleep(states[this.color].duration)       this.color = states[this.color].next     }   }    onDestroy() {     this.#active = false   } } 
Enter fullscreen mode Exit fullscreen mode

I know, I know, I am using a disgusting class ๐Ÿคฎ.

Don't worry, as long as you never use extends, it's just a convenient way to group state and associated functions together. Composition over inheritance โœจ

So why is this good?

Firstly, it doesn't matter if you're a React developer, Angular developer, C++ developer, functional programmer, non-functional programmer – you understand what's going on here.

Secondly, you can use async/await, which is pretty nice IMO

Lastly, while testing this still requires mocking a global variable (we can fix that), you can assert success against object properties rather than against DOM changes.

Quick Note on Reactivity

I don't want to dive deeply into this but let's assume you're using something like Mobx where you can decorate class properties to trigger renders when properties change.

If people are interested, I'll write another article on how reactivity works.

import { reactive, subscribe, useViewModel } from './rx.ts'  class TrafficLightView {   // Converts "count" into a getter/setter   @reactive    count = 0    // Called by "useViewModel" hook   async onInit() {     this.count = 1   }    // Called by "useViewModel" hook   async onDestroy() {     console.log('bye')   } }  const tlv = new TrafficLightView() subscribe(tlv, () => console.log(tlv.count)) tlv.onInit() 
Enter fullscreen mode Exit fullscreen mode

The Presentation Layer

So how do we consume this in React?

Let's use the useViewModel hook from our library

import { reactive, subscribe, useViewModel } from './rx.ts' import { sleep } from './utils.ts'  const states = { /* ... */}  class TrafficLightView {   @reactive    color = 'green'   #active = true    async onInit() {        while(this.#active) {       await sleep(states[this.color].duration)       this.color = states[this.color].next     }   }    onDestroy() {     this.#active = false   } }  function TrafficLight() {   const vm = useViewModel(() => new TrafficLightView())   return <div>{vm.color}</div> } 
Enter fullscreen mode Exit fullscreen mode

A Real Use Case

I know the above implementation is more verbose when compared to the more conventional React approach – however the benefits start to shine through when applied to something less trivial.

Say we have a REST API we want to talk to that has paginated results and we want to ensure that the API layer is abstracted away such that it can be tested without the framework.

This is just plain, framework agnostic, TypeScript:

export type Gif = {   id: string   url: string   image: string }  class GiphyService {   async *getTrending(): AsyncIterableIterator<Gif[]> {     let offset = 0      while (true) {       const response = await fetch(         `https://api.giphy.com/v1/gifs/trending?offset=${offset}&limit=20`       );        if (!response.ok) {         throw new Error("Request failed")       }        yield (await response.json()).data       offset += 20     }   } } 
Enter fullscreen mode Exit fullscreen mode

This service provides you the trending gifs in the form of an async iterator where pagination is automatically handled when the iterator is advanced

const giphyService = new GiphyService() const pages = giphyService.getTrending()  const page1 = await pages.next() const page2 = await pages.next() const page3 = await pages.next() 
Enter fullscreen mode Exit fullscreen mode

Let's write a View Model + Component

class HomePageViewModel {   #pages: AsyncIterableIterator<Gif[]>   @reactive gifs: Gif[]    constructor(giphyService: GiphyService) {     this.gifs = []     this.#pages = giphyService.getTrending()     this.loadMore()   }    async loadMore() {     // Mutation triggers a re-render     this.gifs.push(...await this.#pages.next())    } }  function HomePage({ giphyService }) {   const vm = useViewModel(() => new HomePageViewModel(giphyService))    return <div>     <div>       {vm.gifs.map(gif => <img          key={gif.image}         src={gif.image} />)}     </div>      <button        onClick={() => vm.loadMore()}>       Load More     </button>   </div> } 
Enter fullscreen mode Exit fullscreen mode

Notice how the template now only contains logic to handle presenting the data rather than managing presentation and state.

This also enables us to use async/await to drive React rendering, something that is otherwise a little annoying.

Now the GiphyService can be tested by itself and the component can be tested by itself.

Other Advantages, Forms

class HomeViewModel {   @reactive   input = new FormField('') }  function Home(props) {   const vm = useViewModel(() => new HomeViewModel())    return <div>     <input {...vm.input} />   </div> } 
Enter fullscreen mode Exit fullscreen mode

Where FormField looks like:

class FormField {   @reactive   value    constructor(defaultValue) {     this.value = value   }    onChange = (event) => {this.value = event.target.value} } 
Enter fullscreen mode Exit fullscreen mode

Now we can expand this field type with validation, validation states and even create larger abstractions like FormGroup which composes multiple FormField objects.

class HomeViewModel {   @reactive   form = new FormGroup({     name: new FormField({       placeholder: 'Enter your name',       defaultValue: '',     }),     email: new FormField({       placeholder: 'Enter your email',       defaultValue: '',       validators: [emailValidator]     })   })    async submit() {     if (!this.form.isValid) {       alert('Form not valid!')     }     await submitToApi(this.form.toJSON())   } }  function Home(props) {   const vm = useViewModel(() => new HomeViewModel())    return <div>     <input {...vm.form.name} />     <input {...vm.form.email} />   </div> } 
Enter fullscreen mode Exit fullscreen mode

Perhaps even generating forms directly from the state

 function Home(props) {   const vm = useViewModel(() => new HomeViewModel())    return <div>     {vm.form.fields.map(field => <input {...field} />)}   </div> } 
Enter fullscreen mode Exit fullscreen mode

Again, the objective is to keep the component code minimal, focused as much as possible on presentation whist the view model can be groked with eyes that are not specialized to React.

Closing Thoughts

It's a bit unconventional, but I feel that the intersection of the MVVM/MVC worlds of Angular and Vue with the React world has the potential to improve the readability, testability and portability of React projects.

It doesn't require anything fancy to implement and, though unconventional, it feels like it naturally fits within React

javascriptmvcreactwebdev
  • 0 0 Answers
  • 0 Views
  • 0 Followers
  • 0
Share
  • Facebook
  • Report

Leave an answer
Cancel reply

You must login to add an answer.

Forgot Password?

Need An Account, Sign Up Here

Sidebar

Ask A Question

Stats

  • Questions 4k
  • Answers 0
  • Best Answers 0
  • Users 2k
  • Popular
  • Answers
  • Author

    How to ensure that all the routes on my Symfony ...

    • 0 Answers
  • Author

    Insights into Forms in Flask

    • 0 Answers
  • Author

    Kick Start Your Next Project With Holo Theme

    • 0 Answers

Top Members

Samantha Carter

Samantha Carter

  • 0 Questions
  • 20 Points
Begginer
Ella Lewis

Ella Lewis

  • 0 Questions
  • 20 Points
Begginer
Isaac Anderson

Isaac Anderson

  • 0 Questions
  • 20 Points
Begginer

Explore

  • Home
  • Add group
  • Groups page
  • Communities
  • Questions
    • New Questions
    • Trending Questions
    • Must read Questions
    • Hot Questions
  • Polls
  • Tags
  • Badges
  • Users
  • Help

Footer

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore, ask, and connect. Join our vibrant Q&A community today!

About Us

  • About Us
  • Contact Us
  • All Users

Legal Stuff

  • Terms of Use
  • Privacy Policy
  • Cookie Policy

Help

  • Knowledge Base
  • Support

Follow

© 2022 Querify Question. All Rights Reserved

Insert/edit link

Enter the destination URL

Or link to existing content

    No search term specified. Showing recent items. Search or use up and down arrow keys to select an item.