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

@gabeklein/use-controller

v1.0.0-beta.0

Published

Easy to use state controllers using classes and hooks

Downloads

1

Readme

Quick Start

Install with preferred package manager

npm install --save use-controller

Import and use in your react apps.

import Controller, { use } from "use-controller";

Live demo

Try it for yourself. A demo project is in the /examples directory with a series of examples you can launch, browse through and modify.

git clone https://github.com/gabeklein/use-controller.git
cd use-controller
npm install
npm start

What does this do?

There are two ways to use hooked-controllers, with the use() hook or by extending your control-classes with Controller. Both ways behave pretty much the same, albeit with different features.

What they both do is pretty simple. They a class and turn it into ✨live-state ✨ for your components! But... what does that mean?

Here is a simple example.

class Count {
  number = 1
}

const Counter = () => {
  const state = use(Count);

  return (
    <Container>
      <Button 
        onClick={() => { 
          state.number -= 1
        }}>
        {"-"}
      </Button>
      <Box>{state.number}</Box>
      <Button 
        onClick={() => { 
          state.number += 1
        }}>
        {"+"}
      </Button>
    </Container>
  )
}

For sake of simplicity we are only using one value. You wouldn't typically do this.

Here we create a state controller, with the use() hook, passing in Counter. This will create a new instance of your class, then scan the resulting instance for values. On the returned state, use() will watch for changes in your values, compare updates, and if they're different... trigger a render! 🐰🎩 No need for setValue callbacks, and you can have as many as you want!

Why do it this way?

A quick comparison

Here is an example where we have multiple values to track. All the single-use variables can really add up, and they're not always easy to read or infer. It's also a heck-ton of var pollution.

const EmotionalState = () => {
  const [name, setName] = useState("John Doe");
  const [emotion, setEmotion] = useState("meh");
  const [reason, setReason] = useState("reasons.");

  return (
    <div>
      <div onClick = {() => {
        const name = prompt("What is your name?", name);
        setName(name);
      }}>
        My name is {name}.
      </div>
      <div>
        <span onClick = {() => {
          setName("sad");
        }}>
          I am currently {emotion}
        </span>
        <span onClick = {() => {
          setReason("hooks are still not hipster enough.")
        }}>
          because {reason}
        </span>
      </div>
    </div>
  )
}

This makes John Doe correspondingly sad, as you can see here.

How can we do better?

Use a class and the use() hook.

class EmotionalState {
  name = "John Doe",
  emotion = "whatever",
  reason = "I dunno man."
}

const HappyTown = () => {
  const state = use(EmotionalState);

  return (
    <div>
      <div onClick = {() => {
        state.name = prompt("What is your name?", "John Doe");
      }}>
        My name is {state.name}.
      </div>
      <div>
        <span onClick = {() => {
          state.emotion = "super happy"
        }}>
          I am currently {state.emotion} 
        </span>
        <span onClick = {() => {
          state.reason = "hooks are cooler than my cold-brew® coffee! 👓"
        }}>
          because {state.reason}
        </span>
      </div>
    </div>
  )
}

A bit better.

The hook's argument over there, its constructor, will only run at mount, and the returned object will then be bootstrapped into live state.

The component now updates when any of your declared values change. You can add as many values as you like, and they'll stay clean and relatively organized in your code.

And John Doe seems pretty mollified by this development now too.

Adding Methods

Similar to @action found in MobX.js, you can place methods amongst your watched values.

They'll access your live state and work generally as you'd expect, with regards to this keyword.

All methods are bound automatically (both arrow and proto functions), so you can pass them to callbacks and sub-components.

Let's circle back to our counter example. We can make a few big improvements.

class Count {
  current = 1

  increment = () => this.current++;
  decrement = () => this.current--;
}

const KitchenCounter = () => {
  const count = use(Count);

  return (
    <Row>
      <Button
        onClick={count.decrement}>
        {"-"}
      </Button>
      <Box>{count.current}</Box>
      <Button 
        onClick={count.incremement}>
        {"+"}
      </Button>
    </Row>
  )
}

Nice! Now all logic is out of the component. All is well with the world 👌

Special Entries

While standard practice is to take all methods (and bind them), all properties (and watch them), there are special circumstances to be aware of.

Properties

Arrays

  • if a property is an array, it will be forwarded to your components as a ReactiveArray which triggers a render on mutate.

_anything

  • if a key starts with an underscore it will not trigger a refresh when overwritten (or carry any overhead to do that). No special conversions will happen either. Think of those as "private" keys which don't interact with a component.

Defined post-constructor

  • important to notice that use() can only detect properties which exist (and are enumerable) at time of creation. If you create them after, effectively they're ignored.

Reserved methods (use will define them)

refresh(): void

  • requests a render without requiring that a value has changed.
  • Helpful when working with getters, async and random-number-generators.

export<T>(this: T): { [P in keyof T]: T[P] }

  • takes a snapshot of live state you can pass along without unintended side effects.
  • this will only output the values which were enumerable in the source object.

add(key: string, initial?: any): boolean

  • adds a new tracked value to the live-state.
  • this will return true if adding the key succeeded, false if did not. (because it exists)

Not really recommended after initializing, but hey.

LifeCycle Methods (use will call them)

didMount(): void

  • use() will call this while internally running useEffect(fn, [])

willUnmount(): void

  • use() will call this before it starts cleaning up

Let's take advantage of those lifestyle methods, shall we?

Here we'll spawn an interval on mount, which should also be cleaned up when the component unmounts.

class FunActivity {
  duration = 0;

  didMount(){
    this._stopWatch = 
      setInterval(() => {
        this.duration++;
      }, 1000)
  }

  willUnmount(){
      clearInterval(this._stopWatch)
  }
}
const PaintDrying = () => {
  const fun = use(FunActivity);

  return <div>Well there's {fun.duration} seconds I'm never getting back...</div>
}

The Controller superclass

While we get a lot from just use() and standard (or otherwise extended) classes, there's a few key benefits from actually extending Controller.

  • You can pass arguments to your constructor
  • Type inference and autocomplete are much better
  • extra lifecycles
  • Access to context features 👀
  • error boundaries

We can very easily translate our previous example and build up from there.

Import "Controller" as whatever you like from "use-controller" and extend your class with it.

import Control from "use-controller";

class FunActivity extends Control {
  duration = 0;

  didMount(){
    this._stopWatch = 
      setInterval(() => {
        this.duration++;
      }, 1000)
  }

  willUnmount(){
      clearInterval(this._stopWatch)
  }
}

Instead of the use() hook, we now use the all-new .use() hook!

const PaintDrying = () => {
  const fun = FunActivity.use();

  return (
    <div>
      <span>I've been staring for like </span>
      {fun.duration}
      <span> seconds and I'm starting to see what this is all about.</span>
    </div>
  )
}

This will behave exactly as our previous example, however more appreciatively.

Sidebar: use() also accepts an object

If you prefer to prepare your initial values, without anything fancy, you can do that too. This can be especially useful for situations with closures or HOC's.

Just don't give use() an object literal. It will get regenerated every render!

const values = {
  name: "Bob"
}   

const Component = () => {
  const state = use(values);

  return (
    <div
      onClick = {() => {
        state.name = "Robert"
      }}>
      Hello {state.name}
    </div>
  )
}

Keep in mind updated values are stored on the given object. This can be helpful but only in particular use cases.

🚧 More ideas are currently under construction, so stay tuned! 🏗

License

MIT license.