Skip to content

Commit

Permalink
Merge pull request #25 from saasquatch/v1.2
Browse files Browse the repository at this point in the history
V1.2 - use bunshi
  • Loading branch information
loganvolkers authored Sep 21, 2023
2 parents 442f6fc + eb44e5a commit f814313
Show file tree
Hide file tree
Showing 51 changed files with 11,032 additions and 48,700 deletions.
30 changes: 30 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
"name": "Node.js & TypeScript",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye",
"customizations": {
"vscode": {
"extensions": [
"ZixuanChen.vitest-explorer",
"esbenp.prettier-vscode"
]
}
}

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",

// Configure tool-specific properties.
// "customizations": {},

// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
10 changes: 9 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ jobs:
uses: bahmutov/npm-install@v1

- name: Test
run: npm test -- --ci --coverage --maxWorkers=2
run: npm test

- name: Build
run: npm run build

size:
needs: build
name: Size limit check

steps:
- name: Build
run: npm run size
12 changes: 0 additions & 12 deletions .github/workflows/size.yml

This file was deleted.

8 changes: 0 additions & 8 deletions .storybook/main.js

This file was deleted.

5 changes: 0 additions & 5 deletions .storybook/preview.js

This file was deleted.

17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.2.0] - 2023-09-21

Version 1.2.0 of `jotai-molecules` will be the final release in the 1.x branch (except for security patches).

Version 2 of `jotai-molecules` has been renamed to `bunshi` and is mostly backwards compatible with the API,
but requires changes to imports and uses different internals. Because of this, we release 1.2 as a shim to
simplify the migration process.

This version should be able to interoperate with `bunshi`, so you can gradually move your imports over.

### Changed
- Internally uses `bunshi`, the next version of jotai-molecules
- Removed peer dependency on `jotai`

## [1.1.1] - 2023-04-20

### Changed
Expand Down Expand Up @@ -40,7 +54,8 @@ Initial release of `jotai-molecules`
- `createScope` for creating a scope for molecules
- `ScopeProvider` a React component for providing scope to the tree

[unreleased]: https://github.com/saasquatch/jotai-molecules/compare/v1.1.1...HEAD
[unreleased]: https://github.com/saasquatch/jotai-molecules/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/saasquatch/jotai-molecules/releases/tag/v1.2.0
[1.1.1]: https://github.com/saasquatch/jotai-molecules/releases/tag/v1.1.1
[1.1.0]: https://github.com/saasquatch/jotai-molecules/releases/tag/v1.1.0
[1.0.3]: https://github.com/saasquatch/jotai-molecules/releases/tag/v1.0.3
Expand Down
233 changes: 14 additions & 219 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
# Jotai Molecules
> `jotai-molecules` has been renamed to [bunshi](https://www.npmjs.com/package/bunshi).
>
> All new users of this module should use [bunshi](https://www.npmjs.com/package/bunshi) instead. Bunshi
> adds support for vue, react and vanilla javascript. Development
> and features additions will continue under the [bunshi](https://www.npmjs.com/package/bunshi) package.
>
> Molecules in `jotai-molecules` version 1.2.0 are compatible with
> molecules from `bunshi`, and they can interoperated and depend on each
> others. Version 1.2.0 of `jotai-molecules` is just a wrapper for
> `bunshi`.
>
> Documentation: [bunshi.org](https://www.bunshi.org)
A tiny, fast, dependency-free 1.18kb library for creating [jotai](https://jotai.org/) atoms in a way that lets you lift state up, or push state down. See [Motivation](#motivation) for more details on why we created this library.

Expand All @@ -10,6 +21,8 @@ This module is published on NPM as `jotai-molecules`
npm i jotai-molecules
```

> Note: Prefer [bunshi](https://www.npmjs.com/package/bunshi) to `jotai-molecules`
## Usage

Molecules are a set of atoms that can be easily scoped globally or per context.
Expand Down Expand Up @@ -77,224 +90,6 @@ const UserComponent = () => {
};
```

## Differences from Jotai

Molecules are similar to and inspired by [jotai](https://jotai.org/) atoms, but with a few important differences:

- **Molecules can't be async**, but atoms can be.
- **Molecule scopes can be interconnected**, but atom scopes are "separate universes".
- **Molecules can depend on molecules AND scope**, but atoms only depend on other atoms.
- **Molecules are read-only**, but atoms can be writable.

## Motivation

In jotai, it is easy to do global state... but jotai is [much more powerful](https://jotai.org/docs/guides/atoms-in-atom) when used for more than just global state!

The problem is the atom lifecycle, because we need to follow the mantras of jotai:

- Creating an atom creates state
- You must [memoize your atom creation](https://jotai.org/docs/basics/primitives#atom) (or use a global consts: `export const myAtom = atom("");`)

The challenge with jotai is getting a reference to an atom outside of a component/hook. It is hard to do [recursive atoms](https://github.com/pmndrs/jotai/issues/783) or [scoped atoms](https://github.com/pmndrs/jotai/discussions/682). Jotai molecules fixes this:

- You can [lift state up](https://reactjs.org/docs/lifting-state-up.html), by changing your molecule definitions
- When you lift state up, or push state down, you don't need to refactor your component

Let's examine this idea by looking at an example `Counter` component.

The most important function in these examples is the `createAtom` function, it creates all the state:

```ts
const createAtom = () => atom(0);
```

Here is an example of the two synchronized `Counter` components using **global state**.

```tsx
import { atom, useAtom } from "jotai";

const createAtom = () => atom(0);
const countAtom = createAtom();

const Counter = () => {
const [count, setCount] = useAtom(countAtom);
return (
<div>
count: {count} <button onClick={() => setCount((c) => c + 1)}>+1</button>
</div>
);
};

export const App = () => (
<>
<Counter /> <Counter />
</>
);
```

Here is the same component with **Component State**. Notice the use of `useMemo`:

```tsx
import { atom, useAtom } from "jotai";

const createAtom = () => atom(0);

const Counter = () => {
const countAtom = useMemo(createAtom, []);
const [count, setCount] = useAtom(countAtom);
return (
<div>
count: {count} <button onClick={() => setCount((c) => c + 1)}>+1</button>
</div>
);
};

export const App = () => (
<>
<Counter /> <Counter />
</>
);
```

Here is a component with **context-based state**:

```tsx
import { atom, useAtom } from "jotai";

const createAtom = () => atom(0);

const CountAtomContext = React.createContext(createAtom());
const useCountAtom = () => useContext(CountAtomContext);
const CountAtomScopeProvider = ({ children }) => {
const countAtom = useMemo(createAtom, []);
return (
<CountAtomContext.Provider value={countAtom}>
{children}
</CountAtomContext.Provider>
);
};

const Counter = () => {
const countAtom = useCountAtom();
const [count, setCount] = useAtom(countAtom);
return (
<div>
count: {count} <button onClick={() => setCount((c) => c + 1)}>+1</button>
</div>
);
};

export const App = () => (
<CountAtomScopeProvider>
<Counter />
<CountAtomScopeProvider>
<Counter />
<Counter />
</CountAtomScopeProvider>
</CountAtomScopeProvider>
);
```

Or, to make that context scoped based off a **scoped context**

```tsx
import { atom, useAtom } from "jotai";

const createAtom = (userId: string) =>
atom(userId === "bob@example.com" ? 0 : 1);

const CountAtomContext = React.createContext(createAtom());
const useCountAtom = () => useContext(CountAtomContext);
const CountAtomScopeProvider = ({ children, userId }) => {
// Create a new atom for every user Id
const countAtom = useMemo(() => createAtom(userId), [userId]);
return (
<CountAtomContext.Provider value={countAtom}>
{children}
</CountAtomContext.Provider>
);
};

const Counter = () => {
const countAtom = useCountAtom();
const [count, setCount] = useAtom(countAtom);
return (
<div>
count: {count} <button onClick={() => setCount((c) => c + 1)}>+1</button>
</div>
);
};

export const App = () => (
<CountAtomScopeProvider userId="bob@example.com">
<Counter />
<Counter />
<CountAtomScopeProvider userId="tom@example.com">
<Counter />
<Counter />
</CountAtomScopeProvider>
</CountAtomScopeProvider>
);
```

For all of these examples;

- to lift state up, or push state down, we had to refactor `<Counter>`
- the more specific we want the scope of our state, the more boilerplate is required

With molecules, you can change how atoms are created **without having to refactor your components**.

Here is an example of the `<Counter>` component with **global state**:

```tsx
import { atom, useAtom } from "jotai";
import { molecule, useMolecule } from "jotai-molecules";

const countMolecule = molecule(() => atom(0));

const Counter = () => {
const countAtom = useMolecule(countMolecule);
const [count, setCount] = useAtom(countAtom);
return (
<div>
count: {count} <button onClick={() => setCount((c) => c + 1)}>+1</button>
</div>
);
};

export const App = () => <Counter />;
```

For a scoped molecule, change the molecule definition and don't refactor the component.
Now, you can follow the React best practice of [Lifting State Up](https://reactjs.org/docs/lifting-state-up.html) by adding a molecule, and then lifting the state up, or pushing the state down.

Here is an example of the `<Counter>` component with **scoped context state**:

```tsx
import { atom, useAtom } from "jotai";
import {
molecule,
useMolecule,
createScope,
ScopeProvider,
} from "jotai-molecules";

const UserScope = createScope(undefined);
const countMolecule = molecule((getMol, getScope) => {
const userId = getScope(UserScope);
console.log("Creating a new atom for", userId);
return atom(0);
});

// ... Counter unchanged

export const App = () => (
<ScopeProvider scope={UserScope} value={"bob@example.com"}>
<Counter />
</ScopeProvider>
);
```

## API

### molecule
Expand Down
18 changes: 18 additions & 0 deletions example/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
Loading

0 comments on commit f814313

Please sign in to comment.