Click here to Skip to main content
15,901,205 members
Articles / Programming Languages / Javascript
Tip/Trick

State Validation with JavaScript

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
6 May 2024CPOL4 min read 1.3K   1   1
Overview and implementation of State Validation in Javascript
We will implement the observer pattern to create a means of State Validation in Javascript.

Introduction

State Validation with Javascript. So you can know your data and objects are valid in Javascript.

Background

I had an amazing conversation on the Frontend Masters discord server today. We were discussing many different topics, but the portion that was stuck on my mind was when we were discussing why I dislike server-side Javascript.

It is no secret that you can’t know the exact state of objects or data at all times in Javascript and that “fun” bugs crop up due to this.

That is when DTOs / POJOs were brought up. A DTO is a Data Transfer Object in C# and POJO is Plain Old Java Object. The equivalent of this in Javascript is just called Object Validation.

It never occurred to me that I could and should validate objects and data in Javascript at a core level. I do this all of the time in C#, so why didn’t I do this in Javascript?

This nagged at my very core as I was laying down for a nap (I’m old, get over it). As I was laying down, it just kept coming up in my mind, how would I do this in a clean and efficient manner without using zod or valibot?

I fell asleep with my mind aglow with whirling, transient nodes of thought careening through a cosmic vapor of invention. When I woke up, it hit me! If I want to know the exact state of an object at all times, I can use the Observer Pattern!

I can extend it to also check for validations when moving to a new state, it can fail or succeed. If it fails, no updates can take place, if it passes, then updates are allowed to go through.

It was genius, eloquent and simple!

Using the code

JavaScript
export class Observable {
    constructor(config = {}) {
        const { initialState = {}, validators = [], onSuccess = () => {}, onFailure = () => {} } = config;

        this.state = { ...initialState };
        this.subscribers = [];
        this.validators = validators;
        this.onSuccess = onSuccess;
        this.onFailure = onFailure;
    }

    subscribe(callback) {
        this.subscribers.push(callback);
        return this; // Chainable
    }

    addValidator(validator) {
        this.validators.push(validator);
        return this; // Chainable
    }

    validate(newState) {
        for (const validator of this.validators) {
            const result = validator(this.state, newState);
            if (result !== true) {
                return result || "Invalid state update.";
            }
        }
        return true;
    }

    setState(newState) {
        const validationResult = this.validate(newState);
        if (validationResult === true) {
            this.state = { ...this.state, ...newState };
            this.notify();
            this.onSuccess(this.state); // Success hook
        } else {
            console.log("State update failed:", validationResult);
            this.onFailure(validationResult); // Failure hook
        }
        return this; // Allow chaining
    }

    notify() {
        for (const cb of this.subscribers) {
            cb(this.state);
        }
    }

    getState() {
        return this.state;
    }
}

Let’s break this class down piece by piece.

  • constructor: This is the method that’s called when you create a new instance of the Observable class. It sets up the initial state of the Observable, including its initial state, any validation functions, and what to do when state changes are successful or fail.
  • subscribe(callback): This method allows other parts of your code to subscribe to changes in this Observable’s state. When the state changes, all subscribed callbacks are called with the new state.
  • addValidator(validator): This method allows you to add validation functions that can check whether a proposed state change is valid.
  • validate(newState): This method runs all the validator functions on a proposed new state. If any validator returns a value other than true, it returns that value as an error message. Otherwise, it returns true to indicate that the new state is valid.
  • setState(newState): This method attempts to update the state of the Observable. It first validates the new state. If the new state is valid, it updates the state, notifies all subscribers of the change, and calls the onSuccess callback. If the new state is not valid, it logs an error message and calls the onFailure callback.
  • notify(): This method calls all subscribed callbacks with the current state. It’s called whenever the state changes successfully.
  • getState(): This method returns the current state of the Observable.

This class is chainable, meaning you can link multiple method calls together like this:

JavaScript
observable.subscribe(callback).addValidator(validator).setState(newState);

This is because the subscribe, addValidator, and setState methods return this, which is the instance of the Observable.

For simplicity, I am only going to show a couple validators, but you can make them according to your needs.

JavaScript
export const nonEmptyNameValidator = (oldState, newState) => {
    if ("name" in newState && newState.name.trim() === "") {
        return "Name cannot be empty.";
    }
    return true;
};

export const ageRangeValidator = (oldState, newState) => {
    if ("age" in newState && (newState.age < 0 || newState.age > 150)) {
        return "Age must be between 0 and 150.";
    }
    return true;
};

These are pretty straightforward and self documenting Javascript functions. So I don’t think I need to go into any detail here.

Let’s move on to the index.js file.

JavaScript
import { nonEmptyNameValidator, ageRangeValidator } from './Validators.js';
import { Observable } from './Observable.js';

const config = {
    initialState: { name: "John", age: 30 },
    validators: [nonEmptyNameValidator, ageRangeValidator],
    onSuccess: (newState) => {
        console.log("State successfully updated:", newState);
    },
    onFailure: (error) => {
        console.log("State update failed due to:", error);
    }
};

const myObject = new Observable(config);

myObject.subscribe((newState) => {
    console.log("Subscriber: State updated to:", newState);
});

myObject.setState({ name: "Jane" }).setState({ age: 13 }).setState({ name: "" }).setState({ age: 251 });

console.log(myObject);

We import our Validators.js with the two validations we have and we also import Observable from our Observable class.

We create our config, which is the object we want to keep close watch on. Define the initial state, the validators we want to use with it, what happens on success and failure.

We instantiate our Observable class and pass our config object as a parameter for the constructor. We subscribe to the observer and when we want to update the object, we set the new state.

Image 1

What happens when the state is updated and it fails? The changes aren’t stored.

Points of Interest

What was the point of this story? Well, that is for you to decide. I just wanted to share a source of inspiration I had based on a conversation.

One final tidbit, there is one final thing that bugs me with this implementation, It isn’t as clean with the index.js as I would like. I think the onSuccess and onFailure should probably be optional parameters and have it default to logging to the console. That way, a dev doesn’t have to arduously write out onSuccess and onFailure for every object. Additionally, You should be able to pass in an array of objects like you can with the validators for the Observable constructor.

Again, the subscribe call should be inherent to the constructor, there is no reason why you would need to have to write that out unless it is for DB updates.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Seasoned Software Consultant & Senior Salesforce Developer | Expert in Salesforce Development | Proficient in Full-Stack Solutions with .NET Framework | Agile Methodology Enthusiast | Salesforce Certified Administrator

Comments and Discussions

 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA9-May-24 19:12
professionalȘtefan-Mihai MOGA9-May-24 19:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.