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

@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'