diff --git a/.changeset/four-parrots-poke.md b/.changeset/four-parrots-poke.md new file mode 100644 index 00000000..4ba85d6f --- /dev/null +++ b/.changeset/four-parrots-poke.md @@ -0,0 +1,9 @@ +--- +'react-json-tree': major +--- + +Remove UNSAFE method from react-json-tree + +- Replace `shouldExpandNode` with `shouldExpandNodeInitially`. This function is now only called when a node in the tree is first rendered, when before it would update the expanded state of the node if the results of calling `shouldExpandNode` changed between renders. There is no way to replicate the old behavior exactly, but the new behavior is the intended behavior for the use cases within Redux DevTools. Please open an issue if you need a way to programatically control the expanded state of nodes. +- Bump the minimum React version from `16.3.0` to `16.8.0` so that `react-json-tree` can use hooks. +- Tightened TypeScript prop types to use `unknown` instead of `any` where possible and make the key path array `readonly`. diff --git a/packages/react-json-tree/README.md b/packages/react-json-tree/README.md index 6e802e5f..7a55b3e1 100644 --- a/packages/react-json-tree/README.md +++ b/packages/react-json-tree/README.md @@ -139,7 +139,7 @@ Their full signatures are: #### More Options -- `shouldExpandNode: function(keyPath, data, level)` - determines if node should be expanded (root is expanded by default) +- `shouldExpandNodeInitially: function(keyPath, data, level)` - determines if node should be expanded when it first renders (root is expanded by default) - `hideRoot: boolean` - if `true`, the root node is hidden. - `sortObjectKeys: boolean | function(a, b)` - sorts object keys with compare function (optional). Isn't applied to iterable maps like `Immutable.Map`. - `postprocessValue: function(value)` - maps `value` to a new `value` diff --git a/packages/react-json-tree/examples/src/App.tsx b/packages/react-json-tree/examples/src/App.tsx index 75678a01..a972ce58 100644 --- a/packages/react-json-tree/examples/src/App.tsx +++ b/packages/react-json-tree/examples/src/App.tsx @@ -178,7 +178,7 @@ const App = () => ( 😐 {' '} - {raw}{' '} + {raw as string}{' '} 😐 @@ -194,7 +194,11 @@ const App = () => (

Collapsed root node

- false} /> + false} + />
); diff --git a/packages/react-json-tree/package.json b/packages/react-json-tree/package.json index 07ed2b13..f985868a 100644 --- a/packages/react-json-tree/package.json +++ b/packages/react-json-tree/package.json @@ -47,8 +47,6 @@ "dependencies": { "@babel/runtime": "^7.20.6", "@types/lodash": "^4.14.191", - "@types/prop-types": "^15.7.5", - "prop-types": "^15.8.1", "react-base16-styling": "^0.9.1" }, "devDependencies": { @@ -85,7 +83,7 @@ "typescript": "~4.9.4" }, "peerDependencies": { - "@types/react": "^16.3.0 || ^17.0.0 || ^18.0.0", - "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } } diff --git a/packages/react-json-tree/src/ItemRange.tsx b/packages/react-json-tree/src/ItemRange.tsx index 3848783e..52abad4a 100644 --- a/packages/react-json-tree/src/ItemRange.tsx +++ b/packages/react-json-tree/src/ItemRange.tsx @@ -1,59 +1,39 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { useCallback, useState } from 'react'; import JSONArrow from './JSONArrow'; -import { CircularPropsPassedThroughItemRange } from './types'; +import type { CircularCache, CommonInternalProps } from './types'; -interface Props extends CircularPropsPassedThroughItemRange { - data: any; +interface Props extends CommonInternalProps { + data: unknown; nodeType: string; from: number; to: number; renderChildNodes: (props: Props, from: number, to: number) => React.ReactNode; + circularCache: CircularCache; + level: number; } -interface State { - expanded: boolean; -} - -export default class ItemRange extends React.Component { - static propTypes = { - styling: PropTypes.func.isRequired, - from: PropTypes.number.isRequired, - to: PropTypes.number.isRequired, - renderChildNodes: PropTypes.func.isRequired, - nodeType: PropTypes.string.isRequired, - }; - - constructor(props: Props) { - super(props); - this.state = { expanded: false }; - } - - render() { - const { styling, from, to, renderChildNodes, nodeType } = this.props; - - return this.state.expanded ? ( -
- {renderChildNodes(this.props, from, to)} -
- ) : ( -
- - {`${from} ... ${to}`} -
- ); - } - - handleClick = () => { - this.setState({ expanded: !this.state.expanded }); - }; +export default function ItemRange(props: Props) { + const { styling, from, to, renderChildNodes, nodeType } = props; + + const [expanded, setExpanded] = useState(false); + const handleClick = useCallback(() => { + setExpanded(!expanded); + }, [expanded]); + + return expanded ? ( +
+ {renderChildNodes(props, from, to)} +
+ ) : ( +
+ + {`${from} ... ${to}`} +
+ ); } diff --git a/packages/react-json-tree/src/JSONArrayNode.tsx b/packages/react-json-tree/src/JSONArrayNode.tsx index 97440160..76da807c 100644 --- a/packages/react-json-tree/src/JSONArrayNode.tsx +++ b/packages/react-json-tree/src/JSONArrayNode.tsx @@ -1,35 +1,30 @@ import React from 'react'; -import PropTypes from 'prop-types'; import JSONNestedNode from './JSONNestedNode'; -import { CircularPropsPassedThroughJSONNode } from './types'; +import type { CommonInternalProps } from './types'; // Returns the "n Items" string for this node, // generating and caching it if it hasn't been created yet. -function createItemString(data: any) { +function createItemString(data: unknown) { return `${(data as unknown[]).length} ${ (data as unknown[]).length !== 1 ? 'items' : 'item' }`; } -interface Props extends CircularPropsPassedThroughJSONNode { - data: any; +interface Props extends CommonInternalProps { + data: unknown; nodeType: string; } // Configures to render an Array -const JSONArrayNode: React.FunctionComponent = ({ data, ...props }) => ( - 0} - /> -); - -JSONArrayNode.propTypes = { - data: PropTypes.array, -}; - -export default JSONArrayNode; +export default function JSONArrayNode({ data, ...props }: Props) { + return ( + 0} + /> + ); +} diff --git a/packages/react-json-tree/src/JSONArrow.tsx b/packages/react-json-tree/src/JSONArrow.tsx index 25f7e2f8..e00fe456 100644 --- a/packages/react-json-tree/src/JSONArrow.tsx +++ b/packages/react-json-tree/src/JSONArrow.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { StylingFunction } from 'react-base16-styling'; +import type { StylingFunction } from 'react-base16-styling'; interface Props { styling: StylingFunction; @@ -10,33 +9,21 @@ interface Props { onClick: React.MouseEventHandler; } -const JSONArrow: React.FunctionComponent = ({ +export default function JSONArrow({ styling, - arrowStyle, + arrowStyle = 'single', expanded, nodeType, onClick, -}) => ( -
-
- {'\u25B6'} - {arrowStyle === 'double' && ( -
{'\u25B6'}
- )} +}: Props) { + return ( +
+
+ {'\u25B6'} + {arrowStyle === 'double' && ( +
{'\u25B6'}
+ )} +
-
-); - -JSONArrow.propTypes = { - styling: PropTypes.func.isRequired, - arrowStyle: PropTypes.oneOf(['single', 'double']), - expanded: PropTypes.bool.isRequired, - nodeType: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, -}; - -JSONArrow.defaultProps = { - arrowStyle: 'single', -}; - -export default JSONArrow; + ); +} diff --git a/packages/react-json-tree/src/JSONIterableNode.tsx b/packages/react-json-tree/src/JSONIterableNode.tsx index dff51ad5..0357b511 100644 --- a/packages/react-json-tree/src/JSONIterableNode.tsx +++ b/packages/react-json-tree/src/JSONIterableNode.tsx @@ -1,6 +1,6 @@ import React from 'react'; import JSONNestedNode from './JSONNestedNode'; -import { CircularPropsPassedThroughJSONNode } from './types'; +import type { CommonInternalProps } from './types'; // Returns the "n Items" string for this node, // generating and caching it if it hasn't been created yet. @@ -22,21 +22,20 @@ function createItemString(data: any, limit: number) { return `${hasMore ? '>' : ''}${count} ${count !== 1 ? 'entries' : 'entry'}`; } -interface Props extends CircularPropsPassedThroughJSONNode { - data: any; +interface Props extends CommonInternalProps { + data: unknown; nodeType: string; } // Configures to render an iterable -const JSONIterableNode: React.FunctionComponent = ({ ...props }) => { +export default function JSONIterableNode(props: Props) { return ( ); -}; - -export default JSONIterableNode; +} diff --git a/packages/react-json-tree/src/JSONNestedNode.tsx b/packages/react-json-tree/src/JSONNestedNode.tsx index e678af6c..0e5b5be5 100644 --- a/packages/react-json-tree/src/JSONNestedNode.tsx +++ b/packages/react-json-tree/src/JSONNestedNode.tsx @@ -1,22 +1,19 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { useCallback, useState } from 'react'; import JSONArrow from './JSONArrow'; import getCollectionEntries from './getCollectionEntries'; import JSONNode from './JSONNode'; import ItemRange from './ItemRange'; -import { - CircularPropsPassedThroughJSONNestedNode, - CircularPropsPassedThroughRenderChildNodes, -} from './types'; +import type { CircularCache, CommonInternalProps } from './types'; /** * Renders nested values (eg. objects, arrays, lists, etc.) */ -export interface RenderChildNodesProps - extends CircularPropsPassedThroughRenderChildNodes { - data: any; +export interface RenderChildNodesProps extends CommonInternalProps { + data: unknown; nodeType: string; + circularCache: CircularCache; + level: number; } interface Range { @@ -26,7 +23,7 @@ interface Range { interface Entry { key: string | number; - value: any; + value: unknown; } function isRange(rangeOrEntry: Range | Entry): rangeOrEntry is Range { @@ -89,152 +86,92 @@ function renderChildNodes( return childNodes; } -interface Props extends CircularPropsPassedThroughJSONNestedNode { - data: any; +interface Props extends CommonInternalProps { + data: unknown; nodeType: string; nodeTypeIndicator: string; - createItemString: (data: any, collectionLimit: number) => string; + createItemString: (data: unknown, collectionLimit: number) => string; expandable: boolean; } -interface State { - expanded: boolean; -} +export default function JSONNestedNode(props: Props) { + const { + circularCache = [], + collectionLimit, + createItemString, + data, + expandable, + getItemString, + hideRoot, + isCircular, + keyPath, + labelRenderer, + level = 0, + nodeType, + nodeTypeIndicator, + shouldExpandNodeInitially, + styling, + } = props; -function getStateFromProps(props: Props) { - // calculate individual node expansion if necessary - const expanded = !props.isCircular - ? props.shouldExpandNode(props.keyPath, props.data, props.level) - : false; - return { - expanded, - }; -} + const [expanded, setExpanded] = useState( + // calculate individual node expansion if necessary + isCircular ? false : shouldExpandNodeInitially(keyPath, data, level) + ); -export default class JSONNestedNode extends React.Component { - static propTypes = { - getItemString: PropTypes.func.isRequired, - nodeTypeIndicator: PropTypes.any, - nodeType: PropTypes.string.isRequired, - data: PropTypes.any, - hideRoot: PropTypes.bool.isRequired, - createItemString: PropTypes.func.isRequired, - styling: PropTypes.func.isRequired, - collectionLimit: PropTypes.number, - keyPath: PropTypes.arrayOf( - PropTypes.oneOfType([PropTypes.string, PropTypes.number]) - ).isRequired, - labelRenderer: PropTypes.func.isRequired, - shouldExpandNode: PropTypes.func, - level: PropTypes.number.isRequired, - sortObjectKeys: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]), - isCircular: PropTypes.bool, - expandable: PropTypes.bool, - }; + const handleClick = useCallback(() => { + if (expandable) setExpanded(!expanded); + }, [expandable, expanded]); - static defaultProps = { - data: [], - circularCache: [], - level: 0, - expandable: true, - }; + const renderedChildren = + expanded || (hideRoot && level === 0) + ? renderChildNodes({ ...props, circularCache, level: level + 1 }) + : null; - constructor(props: Props) { - super(props); - this.state = getStateFromProps(props); - } + const itemType = ( + + {nodeTypeIndicator} + + ); + const renderedItemString = getItemString( + nodeType, + data, + itemType, + createItemString(data, collectionLimit), + keyPath + ); + const stylingArgs = [keyPath, nodeType, expanded, expandable] as const; - UNSAFE_componentWillReceiveProps(nextProps: Props) { - const nextState = getStateFromProps(nextProps); - if (getStateFromProps(this.props).expanded !== nextState.expanded) { - this.setState(nextState); - } - } - - shouldComponentUpdate(nextProps: Props, nextState: State) { - return ( - !!Object.keys(nextProps).find( - (key) => - key !== 'circularCache' && - (key === 'keyPath' - ? nextProps[key].join('/') !== this.props[key].join('/') - : nextProps[key as keyof Props] !== this.props[key as keyof Props]) - ) || nextState.expanded !== this.state.expanded - ); - } - - render() { - const { - getItemString, - nodeTypeIndicator, - nodeType, - data, - hideRoot, - createItemString, - styling, - collectionLimit, - keyPath, - labelRenderer, - expandable, - } = this.props; - const { expanded } = this.state; - const renderedChildren = - expanded || (hideRoot && this.props.level === 0) - ? renderChildNodes({ ...this.props, level: this.props.level + 1 }) - : null; - - const itemType = ( - - {nodeTypeIndicator} + return hideRoot ? ( +
  • +
      + {renderedChildren} +
    +
  • + ) : ( +
  • + {expandable && ( + + )} + + + {renderedItemString} - ); - const renderedItemString = getItemString( - nodeType, - data, - itemType, - createItemString(data, collectionLimit), - keyPath - ); - const stylingArgs = [keyPath, nodeType, expanded, expandable] as const; - - return hideRoot ? ( -
  • -
      - {renderedChildren} -
    -
  • - ) : ( -
  • - {expandable && ( - - )} - - - {renderedItemString} - -
      - {renderedChildren} -
    -
  • - ); - } - - handleClick = () => { - if (this.props.expandable) { - this.setState({ expanded: !this.state.expanded }); - } - }; +
      + {renderedChildren} +
    + + ); } diff --git a/packages/react-json-tree/src/JSONNode.tsx b/packages/react-json-tree/src/JSONNode.tsx index a95314c0..f7703350 100644 --- a/packages/react-json-tree/src/JSONNode.tsx +++ b/packages/react-json-tree/src/JSONNode.tsx @@ -1,19 +1,16 @@ import React from 'react'; -import PropTypes from 'prop-types'; import objType from './objType'; import JSONObjectNode from './JSONObjectNode'; import JSONArrayNode from './JSONArrayNode'; import JSONIterableNode from './JSONIterableNode'; import JSONValueNode from './JSONValueNode'; -import { CircularPropsPassedThroughJSONNode } from './types'; +import type { CommonInternalProps } from './types'; -interface Props extends CircularPropsPassedThroughJSONNode { - keyPath: (string | number)[]; - value: any; - isCustomNode: (value: any) => boolean; +interface Props extends CommonInternalProps { + value: unknown; } -const JSONNode: React.FunctionComponent = ({ +export default function JSONNode({ getItemString, keyPath, labelRenderer, @@ -22,7 +19,7 @@ const JSONNode: React.FunctionComponent = ({ valueRenderer, isCustomNode, ...rest -}) => { +}: Props) { const nodeType = isCustomNode(value) ? 'Custom' : objType(value); const simpleNodeProps = { @@ -102,18 +99,4 @@ const JSONNode: React.FunctionComponent = ({ /> ); } -}; - -JSONNode.propTypes = { - getItemString: PropTypes.func.isRequired, - keyPath: PropTypes.arrayOf( - PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired - ).isRequired, - labelRenderer: PropTypes.func.isRequired, - styling: PropTypes.func.isRequired, - value: PropTypes.any, - valueRenderer: PropTypes.func.isRequired, - isCustomNode: PropTypes.func.isRequired, -}; - -export default JSONNode; +} diff --git a/packages/react-json-tree/src/JSONObjectNode.tsx b/packages/react-json-tree/src/JSONObjectNode.tsx index 6cfcbe06..95401f83 100644 --- a/packages/react-json-tree/src/JSONObjectNode.tsx +++ b/packages/react-json-tree/src/JSONObjectNode.tsx @@ -1,35 +1,29 @@ import React from 'react'; -import PropTypes from 'prop-types'; import JSONNestedNode from './JSONNestedNode'; -import { CircularPropsPassedThroughJSONNode } from './types'; +import type { CommonInternalProps } from './types'; // Returns the "n Items" string for this node, // generating and caching it if it hasn't been created yet. -function createItemString(data: any) { +function createItemString(data: unknown) { const len = Object.getOwnPropertyNames(data).length; return `${len} ${len !== 1 ? 'keys' : 'key'}`; } -interface Props extends CircularPropsPassedThroughJSONNode { - data: any; +interface Props extends CommonInternalProps { + data: unknown; nodeType: string; } // Configures to render an Object -const JSONObjectNode: React.FunctionComponent = ({ data, ...props }) => ( - 0} - /> -); - -JSONObjectNode.propTypes = { - data: PropTypes.object, - nodeType: PropTypes.string.isRequired, -}; - -export default JSONObjectNode; +export default function JSONObjectNode({ data, ...props }: Props) { + return ( + 0} + /> + ); +} diff --git a/packages/react-json-tree/src/JSONValueNode.tsx b/packages/react-json-tree/src/JSONValueNode.tsx index c7754256..24e4272d 100644 --- a/packages/react-json-tree/src/JSONValueNode.tsx +++ b/packages/react-json-tree/src/JSONValueNode.tsx @@ -1,18 +1,30 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { JSONValueNodeCircularPropsProvidedByJSONNode } from './types'; +import type { + GetItemString, + Key, + KeyPath, + LabelRenderer, + Styling, + ValueRenderer, +} from './types'; /** * Renders simple values (eg. strings, numbers, booleans, etc) */ -interface Props extends JSONValueNodeCircularPropsProvidedByJSONNode { +interface Props { + getItemString: GetItemString; + key: Key; + keyPath: KeyPath; + labelRenderer: LabelRenderer; nodeType: string; - value: any; - valueGetter?: (value: any) => any; + styling: Styling; + value: unknown; + valueRenderer: ValueRenderer; + valueGetter?: (value: any) => unknown; } -const JSONValueNode: React.FunctionComponent = ({ +export default function JSONValueNode({ nodeType, styling, labelRenderer, @@ -20,27 +32,15 @@ const JSONValueNode: React.FunctionComponent = ({ valueRenderer, value, valueGetter = (value) => value, -}) => ( -
  • - - - {valueRenderer(valueGetter(value), value, ...keyPath)} - -
  • -); - -JSONValueNode.propTypes = { - nodeType: PropTypes.string.isRequired, - styling: PropTypes.func.isRequired, - labelRenderer: PropTypes.func.isRequired, - keyPath: PropTypes.arrayOf( - PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired - ).isRequired, - valueRenderer: PropTypes.func.isRequired, - value: PropTypes.any, - valueGetter: PropTypes.func, -}; - -export default JSONValueNode; +}: Props) { + return ( +
  • + + + {valueRenderer(valueGetter(value), value, ...keyPath)} + +
  • + ); +} diff --git a/packages/react-json-tree/src/createStylingFromTheme.ts b/packages/react-json-tree/src/createStylingFromTheme.ts index 2306b91c..5c776c7e 100644 --- a/packages/react-json-tree/src/createStylingFromTheme.ts +++ b/packages/react-json-tree/src/createStylingFromTheme.ts @@ -1,11 +1,12 @@ import type { CurriedFunction1 } from 'lodash'; -import { +import { createStyling } from 'react-base16-styling'; +import type { Base16Theme, - createStyling, StylingConfig, + StylingFunction, + Theme, } from 'react-base16-styling'; import solarized from './themes/solarized'; -import { StylingFunction, Theme } from 'react-base16-styling/src'; const colorMap = (theme: Base16Theme) => ({ BACKGROUND_COLOR: theme.base00, diff --git a/packages/react-json-tree/src/getCollectionEntries.ts b/packages/react-json-tree/src/getCollectionEntries.ts index 85c5403b..a08c0859 100644 --- a/packages/react-json-tree/src/getCollectionEntries.ts +++ b/packages/react-json-tree/src/getCollectionEntries.ts @@ -1,4 +1,6 @@ -function getLength(type: string, collection: any) { +import type { SortObjectKeys } from './types'; + +function getLength(type: string, collection: unknown) { if (type === 'Object') { // eslint-disable-next-line @typescript-eslint/ban-types return Object.keys(collection as {}).length; @@ -9,17 +11,17 @@ function getLength(type: string, collection: any) { return Infinity; } -function isIterableMap(collection: any) { - return typeof (collection as Map).set === 'function'; +function isIterableMap(collection: unknown) { + return typeof (collection as Map).set === 'function'; } function getEntries( type: string, collection: any, - sortObjectKeys?: ((a: any, b: any) => number) | boolean | undefined, + sortObjectKeys: SortObjectKeys, from = 0, to = Infinity -): { entries: { key: string | number; value: any }[]; hasMore?: boolean } { +): { entries: { key: string | number; value: unknown }[]; hasMore?: boolean } { let res; if (type === 'Object') { @@ -95,8 +97,8 @@ function getRanges(from: number, to: number, limit: number) { export default function getCollectionEntries( type: string, - collection: any, - sortObjectKeys: ((a: any, b: any) => number) | boolean | undefined, + collection: unknown, + sortObjectKeys: SortObjectKeys, limit: number, from = 0, to = Infinity diff --git a/packages/react-json-tree/src/index.tsx b/packages/react-json-tree/src/index.tsx index f5013b9f..b84c747e 100644 --- a/packages/react-json-tree/src/index.tsx +++ b/packages/react-json-tree/src/index.tsx @@ -3,177 +3,87 @@ // Dave Vedder http://www.eskimospy.com/ // port by Daniele Zannotti http://www.github.com/dzannotti -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { useMemo } from 'react'; import JSONNode from './JSONNode'; import createStylingFromTheme from './createStylingFromTheme'; import { invertTheme } from 'react-base16-styling'; +import type { StylingValue, Theme } from 'react-base16-styling'; import type { - StylingConfig, - StylingFunction, - StylingValue, - Theme, -} from 'react-base16-styling'; -import { CircularPropsPassedThroughJSONTree } from './types'; + CommonExternalProps, + GetItemString, + IsCustomNode, + LabelRenderer, + ShouldExpandNodeInitially, +} from './types'; -interface Props extends CircularPropsPassedThroughJSONTree { - data: any; +interface Props extends Partial { + data: unknown; theme?: Theme; - invertTheme: boolean; -} - -interface State { - styling: StylingFunction; + invertTheme?: boolean; } const identity = (value: any) => value; -const expandRootNode = ( - keyPath: (string | number)[], - data: any, - level: number -) => level === 0; -const defaultItemString = ( - type: string, - data: any, - itemType: React.ReactNode, - itemString: string -) => ( +const expandRootNode: ShouldExpandNodeInitially = (keyPath, data, level) => + level === 0; +const defaultItemString: GetItemString = (type, data, itemType, itemString) => ( {itemType} {itemString} ); -const defaultLabelRenderer = ([label]: (string | number)[]) => ( - {label}: -); -const noCustomNode = () => false; +const defaultLabelRenderer: LabelRenderer = ([label]) => {label}:; +const noCustomNode: IsCustomNode = () => false; -function checkLegacyTheming(theme: Theme | undefined, props: Props) { - const deprecatedStylingMethodsMap = { - getArrowStyle: 'arrow', - getListStyle: 'nestedNodeChildren', - getItemStringStyle: 'nestedNodeItemString', - getLabelStyle: 'label', - getValueStyle: 'valueText', - }; +export function JSONTree({ + data: value, + theme, + invertTheme: shouldInvertTheme, + keyPath = ['root'], + labelRenderer = defaultLabelRenderer, + valueRenderer = identity, + shouldExpandNodeInitially = expandRootNode, + hideRoot = false, + getItemString = defaultItemString, + postprocessValue = identity, + isCustomNode = noCustomNode, + collectionLimit = 50, + sortObjectKeys = false, +}: Props) { + const styling = useMemo( + () => + createStylingFromTheme(shouldInvertTheme ? invertTheme(theme) : theme), + [theme, shouldInvertTheme] + ); - const deprecatedStylingMethods = Object.keys( - deprecatedStylingMethodsMap - ).filter((name) => props[name as keyof Props]); - - if (deprecatedStylingMethods.length > 0) { - if (typeof theme === 'string') { - theme = { - extend: theme, - }; - } else { - theme = { ...theme }; - } - - deprecatedStylingMethods.forEach((name) => { - // eslint-disable-next-line no-console - console.error( - `Styling method "${name}" is deprecated, use "theme" property instead` - ); - - (theme as StylingConfig)[ - deprecatedStylingMethodsMap[ - name as keyof typeof deprecatedStylingMethodsMap - ] - ] = ({ style }, ...args) => ({ - style: { - ...style, - ...props[name as keyof Props](...args), - }, - }); - }); - } - - return theme; + return ( +
      + +
    + ); } -function getStateFromProps(props: Props) { - let theme = checkLegacyTheming(props.theme, props); - if (props.invertTheme) { - theme = invertTheme(theme); - } - - return { - styling: createStylingFromTheme(theme), - }; -} - -export class JSONTree extends React.Component { - static propTypes = { - data: PropTypes.any, - hideRoot: PropTypes.bool, - theme: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), - invertTheme: PropTypes.bool, - keyPath: PropTypes.arrayOf( - PropTypes.oneOfType([PropTypes.string, PropTypes.number]) - ), - postprocessValue: PropTypes.func, - sortObjectKeys: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]), - }; - - static defaultProps = { - shouldExpandNode: expandRootNode, - hideRoot: false, - keyPath: ['root'], - getItemString: defaultItemString, - labelRenderer: defaultLabelRenderer, - valueRenderer: identity, - postprocessValue: identity, - isCustomNode: noCustomNode, - collectionLimit: 50, - invertTheme: true, - }; - - constructor(props: Props) { - super(props); - this.state = getStateFromProps(props); - } - - UNSAFE_componentWillReceiveProps(nextProps: Props) { - if ( - ['theme', 'invertTheme'].find( - (k) => nextProps[k as keyof Props] !== this.props[k as keyof Props] - ) - ) { - this.setState(getStateFromProps(nextProps)); - } - } - - shouldComponentUpdate(nextProps: Props) { - return !!Object.keys(nextProps).find((k) => - k === 'keyPath' - ? nextProps[k].join('/') !== this.props[k].join('/') - : nextProps[k as keyof Props] !== this.props[k as keyof Props] - ); - } - - render() { - const { - data: value, - keyPath, - postprocessValue, - hideRoot, - theme, // eslint-disable-line no-unused-vars - invertTheme: _, // eslint-disable-line no-unused-vars - ...rest - } = this.props; - - const { styling } = this.state; - - return ( -
      - -
    - ); - } -} - -export { StylingValue }; +export type { + Key, + KeyPath, + GetItemString, + LabelRenderer, + ValueRenderer, + ShouldExpandNodeInitially, + PostprocessValue, + IsCustomNode, + SortObjectKeys, + Styling, +} from './types'; +export type { StylingValue }; diff --git a/packages/react-json-tree/src/types.ts b/packages/react-json-tree/src/types.ts index 5d8e1f7c..6a67f376 100644 --- a/packages/react-json-tree/src/types.ts +++ b/packages/react-json-tree/src/types.ts @@ -1,81 +1,63 @@ import React from 'react'; import { StylingFunction } from 'react-base16-styling'; -interface SharedCircularPropsPassedThroughJSONTree { - keyPath: (string | number)[]; - labelRenderer: ( - keyPath: (string | number)[], - nodeType: string, - expanded: boolean, - expandable: boolean - ) => React.ReactNode; -} -interface SharedCircularPropsProvidedByJSONTree - extends SharedCircularPropsPassedThroughJSONTree { - styling: StylingFunction; -} -interface JSONValueNodeCircularPropsPassedThroughJSONTree { - valueRenderer: ( - valueAsString: any, - value: any, - ...keyPath: (string | number)[] - ) => React.ReactNode; -} -export type JSONValueNodeCircularPropsProvidedByJSONNode = - SharedCircularPropsProvidedByJSONTree & - JSONValueNodeCircularPropsPassedThroughJSONTree; +export type Key = string | number; -interface JSONNestedNodeCircularPropsPassedThroughJSONTree { - shouldExpandNode: ( - keyPath: (string | number)[], - data: any, - level: number - ) => boolean; +export type KeyPath = readonly (string | number)[]; + +export type GetItemString = ( + nodeType: string, + data: unknown, + itemType: React.ReactNode, + itemString: string, + keyPath: KeyPath +) => React.ReactNode; + +export type LabelRenderer = ( + keyPath: KeyPath, + nodeType: string, + expanded: boolean, + expandable: boolean +) => React.ReactNode; + +export type ValueRenderer = ( + valueAsString: unknown, + value: unknown, + ...keyPath: KeyPath +) => React.ReactNode; + +export type ShouldExpandNodeInitially = ( + keyPath: KeyPath, + data: unknown, + level: number +) => boolean; + +export type PostprocessValue = (value: unknown) => unknown; + +export type IsCustomNode = (value: unknown) => boolean; + +export type SortObjectKeys = ((a: unknown, b: unknown) => number) | boolean; + +export type Styling = StylingFunction; + +export type CircularCache = unknown[]; + +export interface CommonExternalProps { + keyPath: KeyPath; + labelRenderer: LabelRenderer; + valueRenderer: ValueRenderer; + shouldExpandNodeInitially: ShouldExpandNodeInitially; hideRoot: boolean; - getItemString: ( - nodeType: string, - data: any, - itemType: React.ReactNode, - itemString: string, - keyPath: (string | number)[] - ) => React.ReactNode; - postprocessValue: (value: any) => any; - isCustomNode: (value: any) => boolean; + getItemString: GetItemString; + postprocessValue: PostprocessValue; + isCustomNode: IsCustomNode; collectionLimit: number; - sortObjectKeys?: ((a: any, b: any) => number) | boolean; + sortObjectKeys: SortObjectKeys; } -export type CircularPropsPassedThroughJSONTree = - SharedCircularPropsPassedThroughJSONTree & - JSONValueNodeCircularPropsPassedThroughJSONTree & - JSONNestedNodeCircularPropsPassedThroughJSONTree; -interface JSONNestedNodeCircularPropsPassedThroughJSONNode - extends JSONNestedNodeCircularPropsPassedThroughJSONTree { - circularCache?: any[]; - isCircular?: boolean; +export interface CommonInternalProps extends CommonExternalProps { + styling: StylingFunction; + circularCache?: CircularCache; level?: number; + isCircular?: boolean; } -export type CircularPropsPassedThroughJSONNode = - SharedCircularPropsProvidedByJSONTree & - JSONValueNodeCircularPropsPassedThroughJSONTree & - JSONNestedNodeCircularPropsPassedThroughJSONNode; - -export interface JSONNestedNodeCircularPropsPassedThroughJSONNestedNode - extends JSONNestedNodeCircularPropsPassedThroughJSONNode { - circularCache: any[]; - level: number; -} -export type CircularPropsPassedThroughJSONNestedNode = - SharedCircularPropsProvidedByJSONTree & - JSONValueNodeCircularPropsPassedThroughJSONTree & - JSONNestedNodeCircularPropsPassedThroughJSONNestedNode; - -export type CircularPropsPassedThroughRenderChildNodes = - SharedCircularPropsProvidedByJSONTree & - JSONValueNodeCircularPropsPassedThroughJSONTree & - JSONNestedNodeCircularPropsPassedThroughJSONNestedNode; - -export type CircularPropsPassedThroughItemRange = - SharedCircularPropsProvidedByJSONTree & - JSONValueNodeCircularPropsPassedThroughJSONTree & - JSONNestedNodeCircularPropsPassedThroughJSONNestedNode; diff --git a/packages/redux-devtools-inspector-monitor/src/ActionPreview.tsx b/packages/redux-devtools-inspector-monitor/src/ActionPreview.tsx index 0d28d107..aef637df 100644 --- a/packages/redux-devtools-inspector-monitor/src/ActionPreview.tsx +++ b/packages/redux-devtools-inspector-monitor/src/ActionPreview.tsx @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import { Base16Theme } from 'redux-devtools-themes'; import { Action } from 'redux'; import type { StylingFunction } from 'react-base16-styling'; +import type { LabelRenderer } from 'react-json-tree'; import { PerformAction } from '@redux-devtools/core'; import { Delta } from 'jsondiffpatch'; import { DEFAULT_STATE, DevtoolsInspectorState } from './redux'; @@ -11,12 +12,7 @@ import StateTab from './tabs/StateTab'; import ActionTab from './tabs/ActionTab'; export interface TabComponentProps> { - labelRenderer: ( - keyPath: (string | number)[], - nodeType: string, - expanded: boolean, - expandable: boolean - ) => React.ReactNode; + labelRenderer: LabelRenderer; styling: StylingFunction; computedStates: { state: S; error?: string }[]; actions: { [actionId: number]: PerformAction }; @@ -152,11 +148,7 @@ class ActionPreview> extends Component< ); } - labelRenderer = ( - [key, ...rest]: (string | number)[], - nodeType: string, - expanded: boolean - ) => { + labelRenderer: LabelRenderer = ([key, ...rest], nodeType, expanded) => { const { styling, onInspectPath, inspectedPath } = this.props; return ( diff --git a/packages/redux-devtools-inspector-monitor/src/index.ts b/packages/redux-devtools-inspector-monitor/src/index.ts index 2861d4e5..12c0cc0e 100644 --- a/packages/redux-devtools-inspector-monitor/src/index.ts +++ b/packages/redux-devtools-inspector-monitor/src/index.ts @@ -1,4 +1,5 @@ export type { StylingFunction } from 'react-base16-styling'; +export type { LabelRenderer } from 'react-json-tree'; export { default as InspectorMonitor } from './DevtoolsInspector'; export type { Tab, TabComponentProps } from './ActionPreview'; export type { DevtoolsInspectorState } from './redux'; diff --git a/packages/redux-devtools-inspector-monitor/src/tabs/JSONDiff.tsx b/packages/redux-devtools-inspector-monitor/src/tabs/JSONDiff.tsx index fe077a1e..56535823 100644 --- a/packages/redux-devtools-inspector-monitor/src/tabs/JSONDiff.tsx +++ b/packages/redux-devtools-inspector-monitor/src/tabs/JSONDiff.tsx @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import { JSONTree } from 'react-json-tree'; +import type { LabelRenderer, ShouldExpandNodeInitially } from 'react-json-tree'; import { stringify } from 'javascript-stringify'; import { Delta } from 'jsondiffpatch'; import { StylingFunction } from 'react-base16-styling'; @@ -22,11 +23,8 @@ function stringifyAndShrink(val: any, isWideLayout?: boolean) { return str.length > 22 ? `${str.substr(0, 15)}…${str.substr(-5)}` : str; } -const expandFirstLevel = ( - keyName: (string | number)[], - data: any, - level: number -) => level <= 1; +const expandFirstLevel: ShouldExpandNodeInitially = (keyName, data, level) => + level <= 1; function prepareDelta(value: any) { if (value && value._t === 'a') { @@ -53,12 +51,7 @@ interface Props { styling: StylingFunction; base16Theme: Base16Theme; invertTheme: boolean; - labelRenderer: ( - keyPath: (string | number)[], - nodeType: string, - expanded: boolean, - expandable: boolean - ) => React.ReactNode; + labelRenderer: LabelRenderer; isWideLayout: boolean; dataTypeKey: string | symbol | undefined; } @@ -104,7 +97,7 @@ export default class JSONDiff extends Component { valueRenderer={this.valueRenderer} postprocessValue={prepareDelta} isCustomNode={Array.isArray} - shouldExpandNode={expandFirstLevel} + shouldExpandNodeInitially={expandFirstLevel} hideRoot /> ); diff --git a/packages/redux-devtools-log-monitor/src/LogMonitorEntry.tsx b/packages/redux-devtools-log-monitor/src/LogMonitorEntry.tsx index 7fc803a7..fb173664 100644 --- a/packages/redux-devtools-log-monitor/src/LogMonitorEntry.tsx +++ b/packages/redux-devtools-log-monitor/src/LogMonitorEntry.tsx @@ -1,6 +1,7 @@ import React, { CSSProperties, MouseEventHandler, PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { JSONTree, StylingValue } from 'react-json-tree'; +import { JSONTree } from 'react-json-tree'; +import type { ShouldExpandNodeInitially, StylingValue } from 'react-json-tree'; import { Base16Theme } from 'redux-devtools-themes'; import { Action } from 'redux'; import LogMonitorEntryAction from './LogMonitorEntryAction'; @@ -115,7 +116,7 @@ export default class LogMonitorEntry< data={data} invertTheme={false} keyPath={['state']} - shouldExpandNode={this.shouldExpandNode} + shouldExpandNodeInitially={this.shouldExpandNodeInitially} /> ); } catch (err) { @@ -149,10 +150,10 @@ export default class LogMonitorEntry< } }; - shouldExpandNode = ( - keyPath: (string | number)[], - data: any, - level: number + shouldExpandNodeInitially: ShouldExpandNodeInitially = ( + keyPath, + data, + level ) => { return this.props.expandStateRoot && level === 0; }; diff --git a/packages/redux-devtools-log-monitor/src/LogMonitorEntryAction.tsx b/packages/redux-devtools-log-monitor/src/LogMonitorEntryAction.tsx index 9e1754d9..4b231e4c 100644 --- a/packages/redux-devtools-log-monitor/src/LogMonitorEntryAction.tsx +++ b/packages/redux-devtools-log-monitor/src/LogMonitorEntryAction.tsx @@ -1,5 +1,6 @@ import React, { Component, CSSProperties, MouseEventHandler } from 'react'; import { JSONTree } from 'react-json-tree'; +import type { ShouldExpandNodeInitially } from 'react-json-tree'; import { Base16Theme } from 'redux-devtools-themes'; import { Action } from 'redux'; @@ -42,7 +43,7 @@ export default class LogMonitorAction< invertTheme={false} keyPath={['action']} data={payload} - shouldExpandNode={this.shouldExpandNode} + shouldExpandNodeInitially={this.shouldExpandNodeInitially} /> ) : ( '' @@ -51,10 +52,10 @@ export default class LogMonitorAction< ); } - shouldExpandNode = ( - keyPath: (string | number)[], - data: any, - level: number + shouldExpandNodeInitially: ShouldExpandNodeInitially = ( + keyPath, + data, + level ) => { return this.props.expandActionRoot && level === 0; }; diff --git a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewActions.tsx b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewActions.tsx index 083d0896..8c47782c 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewActions.tsx +++ b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewActions.tsx @@ -1,6 +1,7 @@ import { createSelector, Selector } from '@reduxjs/toolkit'; import React, { ReactNode, PureComponent } from 'react'; import { Action, AnyAction } from 'redux'; +import type { KeyPath, ShouldExpandNodeInitially } from 'react-json-tree'; import { QueryPreviewTabs } from '../types'; import { renderTabPanelButtonId, renderTabPanelId } from '../utils/a11y'; import { emptyRecord, identity } from '../utils/object'; @@ -43,7 +44,7 @@ export class QueryPreviewActions extends PureComponent return output; }); - isLastActionNode = (keyPath: (string | number)[], layer: number): boolean => { + isLastActionNode = (keyPath: KeyPath, layer: number): boolean => { if (layer >= 1) { const len = this.props.actionsOfQuery.length; const actionKey = keyPath[keyPath.length - 1]; @@ -58,11 +59,11 @@ export class QueryPreviewActions extends PureComponent return false; }; - shouldExpandNode = ( - keyPath: (string | number)[], - value: unknown, - layer: number - ): boolean => { + shouldExpandNodeInitially: ShouldExpandNodeInitially = ( + keyPath, + value, + layer + ) => { if (layer === 1) { return this.isLastActionNode(keyPath, layer); } @@ -85,7 +86,7 @@ export class QueryPreviewActions extends PureComponent rootProps={rootProps} data={this.selectFormattedActions(actionsOfQuery)} isWideLayout={isWideLayout} - shouldExpandNode={this.shouldExpandNode} + shouldExpandNodeInitially={this.shouldExpandNodeInitially} /> ); } diff --git a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewApi.tsx b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewApi.tsx index e5858369..43838619 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewApi.tsx +++ b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewApi.tsx @@ -1,4 +1,5 @@ import React, { ReactNode, PureComponent } from 'react'; +import type { ShouldExpandNodeInitially } from 'react-json-tree'; import { ApiStats, QueryPreviewTabs, RtkQueryApiState } from '../types'; import { StyleUtilsContext } from '../styles/createStylingFromTheme'; import { TreeView, TreeViewProps } from './TreeView'; @@ -17,11 +18,11 @@ const rootProps: TreeViewProps['rootProps'] = { }; export class QueryPreviewApi extends PureComponent { - shouldExpandApiStateNode = ( - keyPath: (string | number)[], - value: unknown, - layer: number - ): boolean => { + shouldExpandApiStateNode: ShouldExpandNodeInitially = ( + keyPath, + value, + layer + ) => { const lastKey = keyPath[keyPath.length - 1]; return layer <= 1 && lastKey !== 'config'; @@ -45,7 +46,7 @@ export class QueryPreviewApi extends PureComponent { State} data={apiState} - shouldExpandNode={this.shouldExpandApiStateNode} + shouldExpandNodeInitially={this.shouldExpandApiStateNode} isWideLayout={isWideLayout} /> {apiStats && ( diff --git a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewData.tsx b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewData.tsx index cab37563..42b769ab 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewData.tsx +++ b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewData.tsx @@ -1,4 +1,5 @@ import React, { ReactNode, PureComponent } from 'react'; +import type { ShouldExpandNodeInitially } from 'react-json-tree'; import { QueryPreviewTabs, RtkResourceInfo } from '../types'; import { renderTabPanelButtonId, renderTabPanelId } from '../utils/a11y'; import { TreeView, TreeViewProps } from './TreeView'; @@ -15,11 +16,11 @@ const rootProps: TreeViewProps['rootProps'] = { }; export class QueryPreviewData extends PureComponent { - shouldExpandNode = ( - keyPath: (string | number)[], - value: unknown, - layer: number - ): boolean => { + shouldExpandNodeInitially: ShouldExpandNodeInitially = ( + keyPath, + value, + layer + ) => { return layer <= 1; }; @@ -31,7 +32,7 @@ export class QueryPreviewData extends PureComponent { rootProps={rootProps} data={data} isWideLayout={isWideLayout} - shouldExpandNode={this.shouldExpandNode} + shouldExpandNodeInitially={this.shouldExpandNodeInitially} /> ); } diff --git a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewInfo.tsx b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewInfo.tsx index cbd15a7b..9270dc96 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewInfo.tsx +++ b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewInfo.tsx @@ -1,6 +1,7 @@ import { createSelector, Selector } from '@reduxjs/toolkit'; import { QueryStatus } from '@reduxjs/toolkit/dist/query'; import React, { ReactNode, PureComponent } from 'react'; +import type { ShouldExpandNodeInitially } from 'react-json-tree'; import { QueryPreviewTabs, RtkResourceInfo, RTKStatusFlags } from '../types'; import { renderTabPanelButtonId, renderTabPanelId } from '../utils/a11y'; import { formatMs } from '../utils/formatters'; @@ -35,11 +36,11 @@ export interface QueryPreviewInfoProps { isWideLayout: boolean; } export class QueryPreviewInfo extends PureComponent { - shouldExpandNode = ( - keyPath: (string | number)[], - value: unknown, - layer: number - ): boolean => { + shouldExpandNodeInitially: ShouldExpandNodeInitially = ( + keyPath, + value, + layer + ) => { const lastKey = keyPath[keyPath.length - 1]; return layer <= 1 && lastKey !== 'query' && lastKey !== 'mutation'; @@ -107,7 +108,7 @@ export class QueryPreviewInfo extends PureComponent { rootProps={rootProps} data={formattedQuery} isWideLayout={isWideLayout} - shouldExpandNode={this.shouldExpandNode} + shouldExpandNodeInitially={this.shouldExpandNodeInitially} /> ); } diff --git a/packages/redux-devtools-rtk-query-monitor/src/components/TreeView.tsx b/packages/redux-devtools-rtk-query-monitor/src/components/TreeView.tsx index 091f18f2..61d44e57 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/components/TreeView.tsx +++ b/packages/redux-devtools-rtk-query-monitor/src/components/TreeView.tsx @@ -14,7 +14,7 @@ export interface TreeViewProps extends Partial< Pick< ComponentProps, - 'keyPath' | 'shouldExpandNode' | 'hideRoot' + 'keyPath' | 'shouldExpandNodeInitially' | 'hideRoot' > > { data: unknown; @@ -30,7 +30,7 @@ export interface TreeViewProps export class TreeView extends React.PureComponent { static defaultProps = { hideRoot: true, - shouldExpandNode: ( + shouldExpandNodeInitially: ( keyPath: (string | number)[], value: unknown, layer: number @@ -81,7 +81,7 @@ export class TreeView extends React.PureComponent { after, children, keyPath, - shouldExpandNode, + shouldExpandNodeInitially, hideRoot, rootProps, } = this.props; @@ -94,7 +94,7 @@ export class TreeView extends React.PureComponent { {before} {key} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0cfa35b..f029d298 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -536,7 +536,6 @@ importers: '@types/jest': ^29.2.4 '@types/lodash': ^4.14.191 '@types/node': ^18.11.17 - '@types/prop-types': ^15.7.5 '@types/react': ^18.0.26 '@types/react-test-renderer': ^18.0.0 '@typescript-eslint/eslint-plugin': ^5.47.0 @@ -547,7 +546,6 @@ importers: eslint-plugin-react: ^7.31.11 eslint-plugin-react-hooks: ^4.6.0 jest: ^29.3.1 - prop-types: ^15.8.1 react: ^18.2.0 react-base16-styling: ^0.9.1 react-test-renderer: ^18.2.0 @@ -560,8 +558,6 @@ importers: dependencies: '@babel/runtime': 7.20.6 '@types/lodash': 4.14.191 - '@types/prop-types': 15.7.5 - prop-types: 15.8.1 react-base16-styling: link:../react-base16-styling devDependencies: '@babel/cli': 7.19.3_@babel+core@7.20.5