Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V1.2 - use bunshi #25

Merged
merged 7 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
}
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-04-20
loganvolkers marked this conversation as resolved.
Show resolved Hide resolved

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 },
],
},
}
24 changes: 24 additions & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
27 changes: 27 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:

- Configure the top-level `parserOptions` property like this:

```js
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
```

- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
Loading