Giter Site home page Giter Site logo

pmndrs / jotai Goto Github PK

View Code? Open in Web Editor NEW
17.3K 17.3K 548.0 12.24 MB

๐Ÿ‘ป Primitive and flexible state management for React

Home Page: https://jotai.org

License: MIT License

JavaScript 17.26% TypeScript 81.44% CSS 1.30%
atomic hacktoberfest management react state

jotai's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

jotai's Issues

Support reducers for primitive atoms

Allow primitive atoms to define reducers (setters), which will allow to transform the update. I don't think you should need to create a pair of a primitive and a derived atoms (where you don't export the primitive atom at all) just to accomplish this.

It seems it's somewhat supported even now, but it's undocumented and not typed correctly:

type List = { id: string; value: number }[];
const sortedListAtom = atom<List, List>([], (get, set, update) => {
  return set(
    sortedListAtom,
    update.sort((a, b) => a.value - b.value)
  );
});

Sandbox

Use case: transform updates so that callers don't have to do it themselves in each call site. In the above example, we want to update the list and ensure it's sorted. The updater may be much more complex and you don't want to force (and ensure) consumers do that themselves.

jotai.surge.sh broken on safari?

I'm under the impression that the website is broken on Safari, at least with iOS 14 and macOS Safari 14.
I am not able to enter text in the input field - in iOS the keyboard pops up, but typing doesn't have any effect.
On Desktop, the field seems selected, but i get no cursor, and typing has no effect.

This seems to be caused by " * {user-select: none;}".
(On a related note, i also found it very irritating to be unable to select input text, especially when trying to paste stuff instead of typing on mobile)

How does jotai know the exact dependents? Can we have lifescycle explained in the docs

Can you please update the README with one line or small paragraph on how jotai know what the dependents are for an atom with them? ie. In the example of const uppercaseAtom = atom(get => get(textAtom).toUpperCase()) how does it know that textAtom is a dependent of uppercaseAtom and call the callback function only if textAtom updates.

I've sadly failed to confirm after throwing a glance at the source, but I'm guessing it determines all dependents on first actual use of the atom? similar to how hooks work.

Lifecycle transparency is very important when building anything slightly more complex then a simple example; in particular with anything async. If you ever build a docs site a small section to it would be appreciated, since when things are actually likely to get garbage collected (if ever) is another very-good-to-know tidbit to avoid shooting oneself in the foot on more complex interactions (eg. extremely large table/dataset visualization).

You should also include a section on non-singletons (since those also can have weird problems with what/when/how) and typescript types (how the typescript types need to look for example), which I assume is something like...

interface LocalState {
	emailAtom: Atom<string>
	messageAtom: Atom<string>
	transportAtom: Atom<{ send: () => Promise<void> }>
	// ...any other static state
}

function useInit(): LocalState {
	let local = useRef<LocalState>();
	if (local.current == undefined) {
		let emailAtom = atom("");
		let messageAtom = atom("Hello, I can not ");
		
		const transportAtom = atom((get) => {
			let email = get(emailAtom);
			let message = get(messageAtom);
	  
			return {
			  send: async () => {
				console.log("SEND", { email, message });
			  }
			};
		  });

		local.current = {
			emailAtom,
			messageAtom,
			transportAtom
			// ...any other static state
		}
	}	

	return local.current;
}

function Input({ atom }) {
	let [ text, update ] = useAtom(atom);
	return <input value={text} onChange={event => update(event.target.value)}/>
}

export function ReusableContactForm() {
	let { 
		emailAtom, messageAtom, transportAtom
	} = useInit();

	let [ transport ] = useAtom(transportAtom);

	return (
		<>
			<Input atom={emailAtom}/>
			<Input atom={messageAtom}/>
			<button onClick={() => transport.send()}>Submit</button>
		</>
	);
}

JS Snippet Version: https://codesandbox.io/s/jotai-demo-forked-qh3qj?file=/src/App.js

The above also illustrates how for example (because atom internal workings are not very well explained) how you may encounter small gochas with non-obvious explanations (not that its ever obvious when a system gets big enough). In this case why cant you just use a function as a atom value and have to use that transport pattern instead. With out clarifying these use cases its unclear if thats the correct pattern as well; if there was a way to retrieve the value with out going though the entire react dance (and limitations of hooks in conditionals) then send could simply be a a static function declared in the useInit function that simply uses the atoms declared earlier in the scope; it's not easy to figure out why that's a bad idea or cant be done or if it's actually possible just not obvious.

Allow reducer updates to be optional

The dispatch function of reducer atoms should support optional updates if the reducer defines the update as optional.

To detect this, Jotai could use the same pattern that React's useReducer does:

type ReducerWithoutAction<V> = (value: V) => V;
const reducerWithOptionalAction: ReducerWithoutAction<number> = (value, action?: boolean) => v;

Use case:

const sidebarAtom = atomWithReducer(true, (prev, update?: boolean) => {
	return update == null ? !prev : update
});

export default function App() {
	const [isSidebarVisible, toggleSidebar] = useAtom(sidebarAtom);

	return <button onClick={() => toggleSidebar()}>toggle</button>;
}

Write tests for immutableMap

immutableMap is an internal module. It's an abstract module.
I wonder if anyone would be interested in writing tests for it.
Note that we want to test the api, not the implementation (the type ImmutableMap is representing implementation), because the goal is to come up with better implementation with same functionality.

How is jotai different from zustand?

Name

Jotai means "state" in Japanese.
Zustand means "state" in German.

Analogy

Jotai is close to Recoil.
Zustand is close to Redux.

Where state resides

Jotai state is within React component tree.
Zustand state is in the store outside React.

How to structure state

Jotai state consists of atoms (i.e. bottom-up).
Zustand state is one object (i.e. top-down).

Technical difference

The major difference is the state model. Zustand is basically a single store (you could create multiple stores, but they are separated.) Jotai is primitive atoms and composing them. In this sense, it's the matter of programming mental model.

Jotai can be seen as a replacement for useState+useContext. Instead of creating multiple contexts, atoms share one big context.

Zustand is an external store and the hook is to connect the external world to the React world.

When to use which

  • If you need a replacement for useState+useContext, jotai fits well.
  • If you want to update state outside React, zustand works better.
  • If code splitting is important, jotai should perform well.
  • If you prefer Redux devtools, zustand is good to go.
  • If you want to make use of Suspense, jotai is the one.
  • If you need a global store to bridge between two react renderers, zustand will only work. (no longer valid with jotai's provider-less mode, but zustand may work better.)

[Tips] How to use with immer

@gsimone suggested useImmerAtom:

const useImmerAtom = (anAtom) => {
  const [state, setState] = useAtom(anAtom)
  const setStateWithImmer = useCallback((fn) => {
    setState(produce((draft) => fn(draft)))
  }, [setState])
  return [state, setStateWithImmer]
}

Or, we could create a writable atom:

const countAtom = atom(0)

const countWithImmerAtom = atom(
  (get) => get(countAtom),
  (get, set, fn) => set(countAtom, produce((draft) => fn(draft)))
)

We might be able to create a helper function.

const warpWithImmer = (anAtom) => atom(
  (get) => get(anAtom),
  (get, set, fn) => set(anAtom, produce(get(anAtom), (draft) => fn(draft)))
)

Atoms value updates do not get propagated to transitively dependent atoms

Hello,

I encounter an issue with the following snippet:

const value = atom(1)
const double = atom(get => get(value) * 2)
const triple = atom(get => get(double) * 3)

// Dependency chain
// triple --(depends on)--> double --(depends on)--> value

Observed behaviour

When I update value, double is updated as well but triple is not. triple is updated only after a second render. The following snippet explain the sequence:

//Initial state
value => 1
double => 2
triple => 6

// value <= 2
value => 2
double => 4
triple => 6

// Another event cause the component to re-render
value => 2
double => 4
type => 12

Expected behaviour

I would expect the snippet above to work as:

//Initial state
value => 1
double => 2
triple => 6

// value <= 2
value => 2
double => 4
triple => 12

It seems that level > 1 dependency relationships between atoms are not handled well.

Confusion regarding the setter function for atoms; expected `set` to receive the atom's `Value` type instead of its `Update` type

Hi everyone, really appreciate the work done on jotai.

I've been spending hours today trying to understand why set wouldn't accept my atom's Value type but instead accepts the Update type. Codesandbox here.

Can anyone enlighten me?

import { atom } from "jotai";

export interface Person {
  name: string;
  age: number;
}

export interface Action<T> {
  type: string;
  payload: T;
}

export const personAtom = atom<Person, Action<any>>(
  () => ({
    name: "Joe",
    age: 55
  }),
  (get, set, update) => {
    const newPerson = {
      name: "Jon",
      age: 65,
      [update.type]: update.payload
    };
    set(personAtom, newPerson);
    //              ~~~~~~~~~
    // TypeScript error here ^
    // but why am I setting personAtom with an update value?
    // shouldn't it be the real value? i.e. Person?
  }
);

[Tips] useReducer equivalent

useAtom with primitive atoms is analogous to useState. We can create something similar to useReducer.

const countAtom = atom(0)

const countReducer = (prev, action) => {
  if (action.type === 'inc') {
    return prev + 1
  } else if (action.type === 'dec') {
    return prev - 1
  } else {
    throw new Error('unknown action type')
  }
}

const countReducerAtom = atom(
  get => get(countAtom),
  (get, set, action) => set(countAtom, countReducer(get(countAtom), action)
)

const Component = () => {
  const [count, setCount] = useAtom(countAtom) // useState style
  const [count2, dispatch] = useAtom(countReducerAtom) // useReducer style
  // ...
}

[Tips] How to persist a value with localStorage/AsyncStorage

It's not supported by the lib itself, but here's an idea how to persist data.

const strAtom = atom(localStorage.getItem('myKey') ?? 'foo')

const strAtomWithPersistence = atom(
  get => get(strAtom),
  (get, set, newStr) => {
    set(strAtom, newStr)
    localStorage.setItem('myKey', newStr)
  },
)

or

const strAtom = atom('foo')

const persistStrAtom = atom(
  null,
  (get, set, action) => {
    if (action.type === 'init') {
      const str = localStorage.getItem('myKey')
      if (str !== null) {
        set(strAtom, str)
      }
    } else if (action.type === 'set') {
      set(strAtom, action.str)
      localStorage.setItem('myKey', str)
    }
  },
)

Benchmark immutableMap (create a benchmark tool)

After #107, or in parallel with it, we would want to create a benchmark tool for immutableMap.
It would allow us to evaluate the current implementation, compare possible alternative implementations, and improve the entire performance of jotai.

Anybody keen about performance?

Improve docs/*.md

#83 added some drafts. we need to improve them.

There would be several levels of contributions.

  • Fix typo, grammar, and/or expressions (please file a PR)
  • Ask questions in a new issue to clarify, add, and improve descriptions (a followup PR would be nice)
  • Or, you can start a PR directly with a draft doc and discuss there

Feel free to ask general questions in this issue. Also we have a channel in the pmndrs Discord server.

Create a setup local environment tutorial

Description

I've encountered a few error when I setting up at both LINUX & Windows environment.

The reason of my linux environment is I use npm install instead yarn install, while windows couldn't execute the command like mv, cp and rm

To avoid other new-to-jotai contributor and users who want to build jotai at their local environment, I suggest we update a setup tutorial at the bottom of README.md, and create a simple script to classify user's environment and assign appropriate query to execute.

I've already create the os classify script and the tutorial at my local environment, and I hope I can send these files as a Pull Request.

Step to Reproduce

git clone https://github.com/pmndrs/jotai.git
cd ./jotai
npm install

[Tips] useSelector equivalent

While the atom model fits with bottom-up approach (composing small atoms), we could create a big atom and select pieces out of it.

const bigAtom = atom({
  num: 0,
  str: 'hi',
  bool: true,
  obj: { a: 1, b: 2, arr: [3, 4, 5] },
})

const numAtom = atom(
  get => get(bigAtom).num,
  (get, set, update) => {
    const prev = get(bigAtom)
    const num = typeof update === 'function' ? update(prev.num) : update
    set(bigAtom, { ...prev, num })
  }
)

  const [num, setNum] = useAtom(numAtom)

We can make this a hook. Let's try for a read-only atom.

const useSelector = (anAtom, selector) => {
  const selectedAtom = useMemo(() => atom(get => selector(get(anAtom))), [anAtom, selector])
  return useAtom(selectedAtom)[0]
}

[Question] Where do atoms and derived atoms values 'live'?

Hi @dai-shi, first of all thank you for this library. It is great to have different options, approaches and improvements around the React state topic. Good job!

I'm playing a bit with it and trying to better understand what's happening under the hood, which use cases can be easily covered with jotai and which doesn't (if any). And several questions arised. I didn't check code yet, sorry, so my questions are based on guessing.

My first assumption when I saw the Provider wrapping the tree:

<Provider>
  <App />
</Provider>

and also based on this quote:

If you need React Context alternatives, Jotai comes with enough features

was to think that:

  • Provider is kind of a contextual store where all the atom values live. Is that correct?

Also, for learning/debug purposes:

  • Is there a way we can check atom values directly from its "store" without consuming them? Similar to what Redux DevTools offer.

No .cjs version of utils avaliable

Thereโ€™s a index.cjs file in the distribution, but no utils.cjs, is there any reason? Iโ€™m trying to debug an error with jest so Iโ€™m just wondering. Thanks!

Focusable atoms (optics for this lib)

Hello,

Interesting library!

Would it be possible for you to provide utilities for using this in conjunction with a optics library? My idea for a pretty nice api would be:

const bigAtom: WritableAtom<{myKey: number[]}> = atom({myKey: [1,2,3]})
const focusedAtom: WritableAtom<number> = bigAtom.focus(optic => optic.prop('myKey').index(0))

Or even:

const bigAtom: WritableAtom<{myKey: 10}> = atom({myKey: 10})
const focusedAtom: WritableAtom<number> = bigAtom.prop('myKey').index(0)

The optics library I like is https://github.com/akheron/optics-ts

The idea is that if any of the focused atoms are "set", then the big atom is updated, leading to a update to the focused atom.

Is atomFamily behavior possible?

Recoil has atomFamily which allows atoms to be created and referenced with something akin to a groupId. It seems like that kind of behavior can not be replicated without some kind string key identifiers.

How to avoid extra rerender?

with Jotai, the <FakeCard /> will re-render whenever editId changed, but Recoil.js does not because i can select only setter function. How to achieve the same behavior with Jotai?

Recoil

import React from 'react';
import { atom, useRecoilState, useSetRecoilState } from 'recoil';

const editIdAtom = atom<number | null>({
  key: `editIdAtom`,
  default: null,
});

function FakeCard() {
  const setEditId = useSetRecoilState(editIdAtom);
  const handleClick = React.useCallback(
    (e: React.MouseEvent<HTMLAnchorElement>) => {
      setEditId(1);
    },
    [setEditId]
  );
  return (
    <div>
      <a onClick={handleClick}>toggle</a>
      <FakeModal />
    </div>
  );
}

function FakeModal() {
  const [editId, setEditId] = useRecoilState(editIdAtom);
  const handleClose = React.useCallback(() => {
    setEditId(null);
  }, [setEditId]);

  return editId ? <span>editing {editId}</span> : null;
}

Jotai

import React from 'react';
import { atom, useAtom } from 'jotai';

const editIdAtom = atom<number | null>(null);

function FakeCard() {
  const [,setEditId] = useAtom(editIdAtom);
  const handleClick = React.useCallback(
    (e: React.MouseEvent<HTMLAnchorElement>) => {
      setEditId(1);
    },
    [setEditId]
  );
  return (
    <div>
      <a onClick={handleClick}>toggle</a>
      <FakeModal />
    </div>
  );
}

function FakeModal() {
  const [editId, setEditId] = useAtom(editIdAtom);
  const handleClose = React.useCallback(() => {
    setEditId(null);
  }, [setEditId]);

  return editId ? <span>editing {editId}</span> : null;
}

[Docs] API

Here's a draft for API docs.

atom

atom is a function to create an atom config. It's an object and the object identity is important. You can create it from everywhere. Once created, you shouldn't modify the object. (Note: There might be an advanced use case to mutate atom configs after creation. At the moment, it's not officially supported though.)

const primitiveAtom = atom(initialValue)
const derivedAtomWithRead = atom(readFunction)
const derivedAtomWithReadWrite = atom(readFunction, writeFunction)
const derivedAtomWithWriteOnly = atom(null, writeFunction)

There are two kinds of atoms: a writable atom and a read-only atom
Primitive atoms are always writable. Derived atoms are writable if writeFunction is specified.
The writeFunction of primitive atoms is equivalent to the setState of React.useState.

The signature of readFunction is (get) => Value | Promise<Value>, and get is a function that takes an atom config and returns its value stored in Provider described below.
Dependency is tracked, so if get is used for an atom at least once, then whenever the atom value is changed, the readFunction will be reevaluated.

The signature of writeFunction is (get, set, update) => void | Promise<void>.
get is similar to the one described above, but it doesn't track the dependency. set is a function that takes an atom config and a new value, and update the atom value in Provider. update is an arbitrary value that we receive from the updating function returned by useAtom described below.

Provider

Atom configs don't hold values. Atom values are stored in a Provider. A Provider can be used like React context provider. Usually, we place one Provider at the root of the app, however you could use multiple Providers, each storing different atom values for its component tree.

const Root = () => (
  <Provider>
    <App />
  </Provider>
)

useAtom

The useAtom hook is to read an atom value stored in the Provider. It returns the atom value and an updating function as a tuple, just like useState. It takes an atom config created with atom(). Initially, there is no value stored in the Provider. At the first time the atom is used via useAtom, it will add an initial value in the Provider. If the atom is a derived atom, the read function is executed to compute an initial value. When an atom is no longer used, meaning the component using it is unmounted, the value is removed from the Provider.

const [value, updateValue] = useAtom(anAtom)

The updateValue takes just one argument, which will be passed to the third argument of writeFunction of the atom. The behavior is totally depends on how the writeFunction is implemented.

SSR example (w/ Next.js)

It's a refreshing and beautiful take on state management, fitting perfectly my usecase!
Going through the source code, I don't see any re/hydration mechanism.

@dai-shi : If Jotai is SSR-ready or close to, could you set up an example? Or point out how?

Thanks!

[Tips] Updating atom object property

Atoms can hold any primitive values, including objects.
Here's an example to updating a property of an object.

const funkyPigAtom = atom({ funkyPig: { glasses: 'stylish', bling: 'golden' }})

const Component = () => {
  const [funkyPig, setFunkyPig] = useAtom(funkyPigAtom)
  const updateBling = (bling) => {
    setFunkcyPig((prev) => ({ ...prev, funkyPig: { ...prev.funkyPig, bling } }))
  }
  // ...
}

We could also create an update action atom.

const funkyPigAtom = atom({ funkyPig: { glasses: 'stylish', bling: 'golden' }})
const updateBling = atom(
  null,
  (get, set, bling) => {
    const prev = get(funkyPigAtom)
    set(funkyPigAtom, { ...prev, funkyPig: { ...prev.funkyPig, bling } })
  }
)

[Tips] Atoms can be created on demand

Atoms can be created at anytime, like event callbacks and useEffect, or in promises.

Here's a simple Todo example.

import * as React from "react";
import { useState, FormEvent } from "react";
import { Provider, atom, useAtom, PrimitiveAtom } from "jotai";

type Todo = {
  title: string;
  completed: boolean;
};

const TodoItem: React.FC<{
  todoAtom: PrimitiveAtom<Todo>;
  remove: () => void;
}> = ({ todoAtom, remove }) => {
  const [item, setItem] = useAtom(todoAtom);
  const toggleCompleted = () => {
    setItem((prev) => ({ ...prev, completed: !prev.completed }));
  };
  return (
    <li>
      <label>
        <input
          type="checkbox"
          checked={item.completed}
          onChange={toggleCompleted}
        />
        <span style={{ textDecoration: item.completed ? "line-through" : "" }}>
          {item.title}
        </span>
        {item.completed && <button onClick={remove}>Remove</button>}
      </label>
    </li>
  );
};

const todosAtom = atom<PrimitiveAtom<Todo>[]>([]);

const TodoList: React.FC = () => {
  const [title, setTitle] = useState("");
  const [todos, setTodos] = useAtom(todosAtom);
  const addTodo = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const todoAtom = atom<Todo>({ title, completed: false });
    setTodos((prev) => [...prev, todoAtom]);
    setTitle("");
  };
  const removeTodo = (todoAtom: PrimitiveAtom<Todo>) => {
    setTodos((prev) => prev.filter((item) => item !== todoAtom));
  };
  return (
    <ul>
      {todos.map((todoAtom) => (
        <TodoItem
          key={todoAtom.key}
          todoAtom={todoAtom}
          remove={() => removeTodo(todoAtom)}
        />
      ))}
      <li>
        <form onSubmit={addTodo}>
          <input
            value={title}
            onChange={(e) => setTitle(e.target.value)}
            placeholder="Enter title..."
          />
        </form>
      </li>
    </ul>
  );
};

const App: React.FC = () => (
  <Provider>
    <TodoList />
  </Provider>
);

export default App;

Pass parameters to atom with async read function

First of all, amazing library, thank you so much :)

When I initialize an atom that will make an async call to an API endpoint, is there a way to pass an attribute? I know that I could write a primitive atom that will hold the parameter and make the async one dependent on it, but a more direct approach would be more elegant in my opinion.

I tried initializing it as follows but it keeps re-rendering my component infinitely and thus also makes infinite calls to the API

const fetchData =(id) atom({
   async ()=>{
      const res = await getDataByID(id)
      return res.data
   } 
})

Thanks for the support!

[Next.js] Error using useUpdateAtom

Using Next.js
Attached screenshot
The code is like this

import { atom, useAtom } from 'jotai'
import { useUpdateAtom } from 'jotai/utils'

const eventsAtom = atom<Events>([])

export const useCreateSSE = () => {
    let updateEvents = useUpdateAtom(eventsAtom)
}

Wrapping Component in _app with Provider
And then using this custom hook in some component

When I used useUpdateAtom code directly, just copied from the repo, it worked.

my tsconfig:

{
    "compilerOptions": {
        "target": "es6",
        "lib": ["dom", "dom.iterable", "esnext"],
        "allowJs": true,
        "skipLibCheck": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "noEmit": true,
        "noImplicitAny": true,
        "strictNullChecks": true,
        "strictFunctionTypes": true,
        "strictPropertyInitialization": true,
        "noImplicitThis": true,
        "alwaysStrict": true,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": true,
        "esModuleInterop": true,
        "module": "esnext",
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "downlevelIteration": true,
        "baseUrl": ".",
        "allowSyntheticDefaultImports": true,
        "jsx": "preserve",
        "paths": {
            "~/*": ["./src/*"]
        }
    },
    "exclude": ["node_modules", "public", "not_used"],
    "include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx", "src/**/*.jsx", "src/**/*.js"]
}

image

atoms and error boundaries

I'm trying out jotai in one of my projects and it was crashing with code like this

 React.useEffect(() => {
    setState(s => {
      const x = {};
      delete x[123][123];
    });
  });

and I was confused why this error wasn't caught by the react error boundary (of react-error-boundary fame). Then I realized that my component three was like this JotaiProvider -> ErrorBoundary -> MyComponent so doing something like ErrorBoundary -> JotaiProvider -> ErrorBoundary -> MyComponent helped, but it makes me realize interesting consideration that with smaller context providers I can have them closer to my component and thus making errors more local, but errors in jotai are basically globally scoped :)

I don't know if that's an issue per se, but maybe a good point to consider when deciding between custom context vs global context

Improve core tests

Core tests are the followings:

tests/async.test.tsx
tests/basic.test.tsx
tests/dependency.test.tsx
tests/error.test.tsx
tests/items.test.tsx

I need someone to help advice if it's ok or if there's room for improvement. I followed the same pattern as in zustand, but I'm not quite sure if testing with findByText is a good/desired way (instead of using expect.)

Imperatively refresh atomFamily

What would be the best way to imperatively refresh an atomFamily?

Let's say I have following case, where I use get(updateAtomFamily) in order to track it as dependency in the atomFamily so that I trigger the fetch imperatively:

export const fetchAtomFamily = atomFamily(
  (param) => async (get) => {
    get(updateAtomFamily);
    const { data } = await getData({
      param1: param.param1,
      param2: param.param2,
    });
  },
  null,
  deepEqual
);

export const updateAtomFamily = atom(0);

Then my idea was to imperatively useUpdateAtom in my component as a refresh function like this:

  const refresh= useUpdateAtom(updateAtomFamily);

and then have the imperative call:

<button onClick={() => refresh(prev => prev + 1)}>refresh</button> 

However, when I trigger the refresh function, I get following error:

Uncaught Error: atom state not found. possibly a bug.

In my opinion however, even if this approach worked, it feels sluggish and cumbersome to me. It would be extremely helpful if the API had a native approach to re-fetch async atoms :)

Concurrent mode async data fetching with atomFamily

Hey let's say I have a Parent component that fetches data and renders a list of that data. So far so good. Then for each element of the parent data fetched a Child component is rendered that uses atomFamily. Now each Child component initialized the atomFamily with the same ID, so I thought the caching mechanism should kick in and after it sees that data was fetched once from the atomFamily, subsequent calls with useAtom in other children will not make HTTP requests anymore but will read data from cache.

codesandobox: https://codesandbox.io/s/delicate-rain-f89kt You can open the console in the network tab to see that Child is making as many HTTP requests as list elements in Parent

Add utils test

We don't have any tests for jotai/utils. Would someone work on it?

Support functional updates for derived atoms

Currently, setters of derived atoms do not support functional updates.

Proposed API:

const listAtom = atom([]);
const sortedListAtom = atom((get) => get(listAtom), (_get, set, update) => set(listAtom, update));

const App = () => {
	const [list, setList] = useAtom(sortedListAtom);
	return <button onClick={() => setList((prev) => [...prev, "oi!"])}>add</button>
};

In other words, is there a reason why the setter of derived atom isn't a SetStateAction same as for primitive atoms?

How to allow partial objects when updating

Hey,

I have an atom like this:

const test = atom({
  one: 'foo',
  two: 'bar',
  cache: {}
});

In my code I want to do the following:

setTest({
  cache: {
    test: true,
  },
});

Is this supported? Currently, you have to spread ...test inside of the setTest, however, this does not deep merge all objects inside of cache

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.