@solarfluxx/atlas
v2.3.0
Published
Atlas is an object-optimized state management library for React functional components.
Downloads
4
Readme
Description
Atlas is an object-optimized state management library for React functional components.
Installation
# with yarn:
yarn add @solarfluxx/atlas
# with npm:
npm i @solarfluxx/atlas
Guide
The basic premise revolves around "atoms" and "observers". Atoms are a stateful slice of data and observers subscribe to those atoms—updating when state changes.
Take a look the following example of a simple counter app:
import { atom, observe } from '@solarfluxx/atlas';
const count = atom(0); // Creates an atom with the initial value `0`.
function App() {
observe(); // Subscribes this component to watch for updates.
return (
<div>
<div>Count: { count.value }</div>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}
In this example, the atom
function returns an object that looks something like { value: number }
(more information on why primitives are wrapped below). Inside the App
component, a call to observe
is made. This call will subscribe the App
component, causing it to rerender when any of the atoms accessed inside of it are updated.
Note
To prevent unexpected behavior,observe
must be called in every component that accesses an atom and it must be called before any atoms are accessed.
Let's look at a more complex example:
import { atom, observe } from '@solarfluxx/atlas';
const users = atom([
{ name: 'John', email: '[email protected]' },
{ name: 'Ryan', email: '[email protected]' },
]);
function App() {
observe();
return (
<div>
{ users.map(user => <User user={user} />) }
</div>
);
}
function User({ user }) {
observe();
return (
<div>
<input value={user.name} onChange={(event) => (user.name = event.currentTarget.value)} />
<input value={user.email} onChange={(event) => (user.email = event.currentTarget.value)} />
</div>
);
}
In this example, App
will rerender when the users
array changes but not when a user's name or email changes. This is because the App
component does not read name
or email
. However, the User
component does and will rerender in those cases. Why? Because even though { name: 'John', email: '[email protected]' }
is not directly wrapped with atom()
, objects are atomized recursively, so all of the array elements are atomized as well.
Understanding atom
So far I've been talking about atoms as if they were a unique object, and they are under the hood, but practically they mimic the original data structure. Take a look at the following examples:
const user = atom({ name: 'Sam', email: '[email protected]' });
// Accessing properties is as expected.
console.log(user.name, user.email); // 'Sam [email protected]'
const planets = atom([ { name: 'Earth', type: 'Gas' }, { name: 'Saturn', type: 'Gas' } ]);
// Indexing is as expected.
planets[0].type = 'Terrestrial'; // Change Earth's type to Terrestrial.
// Methods also work.
console.log(planets.map(planet => planet.name).join(', ')); // 'Earth, Saturn'
The only exception to the mimic rule is when directly atomizing primitives. Take a look:
const count = atom(5);
// The value is accessed via `.value` instead of directly.
count.value += 10;
console.log(count.value); // 15
However, when a primitive is inside of an object this does not happen:
const counter = atom({ count: 10 });
counter.count += 10; // No use of `.value` here.
console.log(counter.count); // 20
In effect, atom(PRIMITIVE)
is changed to atom({ value: PRIMITIVE })
. The reason for this is because the underlying technology Atlas uses (proxies) only work on objects. This requires primitives to be wrapped in an object to work correctly.
Understanding observe
observe
has two overloads:
observe(): void;
observe(observer: () => void): () => void;
When called without a parameter, it will subscribe a React component using React hooks:
function App() {
observe();
// ...
}
However, when called with an observer it will subscribe that observer (like an event listener) to the atoms accessed inside of it. This alternative can be used to listen to atom's outside of a React component:
const count = atom(0);
// Print the value of `count` when it changes:
observe(() => {
console.log(count.value);
});
Additionally, when passed an observer it will return an unsubscribe function:
const count = atom(0);
// Print the value of `count` when it changes:
const unsubscribe = observe(() => {
console.log(count.value);
});
// ...
unsubscribe();
Understanding unobserve
observe
has a sister method: unobserve
. As the name suggests, it does the opposite of observe
. When called without a parameter, it will unsubscribe the current React component:
function MyComponent() {
unobserve(); // Stops this component from subscribing to atoms.
// Safe to access atom's without triggering rerenders.
// ...
}
Like observe
, unobserve
can accept a callback function. Unlike observe
however, this callback will not subscribe to the atom's accessed inside of it.
unobserve(() => {
// Safe to access atom's without triggering observer updates.
});
This callback function can return a value too:
const count = atom(0);
// ...
const countSnapshot = unobserve(() => count.value);
The observe
and unobserve
functions work together to create and exit reactive scopes. Here's a very strange but valid example of observe
and unobserve
usage:
observe(() => {
// Accessing atoms here will subscribe to them.
unobserve(() => {
// Accessing atoms here does nothing special.
observe(() => {
// Once again, accessing atoms here will subscribe to them.
});
});
});
Please note that I am not suggesting you use them this way. This can create behavior that is hard to read and predict. This example is simply to show you how observe
and unobserve
relate to each other.
Here is example that highlights why you need to be cautious when using these two:
const count = atom(0);
const count2 = atom(0);
observe(() => {
// This code will run when `count` changes.
console.log('count', count.value);
unobserve(() => {
// Therefore, code here will run too since `unobserve` immediately invokes its callback.
// The result of this means that the following code will
// run when `count` changes but will NOT when `count2` changes.
console.log('count2', count2.value);
});
});
As you can see, the logic here feels quite strange. On the contrary though, I'd like you to look at this example; it has the same context structure but is more sensible:
const count = atom(0);
const count2 = atom(0);
// This component will rerender when `count` changes but not when `count2` does.
function App() {
observe();
const count2Snapshot = unobserve(() => count2.value);
return (
<div>
{ count.value }
{ count2Snapshot }
</div>
);
}
In this example, count2
is read without subscribing App
to it. This has essentially the same logic has the previous example (other than unobserve
returning a value) but feels more readable, to me anyway. The takeaway point here is: use unobserve
responsibly.
Understanding isAtom
What if you want to check if an object is an atom? Since the atom mimics the original object, ==
, ===
, instanceof
or other comparison operators won't work. This is where isAtom
comes into play.
const count1 = 0;
const count2 = atom(0);
console.log(isAtom(count1)); // false
console.log(isAtom(count2)); // true
Understanding distillAtom
The distillAtom
function will recursively extract a pure, unatomized, value from an atom.
const user = atom({
id: 14,
name: 'Ted',
friends: [
{ id: 19, name: 'Jeremy' },
{ id: 8, name: 'Sam' }
]
});
console.log(user); // Proxy(Object) { id: 14, name: 'Ted', friends: Proxy(Array) { 0: Proxy(Object) { id: 19, name: 'Jeremy' }, ... } }
const distilledUser = distillAtom(user);
console.log(distilledUser); // { id: 14, name: 'Ted', friends: [ { id: 19, name: 'Jeremy' }, ... ] }
Understanding focusAtom
The focusAtom
function creates a reference to an atom property. The source and reference are linked together; updating one will update the other:
const state = atom({ count: 0 });
const count = focusAtom(() => state.count);
state.count += 5; // Updates `count.value`.
count.value += 8; // Updates `state.count`.
console.log(state); // { count: 13 }
console.log(count); // { value: 13 }
Understanding whenAtom
The whenAtom
function invokes a callback when the target object is atomized for the first time. This gives you a means to execute code after an object is initialized as an atom. This was primarily made for class constructors which execute before atomization preventing observers and other atom dependent code from working.
class User {
public fullName!: string;
constructor(public firstName: string, public lastName: string) {
whenAtom(this, function() {
observe(() => {
this.fullName = `${this.firstName} ${this.lastName}`;
});
});
}
}
const user = atom(new User('John', 'Smith')); // Instantiate User and atomize it.
console.log(user.fullName); // 'John Smith'
user.firstName = 'Jack';
console.log(user.fullName); // 'Jack Smith'