DEV Community

Omri Luz
Omri Luz

Posted on

Implementing a Custom Reactive Library in Vanilla JS

Implementing a Custom Reactive Library in Vanilla JS: A Comprehensive Guide

JavaScript is celebrated for its versatility and capability to handle asynchronous operations and events. The reactive programming paradigm, which emphasizes the propagation of changes and the use of observable data streams, fits seamlessly into the JavaScript ecosystem. This article aims to equip you with the knowledge necessary to implement a custom reactive library in Vanilla JS, exploring its historical context, features, performance considerations, and edge cases in great detail.

Historical and Technical Context

Reactive programming has its roots in functional programming paradigms, which prioritize immutability and pure functions. Introduced prominently in the late 2000s through libraries like RxJS, the concept of a reactive paradigm became invaluable in managing complex data flows and asynchronous events, particularly in Single Page Applications (SPAs). Developers quickly embraced the idea of using "observables"—data sources that can emit values over time, which observers can subscribe to.

In contrast to established frameworks like React and Vue, creating a custom reactive library allows developers to gain deeper insights into reactivity mechanics while fine-tuning a toolkit specifically tailored to their domain or preferences.

Key Concepts in Reactive Programming

  • Observables: Objects that enable multiple components to subscribe to a single data source.
  • Observers: Consumers of observables that are notified when data changes.
  • Operators: Functions that allow transformations and combinations of observables.
  • Subscriptions: Mechanisms through which observers can opt to receive updates from observables.

Building the Foundation: A Minimal Reactive Library

Step 1: Create Observable Class

At the core of any reactive library is the Observable class. It should allow subscribers to register for updates.

class Observable {
    constructor() {
        this.observers = new Set();
    }

    subscribe(observer) {
        this.observers.add(observer);
        return () => this.observers.delete(observer);
    }

    notify(data) {
        this.observers.forEach(observer => observer(data));
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The Constructor (constructor): Initializes an empty Set to hold observers.
  • The Subscribe Method (subscribe): Adds an observer, returning a function to unsubscribe.
  • The Notify Method (notify): Notifies all subscribers with new data.

Step 2: Implementing Reactive Properties

We can enhance our Observable to allow tracking changes to individual properties.

class ReactiveProperty extends Observable {
    constructor(value) {
        super();
        this._value = value;
    }

    get value() {
        return this._value;
    }

    set value(newValue) {
        this._value = newValue;
        this.notify(newValue);
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Property Accessors (get and set): The user can get or set values while calling notify to alert subscribers.

Step 3: Creating a simple Data Store

Next, we combine reactive properties to form a simple data store.

class Store {
    constructor(initialData) {
        Object.keys(initialData).forEach(key => {
            this[key] = new ReactiveProperty(initialData[key]);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage:

const store = new Store({ count: 0 });

store.count.subscribe(value => {
    console.log(`Count updated: ${value}`);
});
store.count.value = 1; // logs "Count updated: 1"
Enter fullscreen mode Exit fullscreen mode

Advanced Implementation Techniques

Custom Operators

Building operators allows us to manipulate data streams in a declarative way. Let’s implement map, a commonly used operator.

class MappedObservable extends Observable {
    constructor(source, transform) {
        super();
        this.source = source;
        this.transform = transform;

        this.source.subscribe(data => {
            this.notify(this.transform(data));
        });
    }
}

// Usage:
const doubledCount = new MappedObservable(store.count, x => x * 2);
doubledCount.subscribe(value => console.log(value));
store.count.value = 5; // logs 10, the doubled value
Enter fullscreen mode Exit fullscreen mode

Combining Observables

Observable composition allows combining data streams. For the following example, we will create a method that merges two observables:

class MergedObservable extends Observable {
    constructor(sourceA, sourceB) {
        super();
        let stateA, stateB;

        sourceA.subscribe(data => {
            stateA = data;
            this.notify({ stateA, stateB });
        });

        sourceB.subscribe(data => {
            stateB = data;
            this.notify({ stateA, stateB });
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage:

const firstName = new ReactiveProperty('John');
const lastName = new ReactiveProperty('Doe');

const fullName = new MergedObservable(firstName, lastName);
fullName.subscribe(({ stateA, stateB }) => {
    console.log(`Full Name: ${stateA} ${stateB}`);
});

firstName.value = 'Jane'; // logs "Full Name: Jane Doe"
Enter fullscreen mode Exit fullscreen mode

Edge Cases and Pitfalls

Circular Dependencies

Observables can inadvertently create circular dependencies, affecting performance and leading to infinite loops. Use a dependency graph or a similar data structure to manage state updates effectively.

Memory Leaks

For long-lived components or applications, ensure proper management of subscriptions. This includes cleaning up subscriptions in scenarios such as component unmounting.

Error Handling

In reactive programming, error handling can be cumbersome. Creating a robust error-handling strategy by providing defaults for observable errors can mitigate issues.

class SafeObservable extends Observable {
    constructor(source) {
        super();
        source.subscribe(data => {
            try {
                this.notify(data);
            } catch (error) {
                this.notify({ error: error.message });
            }
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Real-World Use Cases

  1. Form Handling: Managing form state with automatic updates and validation checks.
  2. Data Binding in SPAs: Observables allow for automatic reactivity when data changes without manual DOM manipulation.
  3. State Management for Large Applications: Libraries refactored using reactive principles lead to smoother state transitions and modularization.

Performance Considerations and Optimization Strategies

To minimize performance impacts:

  1. Batch Notifications: Instead of notifying each time a change occurs, batch multiple updates into a single notification.
  2. Throttle/Debounce: Control how often observables emit data, particularly with user inputs.
  3. Lazy Loading: Load only the necessary data, using observables to fetch data on-demand.

Debugging Techniques

  1. Logging Values: Simple console logs can aid in understanding the flow of data but can become verbose. Implement a logging mechanism.
  2. Monitoring Subscriptions: Track active subscriptions, which can help in identifying memory leaks and ensuring that observables are not erroneously firing when they shouldn’t.

Conclusion

Creating a custom reactive library in Vanilla JS provides a refreshing perspective on JavaScript's power and versatility. By understanding the foundational principles of reactive programming and implementing your library, you can bring sophisticated data handling capabilities to your applications.

Embrace the complexity of managing state and events through a reactive approach while tailoring it to fit your specific requirements and scenarios. Remember, while building your solution offers insight into these advanced concepts, leveraging existing libraries like RxJS or even frameworks like React can sometimes be worthwhile for production applications.

Further Reading & Resources

References

This guide provides a framework upon which you can build and expand your reactive programming knowledge and implementation strategies in Vanilla JS. Happy coding!

Top comments (0)