Tips for implementing self-managed state in Vue.js data object

My approach in organizing my Vue application involves using classes to encapsulate data, manage their own state (edited, deleted, etc), and synchronize with the back-end system. However, this method seems to conflict with Vue in some respects.

To illustrate the issue with minimal code, let's consider a simplified version of my current setup.

Initial Strategy

Let's assume we have an object that defines the data as follows:

ModelState.ts

export public enum ModelState { saved, modified, deleted, new }

NameModel.ts

export public class NameModel {
  public name: string = 'New name'
  public state: ModelState = ModelState.modified

  public save(): Promise<boolean> {
    this.state = ModelState.saved
    return Promise.resolve(true);
  }
}

Now, I want to render this in a Vue component like this:

<script setup lang="ts">
import { NameModel } from 'NameModel'
import { ModelState } from 'ModelState'

export interface MyComponentProps{
  nameModel: NameModel
}
const props = defineProps<MyComponentProps>();

function saveClicked() {
  props.nameModel.save()
}
</script>

<template>
  <div>
    <input v-model="nameModel.name">
    <button 
      v-if="nameModel.state == ModelState.modified" 
      @click="saveClicked()">Save</button>
  </div>
</template>

The problem here is that although clicking the Save button updates the state property of NameModel, the UI doesn't reflect this change due to the proxy wrapping caused by passing NameModel as a prop.

Alternative Approaches

In an attempt to solve this, I made the state property reactive in the NameModel class:

NameModel.ts

import { ref, type Ref } from 'vue'
export public class NameModel {
  public name: string = 'New name'
  public state: Ref<ModelState> = ref(ModelState.modified)

  public save(): Promise<boolean> {
    this.state.value = ModelState.saved
    return Promise.resolve(true);
  }
}

Despite passing all unit tests, this modification resulted in a runtime error due to Vue unwrapping the reactive properties and changing the type of the object's state property from Ref<ModelState> to ModelState.

Revised Approach

To resolve the issue, I shifted the state management logic from the data model to the component. While this solution works, it compromises separation of concerns and requires duplicating the logic across components that mutate the state.

The updated code now looks like this:

export public class NameModel {
  public name: string = 'New name'
  public state: ModelState = ModelState.modified

  public save(): Promise<boolean> {
    return Promise.resolve(true);
  }
}
<script setup lang="ts">
import { NameModel } from 'NameModel'
import { ModelState } from 'ModelState'

export interface MyComponentProps{
  nameModel: NameModel
}
const props = defineProps<MyComponentProps>();

function saveClicked() {
  props.nameModel.save().then((success) => {
    if (success) props.nameModel.state = ModelState.saved
  })
}
</script>

<template>
  <div>
    <input v-model="nameModel.name">
    <button 
      v-if="nameModel.state == ModelState.modified" 
      @click="saveClicked()">Save</button>
  </div>
</template>

Although functional, this workaround feels inadequate. What might be a better approach for handling this?

Additional Details

While this example provides a basic demonstration, my actual implementation is more complex.

I have a Vue component with slots for various user experiences - view, edit, delete, add new. Despite being usable with any data model inheriting from a base class like PersistedEditable, this component now embeds the state management logic, leading to issues with distinct data models requiring different behaviors or permissions.

Furthermore, plans to implement WebSocket notifications for backend changes pose a challenge as updating the data model's state should ideally be handled within the model itself, not the Vue component.

Motivation (EDIT)

Adding more context to my query, I aim to leverage Vue's reactive UI state management capabilities, motivated by past positive experiences with Vue and its reactivity features.

However, my goal is to separate data state management from UI state management. For instance, when receiving a WebSocket message about data deletion, I want all displaying components to adjust based on this state change without duplicating logic across components.

Playgrounds (EDIT)

First attempt - not reactive

Second attempt - runtime error due to unwrapping

Third attempt - works but logic feels misplaced

Composable - suggested by @TheHiggsBroson

Renderless component - I think this is what @TheHiggsBroson suggested

Answer №1

This concept you've presented is truly intriguing. While I'm not certain if achieving this without integrating Vue functionality is possible, I appreciate the unique approach you're taking. Rather than being the typical critic on Stack Overflow, I understand your motivation and have a few potential solutions in mind.

It appears that your aim with these classes is to separate state logic from the component, packaging units of data separately from display logic. If my interpretation is correct, there are ways to address your concerns effectively.

Despite the limitations of using the vanilla class operator for this purpose, there are alternative options available, each with its own set of advantages and drawbacks:

Pinia

Pinia offers a "type safe, extensible, and modular by design" solution similar to a class. It enables you to contain both state and methods just like a class would. Whether through the options API or the composition API, Pinia ensures consistent state management across multiple instances throughout your codebase.

Composables

If you find Pinia's shared state setup inconvenient, consider utilizing composables with the Vue3 Composition API. This alternative allows each instance to maintain distinct memory, offering flexibility in managing states within different components.

Renderless components

Another option involves employing renderless components to store and expose state as needed. Though unconventional, this method grants creative freedom in structuring your codebase but may lack the same level of flexibility as composables.

I trust these suggestions provide some valuable insights for your project. Should you require further assistance, feel free to inquire!

EDIT

Proxy Class

A Proxy object could offer an alternative solution to achieve your desired outcome. By intercepting operations like property setting and getting, it can serve as a viable option if you prefer not to integrate a full-fledged state management library into your project.

Answer №2

If you stumble upon this discussion and want to adopt a similar approach, here is how I tackled it. Shoutout to @TheHiggsBroson for steering me in the right direction with their insightful response.

Customizable Solution

The crux of my solution lies in the Editable.ts file (featured below). It introduces

type Editable<Data> = {...} & Data
, which effectively extends any type Data to include a set of properties and methods responsible for managing the editable state.

Reactivity is achieved by defining a function that constructs a new reactive(Editable<Data>) instance while retaining a reference to it. Subsequently, this function appends methods to Editable<Data> that operate on the reactive proxy, thus ensuring all mutations to the editable are reactive.

The key sections of the newEditable function appear as follows (refer to the complete source code below):

type Editable<Data> = {
  state: string
  cancel(): void
} & Data

const newEditable = function(data: Data): Editable<Data> {
  var editable: Editable<Data> = reactive({ ...data, state }) as Editable<Data>

  editable.cancel = () => {
    // Reflect changes made on the reactive proxy for the editable
    // despite this cancel function being a method of Editable
    editable.state = 'cancelled'
  }
  
  return editable
}

Pinia Stores Implementation

I store all instances of Editable<Data> in pinia, where they are cached and undergo backend integration operations like retrieval, saving, deletion, etc., from the database.

Each store caters to a specific object type and exposes a utility function, such as usePerson(id?: string), which furnishes an editable person wrapper for an existing record or fabricates a novel person record if id is null.

If the editable person isn't present in pinia, the store retrieves the record from the backend and employs the newEditable() method discussed earlier to render it editable.

CrudPanel Composition

A versatile Vue component named CrudPanel orchestrates view, edit, delete functionalities via slots, exhibiting context-sensitive buttons contingent on the current state of the Editable<Data> received as a prop. These buttons either conceal or disable actions unfit for the present state, alongside swapping slots to exhibit various aspects of the record.

PersonCrud Component

PersonCrud serves as a Vue component encapsulating a CrudPanel and populating view, edit, delete slots with pertinent components corresponding to the record type showcased. This necessitates a mere 20-line implementation per record type that warrants modification.

PersonEdit Module

Conceptually, PersonEdit embodies an editor tailored for a person record. Propelling Editable<Person> as a prop ensures reactivity and sustains the editing state. Each categorical record type calls for up to 20-30 lines of code to manifest simple types proficiently.

Full Source Codes

Listed below are the complete source codes spanning across several files:

Editable.ts

import { reactive } from 'vue'

// Remaining code same as original snippet...

builders.ts

import { defineStore } from 'pinia'
// Remaining code same as original snippet...

CrudPanel.vue

<script setup lang="ts">
// Remaining code same as original snippet...
</script>

<template>
  // Remaining code same as original snippet...
</template>

BuilderCrud.vue

<script setup lang="ts">
// Remaining code same as original snippet...
</script>

<template>
  // Remaining code same as original snippet...
</template>

BuilderEdit.vue

<script setup lang="ts">
// Remaining code same as original snippet...
</script>

<template>
  // Remaining code same as original snippet...
</template>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

How can you conceal an HTML element when the user is using an iOS device?

I need the code to determine if a user is using an iOS device and, if not, hide the HTML input type "Play" button. So, I'm uncertain whether my iOS detection is correct, the hiding of the "Play" button in the code, or both: <!DOCTYPE html> < ...

Learn how to easily set a radio button using Angular 4 and JavaScript

It seems like a simple task, but I am looking for a solution without using jQuery. I have the Id of a specific radio button control that I need to set. I tried the following code: let radiobutton = document.getElementById("Standard"); radiobutton.checke ...

The issue of Angular UI Bootstrap buttons not updating persists even after the removal of an

My Radio-bottoms are powered by an Array for a Multi-Choice answer setup. <div ng-repeat="option in options"> <div> <button type="button" style="min-width: 100px" class="btn btn-default" ng-model="question.answer" btn-radio="' ...

Using Jquery to Create a Dropdown Menu for Google Accounts

Is there a way to create a menu like Google's user account menu using jQuery or Javascript? You can find an example of this dropdown menu on the top right corner when logged in to Google. See the image below for reference. ...

How to efficiently remove duplicate items from multiple select dropdowns in Angular using ng-repeat?

Need help with dynamically assigning objects to select boxes in AngularJS. I have a scenario where I add multiple select boxes to a form, but each box should only display items that haven't been selected in other boxes. How can I achieve this functio ...

The function getServerSideProps does not return any value

I'm a beginner with Next.js and I'm currently using getServerSideProps to retrieve an array of objects. This array is fetched from a backend API by utilizing the page parameters as explained in the dynamic routes documentation: https://nextjs.org ...

Can we restrict type T to encompass subclasses of K, excluding K itself?

Can a generic type T be restricted to the subset of subtypes of type K, excluding K itself? I am attempting to define a type for inheritance-based mixin functions. An answer for the opposite case is provided in Question 32488309, and interestingly, this qu ...

Building a static website with the help of Express and making use of a public directory

It seems that I am facing a misunderstanding on how to solve this issue, and despite my efforts in finding an answer, the problem persists. In one of my static sites, the file structure is as follows: --node_modules --index.html --server.js --app.js The ...

Which is better for creating a gradual moving background: Javascript or CSS?

I'm attempting to discover how to create a background image that scrolls at a slower pace than the page contents. I'm currently unsure of how to achieve this effect. A great example of what I'm aiming for can be seen here Would this require ...

The technique of binding methods in React

When working with React.js, it's recommended to define your method binding in the constructor for better performance. Here's an example: constructor(props){ this.someFunction = this.someFunction.bind(this); } This approach is more efficient t ...

How can a JavaScript function be triggered by Flask without relying on any requests from the client-side?

I'm in the process of setting up a GUI server using Flask. The challenge I'm facing is integrating an API that triggers a function whenever there's a change in a specific Sqlite3 database. My goal is to dynamically update a table on the HTML ...

Angular ReactiveForms not receiving real-time updates on dynamic values

I'm using reactive forms in Angular and I have a FormArray that retrieves all the values except for product_total. <tbody formArrayName="products"> <tr *ngFor="let phone of productForms.controls; let i=index" [formGroupName]="i"> ...

Is it possible to configure Express.js to serve after being Webpacked?

I am currently in the process of setting up a system to transpile my Node server (specifically Express) similar to how I handle my client-side scripts, using webpack. The setup for the Express server is quite straightforward. I bring in some node_modules ...

What is the best way to obtain a unique dynamic id?

This is the unique identifier retrieved from the database. <input type="hidden" name="getID" value="<?php echo $row['ID']; ?>"> <input type="submit" name="getbtn" value="Get ID"> How can I fetch and display the specific dynami ...

preventing elements from moving unexpectedly as the navigation bar becomes fixed at the top of the page

I have a website that features a Bootstrap 3 navbar. This navbar is positioned 280px below a block div and becomes sticky at the top of the page when scrolled to that point. HTML (within < head > tags) <script> $(document).ready(function() { ...

Make sure to add 'normalize-scss' to your Vue CLI 3 configuration

Recently integrated the normalize-scss package into my brand new Vue project, but unfortunately, none of the styles seem to be taking effect... I've experimented with both approaches: @import 'normalize-scss' in my styles.scss import &apos ...

Customizing the initial search parameters in Vue InstantSearch: A step-by-step guide

I am utilizing the Vue components from Algolia to perform a search on my index. The search functionality is working correctly, but I am curious about setting the initial values for the refinement list upon page load. This is how I have set up the search: ...

Having difficulty with sending an AJAX GET request to connect to mongodb

I've been facing a challenging task of displaying data from a mongodb collection on the browser using nodejs and express. Here's the client-side call: document.onload= $(function (e) { var i = 0; $.ajax({ type: "GET", url: "http://localh ...

What is the order of reflection in dynamic classes - are they added to the beginning or

Just a general question here: If dynamic classes are added to an element (e.g. through a jQuery-ui add-on), and the element already has classes, does the dynamically added class get appended or prepended to the list of classes? The reason for my question ...

Unlocking the power of variables in Next.js inline sass styles

Is there a way to utilize SASS variables in inline styles? export default function (): JSX.Element { return ( <MainLayout title={title} robots={false}> <nav> <a href="href">Title</a> ...