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

feat(core): Move useAtomValue and useSetAtom to core #989

Merged
merged 13 commits into from
Feb 11, 2022
7 changes: 3 additions & 4 deletions examples/hacker_news/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Suspense } from 'react'
import { a, useSpring } from '@react-spring/web'
import Parser from 'html-react-parser'
import { Provider, atom, useAtom } from 'jotai'
import { useUpdateAtom } from 'jotai/utils'
import { Provider, atom, useAtom, useSetAtom } from 'jotai'

type PostData = {
by: string
Expand Down Expand Up @@ -34,9 +33,9 @@ function Id() {
}

function Next() {
// Use `useUpdateAtom` to avoid re-render
// Use `useSetAtom` to avoid re-render
// const [, set] = useAtom(postId)
const setPostId = useUpdateAtom(postId)
const setPostId = useSetAtom(postId)
return (
<button onClick={() => setPostId((id) => id + 1)}>
<div>→</div>
Expand Down
7 changes: 3 additions & 4 deletions examples/todos/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import type { FormEvent } from 'react'
import { CloseOutlined } from '@ant-design/icons'
import { a, useTransition } from '@react-spring/web'
import { Radio } from 'antd'
import { Provider, atom, useAtom } from 'jotai'
import { Provider, atom, useAtom, useSetAtom } from 'jotai'
import type { PrimitiveAtom } from 'jotai'
import { useUpdateAtom } from 'jotai/utils'

type Todo = {
title: string
Expand Down Expand Up @@ -76,9 +75,9 @@ const Filtered = (props: FilteredType) => {
}

const TodoList = () => {
// Use `useUpdateAtom` to avoid re-render
// Use `useSetAtom` to avoid re-render
// const [, setTodos] = useAtom(todosAtom)
const setTodos = useUpdateAtom(todosAtom)
const setTodos = useSetAtom(todosAtom)
const remove: RemoveFn = (todo) =>
setTodos((prev) => prev.filter((item) => item !== todo))
const add = (e: FormEvent<HTMLFormElement>) => {
Expand Down
8 changes: 4 additions & 4 deletions examples/todos_with_atomFamily/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { FormEvent } from 'react'
import { CloseOutlined } from '@ant-design/icons'
import { a, useTransition } from '@react-spring/web'
import { Radio } from 'antd'
import { Provider, atom, useAtom } from 'jotai'
import { atomFamily, useUpdateAtom } from 'jotai/utils'
import { Provider, atom, useAtom, useSetAtom } from 'jotai'
import { atomFamily } from 'jotai/utils'
import { nanoid } from 'nanoid'

type Param = { id: string; title?: string }
Expand Down Expand Up @@ -75,9 +75,9 @@ const Filtered = ({ remove }: { remove: (id: string) => void }) => {
}

const TodoList = () => {
// Use `useUpdateAtom` to avoid re-render
// Use `useSetAtom` to avoid re-render
// const [, setTodos] = useAtom(todosAtom)
const setTodos = useUpdateAtom(todosAtom)
const setTodos = useSetAtom(todosAtom)
const remove = (id: string) => {
setTodos((prev) => prev.filter((item) => item !== id))
todoAtomFamily.remove({ id })
Expand Down
104 changes: 7 additions & 97 deletions src/core/useAtom.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
import {
useCallback,
useContext,
useDebugValue,
useEffect,
useReducer,
} from 'react'
import type { Reducer } from 'react'
import type { Atom, Scope, SetAtom, WritableAtom } from './atom'
import { getScopeContext } from './contexts'
import { COMMIT_ATOM, READ_ATOM, SUBSCRIBE_ATOM, WRITE_ATOM } from './store'
import type { VersionObject } from './store'
import { useAtomValue } from './useAtomValue'
import { useSetAtom } from './useSetAtom'

type ResolveType<T> = T extends Promise<infer V> ? V : T
Thisen marked this conversation as resolved.
Show resolved Hide resolved

const isWritable = <Value, Update, Result extends void | Promise<void>>(
atom: Atom<Value> | WritableAtom<Value, Update, Result>
): atom is WritableAtom<Value, Update, Result> =>
!!(atom as WritableAtom<Value, Update, Result>).write

export function useAtom<Value, Update, Result extends void | Promise<void>>(
atom: WritableAtom<Value, Update, Result>,
scope?: Scope
Expand All @@ -38,85 +24,9 @@ export function useAtom<Value, Update, Result extends void | Promise<void>>(
)
scope = (atom as { scope: Scope }).scope
}

const ScopeContext = getScopeContext(scope)
const { s: store, w: versionedWrite } = useContext(ScopeContext)

const getAtomValue = useCallback(
(version?: VersionObject) => {
// This call to READ_ATOM is the place where derived atoms will actually be
// recomputed if needed.
const atomState = store[READ_ATOM](atom, version)
if ('e' in atomState) {
throw atomState.e // read error
}
if ('p' in atomState) {
throw atomState.p // read promise
}
if ('v' in atomState) {
return atomState.v as ResolveType<Value>
}
throw new Error('no atom value')
},
[store, atom]
)

// Pull the atoms's state from the store into React state.
const [[version, value, atomFromUseReducer], rerenderIfChanged] = useReducer<
Reducer<
readonly [VersionObject | undefined, ResolveType<Value>, Atom<Value>],
VersionObject | undefined
>,
undefined
>(
useCallback(
(prev, nextVersion) => {
const nextValue = getAtomValue(nextVersion)
if (Object.is(prev[1], nextValue) && prev[2] === atom) {
return prev // bail out
}
return [nextVersion, nextValue, atom]
},
[getAtomValue, atom]
),
undefined,
() => {
// NOTE should/could branch on mount?
const initialVersion = undefined
const initialValue = getAtomValue(initialVersion)
return [initialVersion, initialValue, atom]
}
)

if (atomFromUseReducer !== atom) {
rerenderIfChanged(undefined)
}

useEffect(() => {
// Call `rerenderIfChanged` whenever this atom is invalidated. Note
// that derived atoms may not be recomputed yet.
const unsubscribe = store[SUBSCRIBE_ATOM](atom, rerenderIfChanged)
rerenderIfChanged(undefined)
return unsubscribe
}, [store, atom])

useEffect(() => {
store[COMMIT_ATOM](atom, version)
})

const setAtom = useCallback(
(update: Update) => {
if (isWritable(atom)) {
const write = (version?: VersionObject) =>
store[WRITE_ATOM](atom, update, version)
return versionedWrite ? versionedWrite(write) : write()
} else {
throw new Error('not writable atom')
}
},
[store, versionedWrite, atom]
)

useDebugValue(value)
return [value, setAtom]
return [
useAtomValue(atom, scope),
// We do wrong type assertion here, which results in throwing an error.
useSetAtom(atom as WritableAtom<Value, Update, Result>, scope),
]
}
87 changes: 87 additions & 0 deletions src/core/useAtomValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
useCallback,
useContext,
useDebugValue,
useEffect,
useReducer,
} from 'react'
import type { Reducer } from 'react'
import type { Atom, Scope } from './atom'
import { getScopeContext } from './contexts'
import { COMMIT_ATOM, READ_ATOM, SUBSCRIBE_ATOM } from './store'
import type { VersionObject } from './store'

type ResolveType<T> = T extends Promise<infer V> ? V : T

export function useAtomValue<Value>(
atom: Atom<Value>,
scope?: Scope
): ResolveType<Value> {
const ScopeContext = getScopeContext(scope)
const { s: store } = useContext(ScopeContext)

const getAtomValue = useCallback(
(version?: VersionObject) => {
// This call to READ_ATOM is the place where derived atoms will actually be
// recomputed if needed.
const atomState = store[READ_ATOM](atom, version)
if ('e' in atomState) {
throw atomState.e // read error
}
if ('p' in atomState) {
throw atomState.p // read promise
}
if ('v' in atomState) {
return atomState.v as ResolveType<Value>
}
throw new Error('no atom value')
},
[store, atom]
)

// Pull the atoms's state from the store into React state.
const [[version, value, atomFromUseReducer], rerenderIfChanged] = useReducer<
Reducer<
readonly [VersionObject | undefined, ResolveType<Value>, Atom<Value>],
VersionObject | undefined
>,
undefined
>(
useCallback(
(prev, nextVersion) => {
const nextValue = getAtomValue(nextVersion)
if (Object.is(prev[1], nextValue) && prev[2] === atom) {
return prev // bail out
}
return [nextVersion, nextValue, atom]
},
[getAtomValue, atom]
),
undefined,
() => {
// NOTE should/could branch on mount?
const initialVersion = undefined
const initialValue = getAtomValue(initialVersion)
return [initialVersion, initialValue, atom]
}
)

if (atomFromUseReducer !== atom) {
rerenderIfChanged(undefined)
}

useEffect(() => {
// Call `rerenderIfChanged` whenever this atom is invalidated. Note
// that derived atoms may not be recomputed yet.
const unsubscribe = store[SUBSCRIBE_ATOM](atom, rerenderIfChanged)
rerenderIfChanged(undefined)
return unsubscribe
}, [store, atom])

useEffect(() => {
store[COMMIT_ATOM](atom, version)
})

useDebugValue(value)
return value
}
31 changes: 31 additions & 0 deletions src/core/useSetAtom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useCallback, useContext } from 'react'
import type { Scope, SetAtom, WritableAtom } from '../core/atom'
import { WRITE_ATOM } from '../core/store'
import type { VersionObject } from '../core/store'
import { getScopeContext } from './contexts'

export function useSetAtom<Value, Update, Result extends void | Promise<void>>(
atom: WritableAtom<Value, Update, Result>,
scope?: Scope
): SetAtom<Update, Result> {
const ScopeContext = getScopeContext(scope)
const { s: store, w: versionedWrite } = useContext(ScopeContext)
const setAtom = useCallback(
(update: Update) => {
if (
!('write' in atom) &&
typeof process === 'object' &&
process.env.NODE_ENV !== 'production'
) {
// useAtom can pass non writable atom with wrong type assertion,
// so we should check here.
throw new Error('not writable atom')
}
const write = (version?: VersionObject) =>
store[WRITE_ATOM](atom, update, version)
return versionedWrite ? versionedWrite(write) : write()
},
[store, versionedWrite, atom]
)
return setAtom as SetAtom<Update, Result>
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export { Provider } from './core/Provider'
export { atom } from './core/atom'
export { useAtom } from './core/useAtom'
export { useAtomValue } from './core/useAtomValue'
export { useSetAtom } from './core/useSetAtom'
export { createStoreForExport as unstable_createStore } from './core/store'
export type { Atom, WritableAtom, PrimitiveAtom } from './core/atom'
export type {
Expand Down
4 changes: 2 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { RESET } from './utils/constants'
export { useUpdateAtom } from './utils/useUpdateAtom'
export { useAtomValue } from './utils/useAtomValue'
export { useSetAtom as useUpdateAtom } from 'jotai'
export { useAtomValue } from 'jotai'
export { atomWithReset } from './utils/atomWithReset'
export { useResetAtom } from './utils/useResetAtom'
export { useReducerAtom } from './utils/useReducerAtom'
Expand Down
6 changes: 2 additions & 4 deletions src/utils/useAtomCallback.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { useCallback, useMemo } from 'react'
import { atom } from 'jotai'
import { atom, useSetAtom } from 'jotai'
import type { Setter, WritableAtom } from 'jotai'
import type { Scope } from '../core/atom'
// NOTE importing non-core functions is generally not allowed. this is an exception.
import { useUpdateAtom } from './useUpdateAtom'

type WriteGetter = Parameters<WritableAtom<unknown, unknown>['write']>[0]

Expand Down Expand Up @@ -47,7 +45,7 @@ export function useAtomCallback<Result, Arg>(
),
[callback]
)
const invoke = useUpdateAtom(anAtom, scope)
const invoke = useSetAtom(anAtom, scope)
return useCallback(
(arg: Arg) =>
new Promise<Result>((resolve, reject) => {
Expand Down
7 changes: 0 additions & 7 deletions src/utils/useAtomValue.ts

This file was deleted.

24 changes: 0 additions & 24 deletions src/utils/useUpdateAtom.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { fireEvent, render } from '@testing-library/react'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { getTestProvider } from '../testUtils'
import { atom, useAtomValue, useSetAtom } from 'jotai'
import { getTestProvider } from './testUtils'

const Provider = getTestProvider()

Expand All @@ -10,7 +9,7 @@ it('useAtomValue basic test', async () => {

const Counter = () => {
const count = useAtomValue(countAtom)
const setCount = useUpdateAtom(countAtom)
const setCount = useSetAtom(countAtom)

return (
<>
Expand Down