@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 runninguseEffect(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.