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
105 changes: 6 additions & 99 deletions src/core/useAtom.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
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'

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
import { ResolveType, useAtomValue } from './useAtomValue'
Thisen marked this conversation as resolved.
Show resolved Hide resolved
import { useSetAtom } from './useSetAtom'

export function useAtom<Value, Update, Result extends void | Promise<void>>(
atom: WritableAtom<Value, Update, Result>,
Expand All @@ -38,85 +22,8 @@ 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),
useSetAtom(atom as WritableAtom<Value, Update, Result>, scope),
]
Thisen marked this conversation as resolved.
Show resolved Hide resolved
}
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'

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

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
}
32 changes: 32 additions & 0 deletions src/core/useSetAtom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useCallback, useContext } from 'react'
import { SECRET_INTERNAL_getScopeContext as getScopeContext } from 'jotai'
import type { WritableAtom } from 'jotai'
import type { Atom, Scope, SetAtom } from '../core/atom'
import { WRITE_ATOM } from '../core/store'
import type { VersionObject } from '../core/store'
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 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 (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]
)
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 type { Atom, WritableAtom, PrimitiveAtom } from './core/atom'
export type {
Getter,
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