From f90b3d31849ac0213ad60e002d61808706620f32 Mon Sep 17 00:00:00 2001 From: rtritto Date: Sat, 14 Jan 2023 20:50:29 +0100 Subject: [PATCH 1/7] Replace zustand with jotai --- package.json | 2 +- rollup.config.ts | 3 +- src/components/DataKeyPair.tsx | 50 +++--- src/components/DataTypes/Function.tsx | 5 +- src/components/DataTypes/Object.tsx | 40 +++-- src/components/DataTypes/createEasyType.tsx | 14 +- src/hooks/useColor.ts | 5 - src/hooks/useCopyToClipboard.ts | 5 +- src/hooks/useInspect.ts | 58 ++++--- src/hooks/useIsCycleReference.ts | 8 +- src/index.tsx | 138 +++++++++-------- src/state.ts | 60 ++++++++ src/stores/JsonViewerStore.ts | 160 +++++++------------- src/stores/typeRegistry.tsx | 64 +++----- src/type.ts | 30 +++- tsconfig.json | 2 +- yarn.lock | 71 +++++---- 17 files changed, 400 insertions(+), 315 deletions(-) delete mode 100644 src/hooks/useColor.ts create mode 100644 src/state.ts diff --git a/package.json b/package.json index ba516ac5..be30ad5b 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@emotion/styled": "^11.10.6", "@mui/material": "^5.11.13", "copy-to-clipboard": "^3.3.3", - "zustand": "^4.3.6" + "jotai": "^1.13.0" }, "lint-staged": { "!*.{ts,tsx,js,jsx}": "prettier --write --ignore-unknown", diff --git a/rollup.config.ts b/rollup.config.ts index 3ffc69a7..6fbb9547 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -29,7 +29,8 @@ const external = [ '@mui/material', '@mui/material/styles', 'copy-to-clipboard', - 'zustand', + 'jotai', + 'jotai/utils', 'react', 'react/jsx-runtime', 'react-dom', diff --git a/src/components/DataKeyPair.tsx b/src/components/DataKeyPair.tsx index f2047d18..e16d9695 100644 --- a/src/components/DataKeyPair.tsx +++ b/src/components/DataKeyPair.tsx @@ -1,11 +1,23 @@ import { Box } from '@mui/material' +import { useAtomValue, useSetAtom } from 'jotai' +import { useAtomCallback } from 'jotai/utils' import type { ComponentProps, FC, MouseEvent } from 'react' import { useCallback, useMemo, useState } from 'react' -import { useTextColor } from '../hooks/useColor' import { useClipboard } from '../hooks/useCopyToClipboard' import { useInspect } from '../hooks/useInspect' -import { useJsonViewerStore } from '../stores/JsonViewerStore' +import { + colorspaceAtom, + editableAtom, + enableClipboardAtom, + hoverPathAtom, + keyRendererAtom, + onChangeAtom, + quotesOnKeysAtom, + rootNameAtom, + setHoverAtomFamily, + valueAtom +} from '../state' import { useTypeComponents } from '../stores/typeRegistry' import type { DataItemProps } from '../type' import { getValueSize } from '../utils' @@ -43,7 +55,7 @@ const IconBox: FC = (props) => ( export const DataKeyPair: FC = (props) => { const { value, path, nestedIndex } = props const propsEditable = props.editable ?? undefined - const storeEditable = useJsonViewerStore(store => store.editable) + const storeEditable = useAtomValue(editableAtom) const editable = useMemo(() => { if (storeEditable === false) { return false @@ -60,26 +72,33 @@ export const DataKeyPair: FC = (props) => { const [tempValue, setTempValue] = useState(typeof value === 'function' ? () => value : value) const depth = path.length const key = path[depth - 1] - const hoverPath = useJsonViewerStore(store => store.hoverPath) + const hoverPath = useAtomValue(hoverPathAtom) const isHover = useMemo(() => { return hoverPath && path.every( (value, index) => value === hoverPath.path[index] && nestedIndex === hoverPath.nestedIndex) }, [hoverPath, path, nestedIndex]) - const setHover = useJsonViewerStore(store => store.setHover) - const root = useJsonViewerStore(store => store.value) + const setHover = useAtomCallback( + useCallback((get, set, arg) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + useSetAtom(setHoverAtomFamily(arg)) + }, []) + ) + const root = useAtomValue(valueAtom) const [inspect, setInspect] = useInspect(path, value, nestedIndex) const [editing, setEditing] = useState(false) - const onChange = useJsonViewerStore(store => store.onChange) - const keyColor = useTextColor() - const numberKeyColor = useJsonViewerStore(store => store.colorspace.base0C) + const onChange = useAtomValue(onChangeAtom) + const { + base07: keyColor, + base0C: numberKeyColor + } = useAtomValue(colorspaceAtom) const { Component, PreComponent, PostComponent, Editor } = useTypeComponents(value, path) - const quotesOnKeys = useJsonViewerStore(store => store.quotesOnKeys) - const rootName = useJsonViewerStore(store => store.rootName) + const quotesOnKeys = useAtomValue(quotesOnKeysAtom) + const rootName = useAtomValue(rootNameAtom) const isRoot = root === value const isNumberKey = Number.isInteger(Number(key)) - const enableClipboard = useJsonViewerStore(store => store.enableClipboard) + const enableClipboard = useAtomValue(enableClipboardAtom) const { copy, copied } = useClipboard() const actionIcons = useMemo(() => { @@ -161,7 +180,7 @@ export const DataKeyPair: FC = (props) => { const isEmptyValue = useMemo(() => getValueSize(value) === 0, [value]) const expandable = !isEmptyValue && !!(PreComponent && PostComponent) - const KeyRenderer = useJsonViewerStore(store => store.keyRenderer) + const KeyRenderer = useAtomValue(keyRendererAtom) const downstreamProps: DataItemProps = useMemo(() => ({ path, inspect, @@ -173,10 +192,7 @@ export const DataKeyPair: FC = (props) => { className='data-key-pair' data-testid={'data-key-pair' + path.join('.')} sx={{ userSelect: 'text' }} - onMouseEnter={ - useCallback(() => setHover(path, nestedIndex), - [setHover, path, nestedIndex]) - } + onMouseEnter={() => setHover({ path, nestedIndex })} > > = () => { } export const FunctionType: FC> = (props) => { - const functionColor = useJsonViewerStore(store => store.colorspace.base05) + const { base05: functionColor } = useAtomValue(colorspaceAtom) return ( > = (props) => { - const metadataColor = useJsonViewerStore(store => store.colorspace.base04) - const textColor = useTextColor() + const { + base04: metadataColor, + base07: textColor + } = useAtomValue(colorspaceAtom) const isArray = useMemo(() => Array.isArray(props.value), [props.value]) const isEmptyValue = useMemo(() => getValueSize(props.value) === 0, [props.value]) - const sizeOfValue = useMemo(() => inspectMetadata(props.value), [props.value] - ) - const displayObjectSize = useJsonViewerStore(store => store.displayObjectSize) + const sizeOfValue = useMemo(() => inspectMetadata(props.value), [props.inspect, props.value]) + const displayObjectSize = useAtomValue(displayObjectSizeAtom) const isTrap = useIsCycleReference(props.path, props.value) return ( > = (props) => { } export const PostObjectType: FC> = (props) => { - const metadataColor = useJsonViewerStore(store => store.colorspace.base04) + const { base04: metadataColor } = useAtomValue(colorspaceAtom) const isArray = useMemo(() => Array.isArray(props.value), [props.value]) - const displayObjectSize = useJsonViewerStore(store => store.displayObjectSize) + const displayObjectSize = useAtomValue(displayObjectSizeAtom) const isEmptyValue = useMemo(() => getValueSize(props.value) === 0, [props.value]) const sizeOfValue = useMemo(() => inspectMetadata(props.value), [props.value]) @@ -111,12 +119,14 @@ function getIterator (value: any): value is Iterable { } export const ObjectType: FC> = (props) => { - const keyColor = useTextColor() - const borderColor = useJsonViewerStore(store => store.colorspace.base02) - const groupArraysAfterLength = useJsonViewerStore(store => store.groupArraysAfterLength) + const { + base02: borderColor, + base07: keyColor + } = useAtomValue(colorspaceAtom) + const groupArraysAfterLength = useAtomValue(groupArraysAfterLengthAtom) const isTrap = useIsCycleReference(props.path, props.value) - const [displayLength, setDisplayLength] = useState(useJsonViewerStore(store => store.maxDisplayLength)) - const objectSortKeys = useJsonViewerStore(store => store.objectSortKeys) + const [displayLength, setDisplayLength] = useState(useAtomValue(maxDisplayLengthAtom)) + const objectSortKeys = useAtomValue(objectSortKeysAtom) const elements = useMemo(() => { if (!props.inspect) { return null @@ -245,7 +255,7 @@ export const ObjectType: FC> = (props) => { objectSortKeys ]) const marginLeft = props.inspect ? 0.6 : 0 - const width = useJsonViewerStore(store => store.indentWidth) + const width = useAtomValue(indentWidthAtom) const indentWidth = props.inspect ? width - marginLeft : width const isEmptyValue = useMemo(() => getValueSize(props.value) === 0, [props.value]) if (isEmptyValue) { diff --git a/src/components/DataTypes/createEasyType.tsx b/src/components/DataTypes/createEasyType.tsx index 5d45b96e..4901f1db 100644 --- a/src/components/DataTypes/createEasyType.tsx +++ b/src/components/DataTypes/createEasyType.tsx @@ -1,8 +1,9 @@ import { InputBase } from '@mui/material' +import { useAtomValue } from 'jotai' import type { ChangeEventHandler, ComponentType, FC } from 'react' import { memo, useCallback } from 'react' -import { useJsonViewerStore } from '../../stores/JsonViewerStore' +import { colorspaceAtom, displayDataTypesAtom, onSelectAtom } from '../../state' import type { Colorspace } from '../../theme/base16' import type { DataItemProps, DataType, EditorProps } from '../../type' import { DataTypeLabel } from '../DataTypeLabel' @@ -18,13 +19,11 @@ export function createEasyType ( } ): Omit, 'is'> { const { fromString, colorKey, displayTypeLabel = true } = config - const Render = memo(renderValue) const EasyType: FC> = (props) => { - const storeDisplayDataTypes = useJsonViewerStore(store => store.displayDataTypes) - const color = useJsonViewerStore(store => store.colorspace[colorKey]) - const onSelect = useJsonViewerStore(store => store.onSelect) - + const storeDisplayDataTypes = useAtomValue(displayDataTypesAtom) + const color = useAtomValue(colorspaceAtom)[colorKey] + const onSelect = useAtomValue(onSelectAtom) return ( onSelect?.(props.path, props.value)} sx={{ color }}> {(displayTypeLabel && storeDisplayDataTypes) && } @@ -41,9 +40,8 @@ export function createEasyType ( Component: EasyType } } - const EasyTypeEditor: FC> = ({ value, setValue }) => { - const color = useJsonViewerStore(store => store.colorspace[colorKey]) + const color = useAtomValue(colorspaceAtom)[colorKey] return ( { - return useJsonViewerStore(store => store.colorspace.base07) -} diff --git a/src/hooks/useCopyToClipboard.ts b/src/hooks/useCopyToClipboard.ts index 31e4520b..70660868 100644 --- a/src/hooks/useCopyToClipboard.ts +++ b/src/hooks/useCopyToClipboard.ts @@ -1,7 +1,8 @@ import copyToClipboard from 'copy-to-clipboard' +import { useAtomValue } from 'jotai' import { useCallback, useRef, useState } from 'react' -import { useJsonViewerStore } from '../stores/JsonViewerStore' +import { onCopyAtom } from '../state' import type { JsonViewerOnCopy } from '../type' import { safeStringify } from '../utils' @@ -23,7 +24,7 @@ export function useClipboard ({ timeout = 2000 } = {}) { copyTimeout.current = window.setTimeout(() => setCopied(false), timeout) setCopied(value) }, [timeout]) - const onCopy = useJsonViewerStore(store => store.onCopy) + const onCopy = useAtomValue(onCopyAtom) const copy = useCallback((path, value: unknown) => { if (typeof onCopy === 'function') { diff --git a/src/hooks/useInspect.ts b/src/hooks/useInspect.ts index 48fcd450..22eda0d7 100644 --- a/src/hooks/useInspect.ts +++ b/src/hooks/useInspect.ts @@ -1,7 +1,5 @@ -import type { - Dispatch, - SetStateAction -} from 'react' +import { useAtomValue, useSetAtom } from 'jotai' +import { useAtomCallback } from 'jotai/utils' import { useCallback, useEffect, @@ -9,33 +7,46 @@ import { } from 'react' import { - useJsonViewerStore -} from '../stores/JsonViewerStore' + defaultInspectDepthAtom, + getInspectCacheAtomFamily, + setInspectCacheAtomFamily +} from '../state' import { useIsCycleReference } from './useIsCycleReference' export function useInspect (path: (string | number)[], value: any, nestedIndex?: number) { const depth = path.length const isTrap = useIsCycleReference(path, value) - const getInspectCache = useJsonViewerStore(store => store.getInspectCache) - const setInspectCache = useJsonViewerStore(store => store.setInspectCache) - const defaultInspectDepth = useJsonViewerStore(store => store.defaultInspectDepth) + const defaultInspectDepth = useAtomValue(defaultInspectDepthAtom) + + const getInspectCache = useAtomCallback( + useCallback((get, set, arg) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + useAtomValue(getInspectCacheAtomFamily(arg)) + }, []) + ) + const setInspectCache = useAtomCallback( + useCallback((get, set, arg) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + useSetAtom(setInspectCacheAtomFamily(arg)) + }, []) + ) useEffect(() => { - const inspect = getInspectCache(path, nestedIndex) + const inspect = getInspectCache({ path, nestedIndex }) if (inspect !== undefined) { return } if (nestedIndex !== undefined) { - setInspectCache(path, false, nestedIndex) + setInspectCache({ path, action: false, nestedIndex }) } else { // do not inspect when it is a cycle reference, otherwise there will have a loop const inspect = isTrap ? false : depth < defaultInspectDepth - setInspectCache(path, inspect) + setInspectCache({ path, inspect }) } - }, [defaultInspectDepth, depth, getInspectCache, isTrap, nestedIndex, path, setInspectCache]) - const [inspect, set] = useState(() => { - const shouldInspect = getInspectCache(path, nestedIndex) + }, [defaultInspectDepth, depth, isTrap, nestedIndex, path, getInspectCache, setInspectCache]) + const shouldInspect = useAtomValue(getInspectCacheAtomFamily({ path, nestedIndex })) + const [inspect, setOriginal] = useState(() => { if (shouldInspect !== undefined) { return shouldInspect } @@ -46,12 +57,15 @@ export function useInspect (path: (string | number)[], value: any, nestedIndex?: ? false : depth < defaultInspectDepth }) - const setInspect = useCallback>>((apply) => { - set((oldState) => { - const newState = typeof apply === 'boolean' ? apply : apply(oldState) - setInspectCache(path, newState, nestedIndex) - return newState - }) - }, [nestedIndex, path, setInspectCache]) + const setInspect = useAtomCallback( + useCallback((get, set, apply) => { + setOriginal((oldState) => { + const newState = typeof apply === 'boolean' ? apply : apply(oldState) + // eslint-disable-next-line react-hooks/rules-of-hooks + useSetAtom(setInspectCacheAtomFamily(apply)) + return newState + }) + }, []) + ) return [inspect, setInspect] as const } diff --git a/src/hooks/useIsCycleReference.ts b/src/hooks/useIsCycleReference.ts index 521d3396..1c2ebad7 100644 --- a/src/hooks/useIsCycleReference.ts +++ b/src/hooks/useIsCycleReference.ts @@ -1,11 +1,13 @@ +import { useAtomValue } from 'jotai' import { useMemo } from 'react' -import { useJsonViewerStore } from '../stores/JsonViewerStore' +import { valueAtom } from '../state' import { isCycleReference } from '../utils' export function useIsCycleReference (path: (string | number)[], value: any) { - const rootValue = useJsonViewerStore(store => store.value) + const rootValue = useAtomValue(valueAtom) return useMemo( () => isCycleReference(rootValue, path, value), - [path, value, rootValue]) + [path, value, rootValue] + ) } diff --git a/src/index.tsx b/src/index.tsx index 3fa2bde2..2a086638 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,24 +2,39 @@ import { createTheme, Paper, ThemeProvider } from '@mui/material' +import type { Atom } from 'jotai' +import { useAtom, useSetAtom } from 'jotai' +import { useAtomCallback } from 'jotai/utils' import type { FC, ReactElement } from 'react' -import { useCallback, useContext, useEffect, useMemo, useRef } from 'react' +import { useCallback, useEffect, useMemo, useRef } from 'react' import { DataKeyPair } from './components/DataKeyPair' import { useThemeDetector } from './hooks/useThemeDetector' +import { + colorspaceAtom, + displayDataTypesAtom, + displayObjectSizeAtom, + editableAtom, + enableClipboardAtom, + groupArraysAfterLengthAtom, + indentWidthAtom, + keyRendererAtom, + maxDisplayLengthAtom, + onChangeAtom, + onCopyAtom, + onSelectAtom, + registryTypesAtomFamily, + rootNameAtom, + setHoverAtomFamily, + valueAtom +} from './state' import { createJsonViewerStore, - JsonViewerStoreContext, - useJsonViewerStore + JsonViewerProvider } from './stores/JsonViewerStore' -import { - createTypeRegistryStore, - predefined, - TypeRegistryStoreContext, - useTypeRegistryStore -} from './stores/typeRegistry' +import { predefined } from './stores/typeRegistry' import { darkColorspace, lightColorspace } from './theme/base16' -import type { JsonViewerProps } from './type' +import type { JsonViewerProps, JsonViewerState } from './type' import { applyValue, createDataType, isCycleReference } from './utils' export { applyValue, createDataType, isCycleReference } @@ -27,73 +42,70 @@ export { applyValue, createDataType, isCycleReference } /** * @internal */ -function useSetIfNotUndefinedEffect ( - key: Key, - value: JsonViewerProps[Key] | undefined +function useSetIfNotUndefinedEffect ( + atom: Atom, + value: JsonViewerProps[keyof JsonViewerProps] | undefined ) { - const { setState } = useContext(JsonViewerStoreContext) + const setAtom = useSetAtom(atom) useEffect(() => { if (value !== undefined) { - setState({ - [key]: value - }) + setAtom(value) } - }, [key, value, setState]) + }, [value, setAtom]) } /** * @internal */ const JsonViewerInner: FC = (props) => { - const { setState } = useContext(JsonViewerStoreContext) - useSetIfNotUndefinedEffect('value', props.value) - useSetIfNotUndefinedEffect('editable', props.editable) - useSetIfNotUndefinedEffect('indentWidth', props.indentWidth) - useSetIfNotUndefinedEffect('onChange', props.onChange) - useSetIfNotUndefinedEffect('groupArraysAfterLength', props.groupArraysAfterLength) - useSetIfNotUndefinedEffect('keyRenderer', props.keyRenderer) - useSetIfNotUndefinedEffect('maxDisplayLength', props.maxDisplayLength) - useSetIfNotUndefinedEffect('enableClipboard', props.enableClipboard) - useSetIfNotUndefinedEffect('rootName', props.rootName) - useSetIfNotUndefinedEffect('displayDataTypes', props.displayDataTypes) - useSetIfNotUndefinedEffect('displayObjectSize', props.displayObjectSize) - useSetIfNotUndefinedEffect('onCopy', props.onCopy) - useSetIfNotUndefinedEffect('onSelect', props.onSelect) + const setColorspace = useSetAtom(colorspaceAtom) + useSetIfNotUndefinedEffect(valueAtom, props.value) + useSetIfNotUndefinedEffect(editableAtom, props.editable) + useSetIfNotUndefinedEffect(indentWidthAtom, props.indentWidth) + useSetIfNotUndefinedEffect(onChangeAtom, props.onChange) + useSetIfNotUndefinedEffect(groupArraysAfterLengthAtom, props.groupArraysAfterLength) + useSetIfNotUndefinedEffect(keyRendererAtom, props.keyRenderer) + useSetIfNotUndefinedEffect(maxDisplayLengthAtom, props.maxDisplayLength) + useSetIfNotUndefinedEffect(enableClipboardAtom, props.enableClipboard) + useSetIfNotUndefinedEffect(rootNameAtom, props.rootName) + useSetIfNotUndefinedEffect(displayDataTypesAtom, props.displayDataTypes) + useSetIfNotUndefinedEffect(displayObjectSizeAtom, props.displayObjectSize) + useSetIfNotUndefinedEffect(onCopyAtom, props.onCopy) + useSetIfNotUndefinedEffect(onSelectAtom, props.onSelect) useEffect(() => { if (props.theme === 'light') { - setState({ - colorspace: lightColorspace - }) + setColorspace(lightColorspace) } else if (props.theme === 'dark') { - setState({ - colorspace: darkColorspace - }) + setColorspace(darkColorspace) } else if (typeof props.theme === 'object') { - setState({ - colorspace: props.theme - }) + setColorspace(props.theme) } - }, [setState, props.theme]) + }, [props.theme]) const onceRef = useRef(true) const predefinedTypes = useMemo(() => predefined(), []) - const registerTypes = useTypeRegistryStore(store => store.registerTypes) if (onceRef.current) { - const allTypes = props.valueTypes - ? [...predefinedTypes, ...props.valueTypes] - : [...predefinedTypes] - registerTypes(allTypes) + const allTypes = [...predefinedTypes] + props.valueTypes?.forEach(type => { + allTypes.push(type) + }) + useSetAtom(registryTypesAtomFamily(allTypes)) onceRef.current = false } useEffect(() => { - const allTypes = props.valueTypes - ? [...predefinedTypes, ...props.valueTypes] - : [...predefinedTypes] - registerTypes(allTypes) - }, [props.valueTypes, predefinedTypes, registerTypes]) + const allTypes = [...predefinedTypes] + props.valueTypes?.forEach(type => { + allTypes.push(type) + }) + useSetAtom(registryTypesAtomFamily(allTypes)) + }, [predefinedTypes, props.valueTypes]) - const value = useJsonViewerStore(store => store.value) - const setHover = useJsonViewerStore(store => store.setHover) - const onMouseLeave = useCallback(() => setHover(null), [setHover]) + const value = useAtom(valueAtom) + const setHover = useAtomCallback( + useCallback((get, set, arg) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + useSetAtom(setHoverAtomFamily(arg)) + }, []) + ) return ( = (props) => { userSelect: 'none', contentVisibility: 'auto' }} - onMouseLeave={onMouseLeave} + onMouseLeave={() => setHover(null)} > (props: JsonViewerProps createJsonViewerStore(props), []) - const typeRegistryStore = useMemo(() => createTypeRegistryStore(), []) - return ( - - - - - + {/* merged with JsonViewerProvider because registryAtom isn't set */} + + + + {/* */} ) } diff --git a/src/state.ts b/src/state.ts new file mode 100644 index 00000000..808e30eb --- /dev/null +++ b/src/state.ts @@ -0,0 +1,60 @@ +import { atom } from 'jotai' +import { atomFamily } from 'jotai/utils' + +import type { JsonViewerState, TypeRegistryState } from './type' + +export const valueAtom = atom(undefined) +export const editableAtom = atom(undefined) +export const indentWidthAtom = atom(undefined) +export const onChangeAtom = atom(undefined) +export const groupArraysAfterLengthAtom = atom(undefined) +export const keyRendererAtom = atom(undefined) +export const maxDisplayLengthAtom = atom(undefined) +export const enableClipboardAtom = atom(undefined) +export const rootNameAtom = atom(undefined) +export const displayDataTypesAtom = atom(undefined) +export const displayObjectSizeAtom = atom(undefined) +export const onCopyAtom = atom(undefined) +export const onSelectAtom = atom(undefined) +export const colorspaceAtom = atom(undefined) +export const collapseStringsAfterLengthAtom = atom(undefined) +export const defaultInspectDepthAtom = atom(undefined) +export const objectSortKeysAtom = atom(undefined) +export const quotesOnKeysAtom = atom(undefined) +export const inspectCacheAtom = atom(undefined) +export const hoverPathAtom = atom(undefined) +export const registryAtom = atom(undefined) + +export const getInspectCacheAtomFamily = atomFamily(({ path, nestedIndex }) => { + const target = nestedIndex === undefined + ? path.join('.') + : `${path.join('.')}[${nestedIndex}]nt` + return atom((get) => get(inspectCacheAtom)[target]) +}) +export const setInspectCacheAtomFamily = atomFamily(({ path, action, nestedIndex }) => atom((get, set) => { + const target = nestedIndex === undefined + ? path.join('.') + : `${path.join('.')}[${nestedIndex}]nt` + const inspectCache = get(inspectCacheAtom) + return set(inspectCacheAtom, { + ...inspectCache, + [target]: typeof action === 'function' + ? action(inspectCache[target]) + : action + }) +})) +export const setHoverAtomFamily = atomFamily(({ path, nestedIndex }) => atom( + (get, set) => set(hoverPathAtom, path + ? { path, nestedIndex } + : null + ) +)) +export const registryTypesAtomFamily = atomFamily((setState) => atom( + (get, set) => { + if (typeof setState === 'function') { + set(registryAtom, setState) + return + } + return setState + } +)) diff --git a/src/stores/JsonViewerStore.ts b/src/stores/JsonViewerStore.ts index 92f3a85f..94799368 100644 --- a/src/stores/JsonViewerStore.ts +++ b/src/stores/JsonViewerStore.ts @@ -1,115 +1,71 @@ +import type { Atom } from 'jotai' import type { SetStateAction } from 'react' -import { createContext, useContext } from 'react' -import type { StoreApi } from 'zustand' -import { create, useStore } from 'zustand' -import type { - JsonViewerOnChange, - JsonViewerOnCopy, - JsonViewerOnSelect, - JsonViewerProps, - Path -} from '..' -import type { Colorspace } from '../theme/base16' +import type { JsonViewerProps, Path } from '..' +import { + collapseStringsAfterLengthAtom, + colorspaceAtom, + defaultInspectDepthAtom, + displayDataTypesAtom, + displayObjectSizeAtom, + editableAtom, + enableClipboardAtom, + groupArraysAfterLengthAtom, + hoverPathAtom, + indentWidthAtom, + inspectCacheAtom, + keyRendererAtom, + maxDisplayLengthAtom, + objectSortKeysAtom, + onChangeAtom, + onCopyAtom, + onSelectAtom, + quotesOnKeysAtom, + registryAtom, + rootNameAtom, + valueAtom +} from '../state' import { lightColorspace } from '../theme/base16' -import type { JsonViewerKeyRenderer } from '../type' +import type { JsonViewerKeyRenderer, JsonViewerState } from '../type' + +export { Provider as JsonViewerProvider } from 'jotai' const DefaultKeyRenderer: JsonViewerKeyRenderer = () => null DefaultKeyRenderer.when = () => false -export type JsonViewerState = { - inspectCache: Record - hoverPath: { path: Path; nestedIndex?: number } | null - indentWidth: number - groupArraysAfterLength: number - enableClipboard: boolean - maxDisplayLength: number - defaultInspectDepth: number - collapseStringsAfterLength: number - objectSortKeys: boolean | ((a: string, b: string) => number) - quotesOnKeys: boolean - colorspace: Colorspace - editable: boolean | ((path: Path, currentValue: U) => boolean) - displayDataTypes: boolean - rootName: false | string - value: T - onChange: JsonViewerOnChange - onCopy: JsonViewerOnCopy | undefined - onSelect: JsonViewerOnSelect | undefined - keyRenderer: JsonViewerKeyRenderer - displayObjectSize: boolean - +export type JsonViewerActions = { getInspectCache: (path: Path, nestedIndex?: number) => boolean setInspectCache: ( path: Path, action: SetStateAction, nestedIndex?: number) => void setHover: (path: Path | null, nestedIndex?: number) => void } -export const createJsonViewerStore = (props: JsonViewerProps) => { - return create()((set, get) => ({ - // provided by user - enableClipboard: props.enableClipboard ?? true, - indentWidth: props.indentWidth ?? 3, - groupArraysAfterLength: props.groupArraysAfterLength ?? 100, - collapseStringsAfterLength: - (props.collapseStringsAfterLength === false) - ? Number.MAX_VALUE - : props.collapseStringsAfterLength ?? 50, - maxDisplayLength: props.maxDisplayLength ?? 30, - rootName: props.rootName ?? 'root', - onChange: props.onChange ?? (() => {}), - onCopy: props.onCopy ?? undefined, - onSelect: props.onSelect ?? undefined, - keyRenderer: props.keyRenderer ?? DefaultKeyRenderer, - editable: props.editable ?? false, - defaultInspectDepth: props.defaultInspectDepth ?? 5, - objectSortKeys: props.objectSortKeys ?? false, - quotesOnKeys: props.quotesOnKeys ?? true, - displayDataTypes: props.displayDataTypes ?? true, - // internal state - inspectCache: {}, - hoverPath: null, - colorspace: lightColorspace, - value: props.value, - displayObjectSize: props.displayObjectSize ?? true, - - getInspectCache: (path, nestedIndex) => { - const target = nestedIndex !== undefined - ? path.join('.') + - `[${nestedIndex}]nt` - : path.join('.') - return get().inspectCache[target] - }, - setInspectCache: (path, action, nestedIndex) => { - const target = nestedIndex !== undefined - ? path.join('.') + - `[${nestedIndex}]nt` - : path.join('.') - set(state => ({ - inspectCache: { - ...state.inspectCache, - [target]: typeof action === 'function' - ? action( - state.inspectCache[target]) - : action - } - })) - }, - setHover: (path, nestedIndex) => { - set({ - hoverPath: path - ? ({ path, nestedIndex }) - : null - }) - } - })) -} - -export const JsonViewerStoreContext = createContext>(undefined) - -export const JsonViewerProvider = JsonViewerStoreContext.Provider - -export const useJsonViewerStore = (selector: (state: JsonViewerState) => U, equalityFn?: (a: U, b: U) => boolean) => { - const store = useContext(JsonViewerStoreContext) - return useStore(store, selector, equalityFn) -} +export const createJsonViewerStore = (props: JsonViewerProps): Iterable, JsonViewerState[keyof JsonViewerState]]> => [ + // provided by user + [enableClipboardAtom, props.enableClipboard ?? true], + [indentWidthAtom, props.indentWidth ?? 3], + [groupArraysAfterLengthAtom, props.groupArraysAfterLength ?? 100], + [collapseStringsAfterLengthAtom, + (props.collapseStringsAfterLength === false) + ? Number.MAX_VALUE + : props.collapseStringsAfterLength ?? 50], + [maxDisplayLengthAtom, props.maxDisplayLength ?? 30], + [rootNameAtom, props.rootName ?? 'root'], + [onChangeAtom, props.onChange ?? (() => {})], + [onCopyAtom, props.onCopy ?? undefined], + [onSelectAtom, props.onSelect ?? undefined], + [keyRendererAtom, props.keyRenderer ?? DefaultKeyRenderer], + [editableAtom, props.editable ?? false], + [defaultInspectDepthAtom, props.defaultInspectDepth ?? 5], + [objectSortKeysAtom, props.objectSortKeys ?? false], + [quotesOnKeysAtom, props.quotesOnKeys ?? true], + [displayDataTypesAtom, props.displayDataTypes ?? true], + // internal state + [inspectCacheAtom, {}], + [hoverPathAtom, null], + [colorspaceAtom, lightColorspace], + [valueAtom, props.value], + [displayObjectSizeAtom, props.displayObjectSize ?? true], + [registryAtom, []] // moved from JsonViewerProvider +] +export type JsonViewerStore = ReturnType diff --git a/src/stores/typeRegistry.tsx b/src/stores/typeRegistry.tsx index acebee84..07d77b89 100644 --- a/src/stores/typeRegistry.tsx +++ b/src/stores/typeRegistry.tsx @@ -1,8 +1,6 @@ import { Box } from '@mui/material' -import type { SetStateAction } from 'react' -import { createContext, memo, useContext, useMemo, useState } from 'react' -import type { StoreApi } from 'zustand' -import { createStore, useStore } from 'zustand' +import { Atom, useAtomValue } from 'jotai' +import { memo, useMemo, useState } from 'react' import { createEasyType } from '../components/DataTypes/createEasyType' import { @@ -15,38 +13,18 @@ import { PostObjectType, PreObjectType } from '../components/DataTypes/Object' -import type { DataItemProps, DataType, Path } from '../type' -import { useJsonViewerStore } from './JsonViewerStore' - -type TypeRegistryState = { - registry: DataType[] - - registerTypes: (setState: SetStateAction[]>) => void -} - -export const createTypeRegistryStore = () => { - return createStore()((set) => ({ - registry: [], - - registerTypes: (setState) => { - set(state => ({ - registry: - typeof setState === 'function' - ? setState(state.registry) - : setState - })) - } - })) -} - -export const TypeRegistryStoreContext = createContext>(undefined) +import { + collapseStringsAfterLengthAtom, + colorspaceAtom, + registryAtom +} from '../state' +import type { DataItemProps, DataType, Path, TypeRegistryState } from '../type' -export const TypeRegistryProvider = TypeRegistryStoreContext.Provider +export { Provider as TypeRegistryProvider } from 'jotai' -export const useTypeRegistryStore = (selector: (state: TypeRegistryState) => U, equalityFn?: (a: U, b: U) => boolean) => { - const store = useContext(TypeRegistryStoreContext) - return useStore(store, selector, equalityFn) -} +export const createTypeRegistryStore = (): Iterable, TypeRegistryState[keyof TypeRegistryState]]> => [ + [registryAtom, []] // moved to createJsonViewerStore (JsonViewerProvider) +] const objectType: DataType = { is: (value) => typeof value === 'object', @@ -56,7 +34,10 @@ const objectType: DataType = { } export function matchTypeComponents ( - value: Value, path: Path, registry: TypeRegistryState['registry']): DataType { + value: Value, + path: Path, + registry: TypeRegistryState['registry'] +): DataType { let potential: DataType | undefined for (const T of registry) { if (T.is(value, path)) { @@ -77,7 +58,7 @@ export function matchTypeComponents ( } export function useTypeComponents (value: unknown, path: Path) { - const registry = useTypeRegistryStore(store => store.registry) + const registry = useAtomValue(registryAtom) return useMemo(() => matchTypeComponents(value, path, registry), [value, path, registry]) } @@ -149,8 +130,7 @@ export function predefined (): DataType[] { ...createEasyType( 'null', () => { - const backgroundColor = useJsonViewerStore( - store => store.colorspace.base02) + const { base02: backgroundColor } = useAtomValue(colorspaceAtom) return ( [] { ...createEasyType( 'undefined', () => { - const backgroundColor = useJsonViewerStore( - store => store.colorspace.base02) + const { base02: backgroundColor } = useAtomValue(colorspaceAtom) return ( [] { 'string', (props) => { const [showRest, setShowRest] = useState(false) - const collapseStringsAfterLength = useJsonViewerStore(store => store.collapseStringsAfterLength) + const collapseStringsAfterLength = useAtomValue(collapseStringsAfterLengthAtom) const value = showRest ? props.value : props.value.slice(0, collapseStringsAfterLength) @@ -259,8 +238,7 @@ export function predefined (): DataType[] { ...createEasyType( 'NaN', () => { - const backgroundColor = useJsonViewerStore( - store => store.colorspace.base02) + const { base02: backgroundColor } = useAtomValue(colorspaceAtom) return ( ( path: Path, oldValue: U, - newValue: U /*, type: ChangeType */) => void + newValue: U /*, type: ChangeType */ +) => void /** * @param path path to the target value @@ -54,6 +55,10 @@ export type DataType = { PostComponent?: ComponentType> } +export type TypeRegistryState = { + registry: DataType[] +} + export interface JsonViewerKeyRenderer extends FC { when (props: DataItemProps): boolean } @@ -167,3 +172,26 @@ export type JsonViewerProps = { */ displayObjectSize?: boolean } + +export type JsonViewerState = { + inspectCache: Record + hoverPath: { path: Path; nestedIndex?: number } | null + indentWidth: number + groupArraysAfterLength: number + enableClipboard: boolean + maxDisplayLength: number + defaultInspectDepth: number + collapseStringsAfterLength: number + objectSortKeys: boolean | ((a: string, b: string) => number) + quotesOnKeys: boolean + colorspace: Colorspace + editable: boolean | ((path: Path, currentValue: U) => boolean) + displayDataTypes: boolean + rootName: false | string + value: T + onChange: JsonViewerOnChange + onCopy: JsonViewerOnCopy | undefined + onSelect: JsonViewerOnCopy | undefined + keyRenderer: JsonViewerKeyRenderer + displayObjectSize: boolean +} diff --git a/tsconfig.json b/tsconfig.json index fac4e662..fdbf4028 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES5", + "target": "ESNext", "outDir": "./dist/out/", "rootDir": "./src/", "jsx": "react-jsx", diff --git a/yarn.lock b/yarn.lock index db551a45..5e8402b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2003,6 +2003,7 @@ __metadata: eslint-plugin-unused-imports: ^2.0.0 expect-type: ^0.15.0 husky: ^8.0.3 + jotai: ^1.13.0 jsdom: ^21.1.1 lint-staged: ^13.2.0 pinst: ^3.0.0 @@ -2016,7 +2017,6 @@ __metadata: typescript: ^5.0.2 vite: ^4.2.0 vitest: ^0.29.3 - zustand: ^4.3.6 peerDependencies: react: ^17 || ^18 react-dom: ^17 || ^18 @@ -5815,6 +5815,49 @@ __metadata: languageName: node linkType: hard +"jotai@npm:^1.13.0": + version: 1.13.0 + resolution: "jotai@npm:1.13.0" + peerDependencies: + "@babel/core": "*" + "@babel/template": "*" + jotai-devtools: "*" + jotai-immer: "*" + jotai-optics: "*" + jotai-redux: "*" + jotai-tanstack-query: "*" + jotai-urql: "*" + jotai-valtio: "*" + jotai-xstate: "*" + jotai-zustand: "*" + react: ">=16.8" + peerDependenciesMeta: + "@babel/core": + optional: true + "@babel/template": + optional: true + jotai-devtools: + optional: true + jotai-immer: + optional: true + jotai-optics: + optional: true + jotai-redux: + optional: true + jotai-tanstack-query: + optional: true + jotai-urql: + optional: true + jotai-valtio: + optional: true + jotai-xstate: + optional: true + jotai-zustand: + optional: true + checksum: ccb301a98c52a32d5ef377b2a479aa67512548a1faacc4b821024c92211721785bbe5f5d035e5f206e5058d2fb4a4d83291eca43c71ea2f3bf3d66f4482d7a31 + languageName: node + linkType: hard + "js-sdsl@npm:^4.1.4": version: 4.2.0 resolution: "js-sdsl@npm:4.2.0" @@ -9950,15 +9993,6 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:1.2.0": - version: 1.2.0 - resolution: "use-sync-external-store@npm:1.2.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 5c639e0f8da3521d605f59ce5be9e094ca772bd44a4ce7322b055a6f58eeed8dda3c94cabd90c7a41fb6fa852210092008afe48f7038792fd47501f33299116a - languageName: node - linkType: hard - "util-deprecate@npm:^1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -10518,23 +10552,6 @@ __metadata: languageName: node linkType: hard -"zustand@npm:^4.3.6": - version: 4.3.6 - resolution: "zustand@npm:4.3.6" - dependencies: - use-sync-external-store: 1.2.0 - peerDependencies: - immer: ">=9.0" - react: ">=16.8" - peerDependenciesMeta: - immer: - optional: true - react: - optional: true - checksum: 4d3cec03526f04ff3de6dc45b6f038c47f091836af9660fbf5f682cae1628221102882df20e4048dfe699a43f67424e5d6afc1116f3838a80eea5dd4f95ddaed - languageName: node - linkType: hard - "zwitch@npm:^2.0.0": version: 2.0.2 resolution: "zwitch@npm:2.0.2" From e9bb7ac876365ea15f9a540bbc4de623ac3cdd72 Mon Sep 17 00:00:00 2001 From: rtritto Date: Sun, 15 Jan 2023 03:51:42 +0100 Subject: [PATCH 2/7] Remove AtomFamily --- src/components/DataKeyPair.tsx | 15 ++++---- src/hooks/useInspect.ts | 43 ++++++++--------------- src/index.tsx | 27 +++++++-------- src/state.ts | 63 +++++++++++++++++++--------------- 4 files changed, 69 insertions(+), 79 deletions(-) diff --git a/src/components/DataKeyPair.tsx b/src/components/DataKeyPair.tsx index e16d9695..c7e8c303 100644 --- a/src/components/DataKeyPair.tsx +++ b/src/components/DataKeyPair.tsx @@ -1,6 +1,5 @@ import { Box } from '@mui/material' import { useAtomValue, useSetAtom } from 'jotai' -import { useAtomCallback } from 'jotai/utils' import type { ComponentProps, FC, MouseEvent } from 'react' import { useCallback, useMemo, useState } from 'react' @@ -15,7 +14,7 @@ import { onChangeAtom, quotesOnKeysAtom, rootNameAtom, - setHoverAtomFamily, + setHoverAtom, valueAtom } from '../state' import { useTypeComponents } from '../stores/typeRegistry' @@ -78,12 +77,7 @@ export const DataKeyPair: FC = (props) => { (value, index) => value === hoverPath.path[index] && nestedIndex === hoverPath.nestedIndex) }, [hoverPath, path, nestedIndex]) - const setHover = useAtomCallback( - useCallback((get, set, arg) => { - // eslint-disable-next-line react-hooks/rules-of-hooks - useSetAtom(setHoverAtomFamily(arg)) - }, []) - ) + const setHover = useSetAtom(setHoverAtom) const root = useAtomValue(valueAtom) const [inspect, setInspect] = useInspect(path, value, nestedIndex) const [editing, setEditing] = useState(false) @@ -192,7 +186,10 @@ export const DataKeyPair: FC = (props) => { className='data-key-pair' data-testid={'data-key-pair' + path.join('.')} sx={{ userSelect: 'text' }} - onMouseEnter={() => setHover({ path, nestedIndex })} + onMouseEnter={ + useCallback(() => setHover({ path, nestedIndex }), + [setHover, path, nestedIndex]) + } > { - // eslint-disable-next-line react-hooks/rules-of-hooks - useAtomValue(getInspectCacheAtomFamily(arg)) - }, []) - ) - const setInspectCache = useAtomCallback( - useCallback((get, set, arg) => { - // eslint-disable-next-line react-hooks/rules-of-hooks - useSetAtom(setInspectCacheAtomFamily(arg)) - }, []) - ) + const getInspectCache = useSetAtom(getInspectCacheAtom) + const setInspectCache = useSetAtom(setInspectCacheAtom) useEffect(() => { const inspect = getInspectCache({ path, nestedIndex }) if (inspect !== undefined) { @@ -45,8 +35,8 @@ export function useInspect (path: (string | number)[], value: any, nestedIndex?: setInspectCache({ path, inspect }) } }, [defaultInspectDepth, depth, isTrap, nestedIndex, path, getInspectCache, setInspectCache]) - const shouldInspect = useAtomValue(getInspectCacheAtomFamily({ path, nestedIndex })) - const [inspect, setOriginal] = useState(() => { + const [inspect, set] = useState(() => { + const shouldInspect = getInspectCache({ path, nestedIndex }) if (shouldInspect !== undefined) { return shouldInspect } @@ -57,15 +47,12 @@ export function useInspect (path: (string | number)[], value: any, nestedIndex?: ? false : depth < defaultInspectDepth }) - const setInspect = useAtomCallback( - useCallback((get, set, apply) => { - setOriginal((oldState) => { - const newState = typeof apply === 'boolean' ? apply : apply(oldState) - // eslint-disable-next-line react-hooks/rules-of-hooks - useSetAtom(setInspectCacheAtomFamily(apply)) - return newState - }) - }, []) - ) + const setInspect = useCallback>>((apply) => { + set((oldState) => { + const newState = typeof apply === 'boolean' ? apply : apply(oldState) + setInspectCache({ path, newState, nestedIndex }) + return newState + }) + }, [nestedIndex, path, setInspectCache]) return [inspect, setInspect] as const } diff --git a/src/index.tsx b/src/index.tsx index 2a086638..ace587fa 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,7 +4,6 @@ import { } from '@mui/material' import type { Atom } from 'jotai' import { useAtom, useSetAtom } from 'jotai' -import { useAtomCallback } from 'jotai/utils' import type { FC, ReactElement } from 'react' import { useCallback, useEffect, useMemo, useRef } from 'react' @@ -23,9 +22,9 @@ import { onChangeAtom, onCopyAtom, onSelectAtom, - registryTypesAtomFamily, + registryTypesAtom, rootNameAtom, - setHoverAtomFamily, + setHoverAtom, valueAtom } from './state' import { @@ -80,15 +79,16 @@ const JsonViewerInner: FC = (props) => { } else if (typeof props.theme === 'object') { setColorspace(props.theme) } - }, [props.theme]) + }, [props.theme, setColorspace]) const onceRef = useRef(true) const predefinedTypes = useMemo(() => predefined(), []) + const registerTypes = useSetAtom(registryTypesAtom) if (onceRef.current) { const allTypes = [...predefinedTypes] props.valueTypes?.forEach(type => { allTypes.push(type) }) - useSetAtom(registryTypesAtomFamily(allTypes)) + registerTypes(allTypes) onceRef.current = false } useEffect(() => { @@ -96,16 +96,11 @@ const JsonViewerInner: FC = (props) => { props.valueTypes?.forEach(type => { allTypes.push(type) }) - useSetAtom(registryTypesAtomFamily(allTypes)) - }, [predefinedTypes, props.valueTypes]) + registerTypes(allTypes) + }, [predefinedTypes, props.valueTypes, registerTypes]) const value = useAtom(valueAtom) - const setHover = useAtomCallback( - useCallback((get, set, arg) => { - // eslint-disable-next-line react-hooks/rules-of-hooks - useSetAtom(setHoverAtomFamily(arg)) - }, []) - ) + const setHover = useSetAtom(setHoverAtom) return ( = (props) => { userSelect: 'none', contentVisibility: 'auto' }} - onMouseLeave={() => setHover(null)} + onMouseLeave={ + useCallback(() => { + setHover(null) + }, [setHover]) + } > (undefined) export const registryAtom = atom(undefined) -export const getInspectCacheAtomFamily = atomFamily(({ path, nestedIndex }) => { - const target = nestedIndex === undefined - ? path.join('.') - : `${path.join('.')}[${nestedIndex}]nt` - return atom((get) => get(inspectCacheAtom)[target]) -}) -export const setInspectCacheAtomFamily = atomFamily(({ path, action, nestedIndex }) => atom((get, set) => { - const target = nestedIndex === undefined - ? path.join('.') - : `${path.join('.')}[${nestedIndex}]nt` - const inspectCache = get(inspectCacheAtom) - return set(inspectCacheAtom, { - ...inspectCache, - [target]: typeof action === 'function' - ? action(inspectCache[target]) - : action - }) -})) -export const setHoverAtomFamily = atomFamily(({ path, nestedIndex }) => atom( - (get, set) => set(hoverPathAtom, path +export const getInspectCacheAtom = atom( + (get) => get(inspectCacheAtom), + (get, _set, { path, nestedIndex }) => { + const target = nestedIndex === undefined + ? path.join('.') + : `${path.join('.')}[${nestedIndex}]nt` + return get(inspectCacheAtom)[target] + } +) +export const setInspectCacheAtom = atom( + (get) => get(inspectCacheAtom), + (get, set, { path, action, nestedIndex }) => { + const target = nestedIndex === undefined + ? path.join('.') + : `${path.join('.')}[${nestedIndex}]nt` + const inspectCache = get(inspectCacheAtom) + return set(inspectCacheAtom, { + ...inspectCache, + [target]: typeof action === 'function' + ? action(inspectCache[target]) + : action + }) + } +) +export const setHoverAtom = atom( + (get) => get(hoverPathAtom), + (_get, set, { path, nestedIndex }) => set(hoverPathAtom, path ? { path, nestedIndex } : null ) -)) -export const registryTypesAtomFamily = atomFamily((setState) => atom( - (get, set) => { +) + +export const registryTypesAtom = atom( + (get) => get(registryAtom), + (get, set, setState) => { if (typeof setState === 'function') { - set(registryAtom, setState) - return + return setState(get(registryAtom)) } - return setState + return set(registryAtom, setState) } -)) +) From ba22b3a655703716ba99a63ed296627e5671ef67 Mon Sep 17 00:00:00 2001 From: rtritto Date: Sun, 15 Jan 2023 18:15:55 +0100 Subject: [PATCH 3/7] Move initial values from Provider to default atom --- src/state.ts | 9 +++++---- src/stores/JsonViewerStore.ts | 11 +---------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/state.ts b/src/state.ts index 1799b689..89112c25 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,5 +1,6 @@ import { atom } from 'jotai' +import { lightColorspace } from './theme/base16' import type { JsonViewerState, TypeRegistryState } from './type' export const valueAtom = atom(undefined) @@ -15,14 +16,14 @@ export const displayDataTypesAtom = atom(undefined) export const onCopyAtom = atom(undefined) export const onSelectAtom = atom(undefined) -export const colorspaceAtom = atom(undefined) +export const colorspaceAtom = atom(lightColorspace) export const collapseStringsAfterLengthAtom = atom(undefined) export const defaultInspectDepthAtom = atom(undefined) export const objectSortKeysAtom = atom(undefined) export const quotesOnKeysAtom = atom(undefined) -export const inspectCacheAtom = atom(undefined) -export const hoverPathAtom = atom(undefined) -export const registryAtom = atom(undefined) +export const inspectCacheAtom = atom({}) +export const hoverPathAtom = atom(null) +export const registryAtom = atom([]) export const getInspectCacheAtom = atom( (get) => get(inspectCacheAtom), diff --git a/src/stores/JsonViewerStore.ts b/src/stores/JsonViewerStore.ts index 94799368..cd8e6a81 100644 --- a/src/stores/JsonViewerStore.ts +++ b/src/stores/JsonViewerStore.ts @@ -4,16 +4,13 @@ import type { SetStateAction } from 'react' import type { JsonViewerProps, Path } from '..' import { collapseStringsAfterLengthAtom, - colorspaceAtom, defaultInspectDepthAtom, displayDataTypesAtom, displayObjectSizeAtom, editableAtom, enableClipboardAtom, groupArraysAfterLengthAtom, - hoverPathAtom, indentWidthAtom, - inspectCacheAtom, keyRendererAtom, maxDisplayLengthAtom, objectSortKeysAtom, @@ -21,11 +18,9 @@ import { onCopyAtom, onSelectAtom, quotesOnKeysAtom, - registryAtom, rootNameAtom, valueAtom } from '../state' -import { lightColorspace } from '../theme/base16' import type { JsonViewerKeyRenderer, JsonViewerState } from '../type' export { Provider as JsonViewerProvider } from 'jotai' @@ -61,11 +56,7 @@ export const createJsonViewerStore = (props: JsonViewerProps): [quotesOnKeysAtom, props.quotesOnKeys ?? true], [displayDataTypesAtom, props.displayDataTypes ?? true], // internal state - [inspectCacheAtom, {}], - [hoverPathAtom, null], - [colorspaceAtom, lightColorspace], [valueAtom, props.value], - [displayObjectSizeAtom, props.displayObjectSize ?? true], - [registryAtom, []] // moved from JsonViewerProvider + [displayObjectSizeAtom, props.displayObjectSize ?? true] ] export type JsonViewerStore = ReturnType From 7c33b1c0f23ef032a1f40ab6aaa378a0a2434681 Mon Sep 17 00:00:00 2001 From: rtritto Date: Thu, 19 Jan 2023 20:46:40 +0100 Subject: [PATCH 4/7] Fix inspectCache atom --- package.json | 1 + src/hooks/useInspect.ts | 19 +++++++++++-------- src/state.ts | 20 +++++++++++--------- src/type.ts | 7 ++++++- yarn.lock | 1 + 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index be30ad5b..17bd0c9f 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@emotion/styled": "^11.10.6", "@mui/material": "^5.11.13", "copy-to-clipboard": "^3.3.3", + "fast-deep-equal": "^3.1.3", "jotai": "^1.13.0" }, "lint-staged": { diff --git a/src/hooks/useInspect.ts b/src/hooks/useInspect.ts index 80471122..fc37e511 100644 --- a/src/hooks/useInspect.ts +++ b/src/hooks/useInspect.ts @@ -12,17 +12,21 @@ import { getInspectCacheAtom, setInspectCacheAtom } from '../state' +import type { HostPath, JsonViewerProps } from '../type' import { useIsCycleReference } from './useIsCycleReference' -export function useInspect (path: (string | number)[], value: any, nestedIndex?: number) { +export function useInspect ( + path: HostPath['path'], + value: JsonViewerProps['value'], + nestedIndex?: HostPath['nestedIndex'] +) { const depth = path.length const isTrap = useIsCycleReference(path, value) const defaultInspectDepth = useAtomValue(defaultInspectDepthAtom) - const getInspectCache = useSetAtom(getInspectCacheAtom) + const inspectCache = useAtomValue(getInspectCacheAtom({ path, nestedIndex })) const setInspectCache = useSetAtom(setInspectCacheAtom) useEffect(() => { - const inspect = getInspectCache({ path, nestedIndex }) - if (inspect !== undefined) { + if (inspectCache !== undefined) { return } if (nestedIndex !== undefined) { @@ -34,11 +38,10 @@ export function useInspect (path: (string | number)[], value: any, nestedIndex?: : depth < defaultInspectDepth setInspectCache({ path, inspect }) } - }, [defaultInspectDepth, depth, isTrap, nestedIndex, path, getInspectCache, setInspectCache]) + }, [defaultInspectDepth, depth, isTrap, nestedIndex, path, inspectCache, setInspectCache]) const [inspect, set] = useState(() => { - const shouldInspect = getInspectCache({ path, nestedIndex }) - if (shouldInspect !== undefined) { - return shouldInspect + if (inspectCache !== undefined) { + return inspectCache } if (nestedIndex !== undefined) { return false diff --git a/src/state.ts b/src/state.ts index 89112c25..71b57d7e 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,4 +1,6 @@ +import deepEqual from 'fast-deep-equal' import { atom } from 'jotai' +import { atomFamily } from 'jotai/utils' import { lightColorspace } from './theme/base16' import type { JsonViewerState, TypeRegistryState } from './type' @@ -21,19 +23,20 @@ export const collapseStringsAfterLengthAtom = atom(undefined) export const objectSortKeysAtom = atom(undefined) export const quotesOnKeysAtom = atom(undefined) -export const inspectCacheAtom = atom({}) +export const inspectCacheAtom = atom({}) export const hoverPathAtom = atom(null) export const registryAtom = atom([]) -export const getInspectCacheAtom = atom( - (get) => get(inspectCacheAtom), - (get, _set, { path, nestedIndex }) => { +// TODO check: if memory leaks, add to last line of useEffect: +// return () => { atomFamily.remove ... // Anything in here is fired on component unmount } +export const getInspectCacheAtom = atomFamily(({ path, nestedIndex }) => atom( + (get) => { const target = nestedIndex === undefined ? path.join('.') : `${path.join('.')}[${nestedIndex}]nt` return get(inspectCacheAtom)[target] } -) +), deepEqual) export const setInspectCacheAtom = atom( (get) => get(inspectCacheAtom), (get, set, { path, action, nestedIndex }) => { @@ -51,10 +54,9 @@ export const setInspectCacheAtom = atom( ) export const setHoverAtom = atom( (get) => get(hoverPathAtom), - (_get, set, { path, nestedIndex }) => set(hoverPathAtom, path - ? { path, nestedIndex } - : null - ) + (_get, set, { path, nestedIndex }) => { + set(hoverPathAtom, path ? { path, nestedIndex } : null) + } ) export const registryTypesAtom = atom( diff --git a/src/type.ts b/src/type.ts index 85636d7f..7898d9fc 100644 --- a/src/type.ts +++ b/src/type.ts @@ -173,9 +173,14 @@ export type JsonViewerProps = { displayObjectSize?: boolean } +export type HostPath = { + path: Path + nestedIndex?: number +} + export type JsonViewerState = { inspectCache: Record - hoverPath: { path: Path; nestedIndex?: number } | null + hoverPath: HostPath | null indentWidth: number groupArraysAfterLength: number enableClipboard: boolean diff --git a/yarn.lock b/yarn.lock index 5e8402b7..a7994aa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2002,6 +2002,7 @@ __metadata: eslint-plugin-simple-import-sort: ^10.0.0 eslint-plugin-unused-imports: ^2.0.0 expect-type: ^0.15.0 + fast-deep-equal: ^3.1.3 husky: ^8.0.3 jotai: ^1.13.0 jsdom: ^21.1.1 From 1dfc090db78eb05539c0c0c3b291071eedaa8774 Mon Sep 17 00:00:00 2001 From: rtritto Date: Thu, 19 Jan 2023 20:47:05 +0100 Subject: [PATCH 5/7] Lint --- src/components/DataKeyPair.tsx | 5 +++-- src/components/DataTypes/Object.tsx | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/DataKeyPair.tsx b/src/components/DataKeyPair.tsx index c7e8c303..03a51327 100644 --- a/src/components/DataKeyPair.tsx +++ b/src/components/DataKeyPair.tsx @@ -187,8 +187,9 @@ export const DataKeyPair: FC = (props) => { data-testid={'data-key-pair' + path.join('.')} sx={{ userSelect: 'text' }} onMouseEnter={ - useCallback(() => setHover({ path, nestedIndex }), - [setHover, path, nestedIndex]) + useCallback(() => { + setHover({ path, nestedIndex }) + }, [setHover, path, nestedIndex]) } > > = (props) => { } = useAtomValue(colorspaceAtom) const isArray = useMemo(() => Array.isArray(props.value), [props.value]) const isEmptyValue = useMemo(() => getValueSize(props.value) === 0, [props.value]) - const sizeOfValue = useMemo(() => inspectMetadata(props.value), [props.inspect, props.value]) + const sizeOfValue = useMemo(() => inspectMetadata(props.value), [props.value]) const displayObjectSize = useAtomValue(displayObjectSizeAtom) const isTrap = useIsCycleReference(props.path, props.value) return ( From 3d65c0326e3de57d7466533e8868da45287288e0 Mon Sep 17 00:00:00 2001 From: rtritto Date: Thu, 19 Jan 2023 22:53:23 +0100 Subject: [PATCH 6/7] Fix typo --- src/hooks/useInspect.ts | 4 ++-- src/index.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks/useInspect.ts b/src/hooks/useInspect.ts index fc37e511..2117eb42 100644 --- a/src/hooks/useInspect.ts +++ b/src/hooks/useInspect.ts @@ -36,7 +36,7 @@ export function useInspect ( const inspect = isTrap ? false : depth < defaultInspectDepth - setInspectCache({ path, inspect }) + setInspectCache({ path, action: inspect }) } }, [defaultInspectDepth, depth, isTrap, nestedIndex, path, inspectCache, setInspectCache]) const [inspect, set] = useState(() => { @@ -53,7 +53,7 @@ export function useInspect ( const setInspect = useCallback>>((apply) => { set((oldState) => { const newState = typeof apply === 'boolean' ? apply : apply(oldState) - setInspectCache({ path, newState, nestedIndex }) + setInspectCache({ path, action: newState, nestedIndex }) return newState }) }, [nestedIndex, path, setInspectCache]) diff --git a/src/index.tsx b/src/index.tsx index ace587fa..0bfab972 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,8 +2,8 @@ import { createTheme, Paper, ThemeProvider } from '@mui/material' +import { useAtomValue, useSetAtom } from 'jotai' import type { Atom } from 'jotai' -import { useAtom, useSetAtom } from 'jotai' import type { FC, ReactElement } from 'react' import { useCallback, useEffect, useMemo, useRef } from 'react' @@ -99,7 +99,7 @@ const JsonViewerInner: FC = (props) => { registerTypes(allTypes) }, [predefinedTypes, props.valueTypes, registerTypes]) - const value = useAtom(valueAtom) + const value = useAtomValue(valueAtom) const setHover = useSetAtom(setHoverAtom) return ( Date: Fri, 20 Jan 2023 00:16:00 +0100 Subject: [PATCH 7/7] Refactor inspectCache atom --- src/hooks/useInspect.ts | 11 +++-------- src/state.ts | 18 ++++++++---------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/hooks/useInspect.ts b/src/hooks/useInspect.ts index 2117eb42..b026cb96 100644 --- a/src/hooks/useInspect.ts +++ b/src/hooks/useInspect.ts @@ -1,4 +1,4 @@ -import { useAtomValue, useSetAtom } from 'jotai' +import { useAtom, useAtomValue } from 'jotai' import { Dispatch, SetStateAction, @@ -7,11 +7,7 @@ import { useState } from 'react' -import { - defaultInspectDepthAtom, - getInspectCacheAtom, - setInspectCacheAtom -} from '../state' +import { defaultInspectDepthAtom, inspectCacheAtom } from '../state' import type { HostPath, JsonViewerProps } from '../type' import { useIsCycleReference } from './useIsCycleReference' @@ -23,8 +19,7 @@ export function useInspect ( const depth = path.length const isTrap = useIsCycleReference(path, value) const defaultInspectDepth = useAtomValue(defaultInspectDepthAtom) - const inspectCache = useAtomValue(getInspectCacheAtom({ path, nestedIndex })) - const setInspectCache = useSetAtom(setInspectCacheAtom) + const [inspectCache, setInspectCache] = useAtom(inspectCacheAtom({ path, nestedIndex })) useEffect(() => { if (inspectCache !== undefined) { return diff --git a/src/state.ts b/src/state.ts index 71b57d7e..83035be1 100644 --- a/src/state.ts +++ b/src/state.ts @@ -23,35 +23,33 @@ export const collapseStringsAfterLengthAtom = atom(undefined) export const objectSortKeysAtom = atom(undefined) export const quotesOnKeysAtom = atom(undefined) -export const inspectCacheAtom = atom({}) export const hoverPathAtom = atom(null) export const registryAtom = atom([]) +const _inspectCacheAtom = atom({}) // TODO check: if memory leaks, add to last line of useEffect: // return () => { atomFamily.remove ... // Anything in here is fired on component unmount } -export const getInspectCacheAtom = atomFamily(({ path, nestedIndex }) => atom( +export const inspectCacheAtom = atomFamily(({ path, nestedIndex }) => atom( (get) => { const target = nestedIndex === undefined ? path.join('.') : `${path.join('.')}[${nestedIndex}]nt` - return get(inspectCacheAtom)[target] - } -), deepEqual) -export const setInspectCacheAtom = atom( - (get) => get(inspectCacheAtom), + return get(_inspectCacheAtom)[target] + }, (get, set, { path, action, nestedIndex }) => { const target = nestedIndex === undefined ? path.join('.') : `${path.join('.')}[${nestedIndex}]nt` - const inspectCache = get(inspectCacheAtom) - return set(inspectCacheAtom, { + const inspectCache = get(_inspectCacheAtom) + return set(_inspectCacheAtom, { ...inspectCache, [target]: typeof action === 'function' ? action(inspectCache[target]) : action }) } -) +), deepEqual) + export const setHoverAtom = atom( (get) => get(hoverPathAtom), (_get, set, { path, nestedIndex }) => {