npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

react-domain-hooks

v1.1.0

Published

Use React hooks with a domain object. This allows you to separate view logic from interaction logic.

Downloads

7

Readme

React Domain Hooks

Use React hooks with a domain object. This allows you to separate view logic from interaction logic.

Let's demonstrate how it works.

First we'll create an abstract interaction model that looks like this:

export default class GalleryInteraction {
  constructor() {
    this._images = []
    this._selectedImage = 0
  }

  // command
  nextImage() {
    if (this._selectedImage < this.images.length - 1) {
      this._selectedImage++
    }
  }

  // command
  previousImage() {
    if (this._selectedImage > 0) {
      this._selectedImage--
    }
  }

  // command
  addImage(image) {
    this._images.push(image)
  }

  // query
  currentImage() {
    return this._images[this._selectedImage]
  }
}

Notice how the model is designed as commands and queries as per the comments. This is a practice known as CQRS which stands for Command Query Responsibility Segregation. Building code using the CQRS principle is common amongst DDD (Domain Driven Design) practitioners as it creates highly decoupled code and ultimately reduces complexity.

This library capitalizes on the CQRS principle and gives you three options to bind the React hooks to your interaction model. We'll look into binding options later but for now let's look at how you use the model above in your React component using the useDomain hook.

import React, {useEffect} from 'react'
import useDomain from '../helpers/useDomain'

// ultra thin component with UI logic only. Note it's up to you to inject the model here. You woudl do so in your composite root/entry point of your app.
export default function GalleryComponent({model}) {
  
  // notice how the userDomain method takes a model and returns an array of queries, commands and history
  const [queries, commands, history] = useDomain(model)

  return (
    <>
      <h5>Component</h5>
      <p>Image = [{queries.currentImage()}]</p> // queries are used to read from the model
      <button onClick={commands.previousImage}>Previous Image</button> // commands are used to act on the model
      <button onClick={commands.nextImage}>Next Image</button> // if a command changes the model, the useDomain hook will trigger a re-render
    </>
  )
}

Note that if the values inside the domain object do not change, the useDomain hook will not re-render the component. This is achieved by using setState with a hash of the model object (See binding options below). You can see this in action by trying to repeatedly click the "Previous Image" button. The previousImage command in the GalleryInteraction domain model will stop changing the currentImage when it gets to 0, and since the values inside the domain model are no longer changing, the hash method on the model ensures that the React component will not re-render. Sweet!

You can also add as many useEffect methods as you like as follows:

  // ...

  // You can have effet react to specific queries
  useEffect(() => {
    console.log('effect currentImage()')
    // since you have commands, you no longer need to dispatch events with reducers.
    // You can work with the interaction domain object directly and handle all complexities there
    // commands.doSomething(...)
  }, [queries.currentImage()]) 

  useEffect(() => {
    console.log('effect images')
    // command.doSomethingElse(...)
  }, [queries._images]) // you can also access member variables directly since the command will trigger a rerender, though it's advised you don't do this as it couples your view to your interaction model. It could be useful for debugging. 
  
  // ...

Finally, you also have access to a history object (thanks to @TillaTheHun0 for this addition):

  // ...
  
  <h5>History</h5>
  <ul>
    {history.map(cur => (<li key={cur.id}>{cur.id + ' ' + cur.command}</li>))}
  </ul>
  
  // ...

The history object keeps track of all commands that have been fired, which can be useful for testing, tracking, creating undo buffers, or even dumping a re-playable sequence of events for debugging purposes.

Binding Options

There are three ways to bind your model to the useDomain hook, each of which comes with its own pros and cons, so it depends on your architecture which one you'd like to use.

Option 1: Using Decorators

  1. import the @command, @query, and @hashable decorators from this module
  2. Decorate the class of the model with the @hashable decorator. This adds a hash method to the class (See hashing below)
  3. Decorate your command methods with the @command decorator and your query methods with the @query decorator
  4. Configure your project to use decorators. See the Babel instructions below
import {command, query, hashable} from 'react-domain-hooks'

@hashable
class GalleryInteraction {

  @live
  property = 'foo'

  @command
  nextImage() {
    // implementation omitted for brevity
  }

  @command
  previousImage() {
    // implementation omitted for brevity
  }

  @command
  addImage(image) {
    // implementation omitted for brevity
  }

  @query
  currentImage() {
    // implementation omitted for brevity
  }
}

Pros: (1) Has the most readable syntax of all the options and (2) requires the least boiler plate code

Cons: (1) You are polluting the interaction domain abstraction and (2) you have to configure Babel

The @live property decorator

With this decorator you can annotate a class property to make it "live", which means changes to the property will also trigger a component re-render. The decorator is syntax sugar for creating a getter and setter for your property and marking them as commands and queries.

Option 2: Using a Decoupled Explicit Syntax

which converts the raw model objects into an object that contains commands, queries and a hash method. The commands and queries are delegated to the underlying model and the hash method is used to know when the model has changed (see below for more info)

  1. import the toCQRSWithHash method
  2. explicitly provide the method with the list of command and queries
  3. export the returned object as your domain model
  4. optionally put this code in your composite root or container if you're using dependency injection
import toCQRSWithHash from 'toCQRSWithHash'
import GalleryInteraction from './GalleryInteraction'

// NOTE the GalleryInteraction class would not have any decoratos and would be POJO (Plain Old Javascript Object)
const model = new GalleryInteraction()
const galleryInteraction = toCQRSWithHash({
  model,
  commands: [
    model.nextImage,
    model.previousImage,
    model.addImage,
  ],
  queries: [
    model.currentImage,
  ]
})

export {galleryInteraction}

Pros: (1) Keeps the interaction domain clean and (2) does not require any special build tooling

Cons: Requires boilerplate code every time you want to add a command/query to your domain model

Option 3: Using a Localized Explicit Syntax

This is a combination of option 1 and 2 where you are explicit but you do so in the same file as the class making slightly more bearable.

import toCQRSWithHash from 'toCQRSWithHash'

class GalleryInteraction {

  nextImage() {
    // implementation omitted for brevity
  }

  previousImage() {
    // implementation omitted for brevity
  }

  addImage(image) {
    // implementation omitted for brevity
  }

  currentImage() {
    // implementation omitted for brevity
  }
}

const model = new GalleryInteraction()
const galleryInteraction = toCQRSWithHash({
  model,
  commands: [
    model.nextImage,
    model.previousImage,
    model.addImage,
  ],
  queries: [
    model.currentImage,
  ]
})

export {galleryInteraction}

About Hashing

Both the decorator based option as well as the explicit options adds a hash method to the model, which computes a unique hash value for a given object based on its values. That is, the same values for a given instance will always return the same hash. This allows the useDomain React hook to only re-render when necessary.

Babel Decorators Configuration

For this library, the following steps were taken. Your mileage may vary.

These npm modules were added:

  "@babel/plugin-proposal-class-properties"
  "@babel/plugin-syntax-decorators"

They were added to the babel.config.js file:

  plugins: [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "legacy": true }],
  ]

Why do this?

Having an abstract interaction object has many advantages:

  • It can be used by any view layer like React or Vue, or a speech UI, or even a camera gesture UI.
  • The abstraction makes it easier to reason about the interaction independently of its presentation
  • Changes can be made to the interaction logic without touching the interface components
  • Allows the practice of the Separation of Concerns and the Single Responsibility Principles
  • Makes it easy to perform behaviour driven development and modeling by example