diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/getData/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/getData/info.mdx
index 9abadb531ef..cd937ae186b 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/getData/info.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/getData/info.mdx
@@ -23,7 +23,7 @@ const { getValue, data, filterData, reduceToVisibleFields } =
- `filterData` will filter the data based on your own logic.
- `reduceToVisibleFields` will reduce the given data set to only contain the visible fields (mounted fields).
-You link them together via the `id` (string) property.
+You link them together via the `id` (string, function, object or React Context as the reference) property.
TypeScript support:
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/setData/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/setData/info.mdx
index eac652cd94e..5363b5580b6 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/setData/info.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/setData/info.mdx
@@ -16,7 +16,7 @@ function Component() {
}
```
-You link them together via the `id` (string) property.
+You link them together via the `id` (string, function, object or React Context as the reference) property.
Related helpers:
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useData/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useData/info.mdx
index 34c96df04a6..c46da0a4235 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useData/info.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useData/info.mdx
@@ -42,7 +42,7 @@ render(
## Usage
-You can use the `Form.useData` hook with or without an `id` (string) property, which is optional and can be used to link the data to a specific [Form.Handler](/uilib/extensions/forms/Form/Handler/) component.
+You can use the `Form.useData` hook with or without an `id` (string, function, object or React Context as the reference) property, which is optional and can be used to link the data to a specific [Form.Handler](/uilib/extensions/forms/Form/Handler/) component.
### Without an `id` property
@@ -66,7 +66,7 @@ function Component() {
### With an `id` property
-While in this example, "Component" is outside the `Form.Handler` context, but linked together via the `id` (string) property:
+While in this example, "Component" is outside the `Form.Handler` context, but linked together via the `id` (string, function, object or React Context as the reference) property:
```jsx
import { Form } from '@dnb/eufemia/extensions/forms'
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/info.mdx
index b4d6895f3c1..07ea4ecbf50 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/info.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/info.mdx
@@ -61,7 +61,7 @@ You can check out examples in the demo section.
## Usage of the `Form.useSnapshot` hook
-You can use the `Form.useSnapshot` hook with or without an `id` (string) property, which is optional and can be used to link the data to a specific [Form.Handler](/uilib/extensions/forms/Form/Handler/) component.
+You can use the `Form.useSnapshot` hook with or without an `id` (string, function, object or React Context as the reference) property, which is optional and can be used to link the data to a specific [Form.Handler](/uilib/extensions/forms/Form/Handler/) component.
### Without an `id` property
@@ -85,7 +85,7 @@ function Component() {
### With an `id` property
-While in this example, "Component" is outside the `Form.Handler` context, but linked together via the `id` (string) property:
+While in this example, "Component" is outside the `Form.Handler` context, but linked together via the `id` (string, function, object or React Context as the reference) property:
```jsx
import { Form } from '@dnb/eufemia/extensions/forms'
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useValidation/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useValidation/info.mdx
index 3d74e108e06..963606e8572 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useValidation/info.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useValidation/info.mdx
@@ -45,7 +45,7 @@ function Component() {
}
```
-Or by linking the hook together with the form by using the `id` (string) property:
+Or by linking the hook together with the form by using the `id` (string, function, object or React Context as the reference) property:
```jsx
import { Form } from '@dnb/eufemia/extensions/forms'
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/Container/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/Container/info.mdx
index 89729d6ded8..1161682d231 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/Container/info.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/Container/info.mdx
@@ -117,7 +117,7 @@ const MyForm = () => {
}
```
-When using the `useStep` hook outside of the `Wizard.Container` context, you need to provide an unique `id` (string):
+When using the `useStep` hook outside of the `Wizard.Container` context, you need to provide an unique `id` (string, function, object or React Context as the reference):
```tsx
import { Form, Wizard } from '@dnb/eufemia/extensions/forms'
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/location-hooks/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/location-hooks/info.mdx
index 12512df0d29..40e692bffe9 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/location-hooks/info.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/location-hooks/info.mdx
@@ -106,7 +106,7 @@ function MyForm() {
## Without a router
-You connect the hook with the `Wizard.Container` component via an unique `id` (string). The `id` will be used in the URL query string: `url?unique-id-step=1`.
+You connect the hook with the `Wizard.Container` component via an unique `id` (string, function, object or React Context as the reference). The `id` will be used in the URL query string: `url?unique-id-step=1`.
```jsx
import { Form, Wizard } from '@dnb/eufemia/extensions/forms'
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/useStep/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/useStep/info.mdx
index 0b743c3b7ac..25e58b9851b 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/useStep/info.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/useStep/info.mdx
@@ -26,7 +26,7 @@ function MyForm() {
}
```
-You can also connect the hook with the `Wizard.Container` via an `id` (string). This lets you render the hook outside of the context:
+You can also connect the hook with the `Wizard.Container` via an `id` (string, function, object or React Context as the reference). This lets you render the hook outside of the context:
```jsx
import { Form } from '@dnb/eufemia/extensions/forms'
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx
index 8378af5c301..bcd12191b4f 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx
@@ -128,9 +128,11 @@ Here is an example of how to use these methods:
```jsx
import { Form } from '@dnb/eufemia/extensions/forms'
+const myFormId = 'unique-id' // or a function, object or React Context reference
+
function MyForm() {
return (
-
+
)
@@ -145,15 +147,15 @@ function MyComponent() {
data,
filterData,
reduceToVisibleFields,
- } = Form.useData() // optionally provide an id (unique-id)
+ } = Form.useData() // optionally provide an id or reference
}
// You can also use the setData:
-Form.setData('unique-id', { companyName: 'DNB' })
+Form.setData(myFormId, { companyName: 'DNB' })
// ... and the getData – method when ever you need to:
const { getValue, data, filterData, reduceToVisibleFields } =
- Form.getData('unique-id')
+ Form.getData(myFormId)
```
- `getValue` will return the value of the given path.
diff --git a/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts b/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts
index d78adf557f9..62e684e83bc 100644
--- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts
@@ -7,7 +7,6 @@ import {
Path,
EventStateObject,
EventReturnWithStateObject,
- Identifier,
FieldProps,
ValueProps,
OnChange,
@@ -15,6 +14,7 @@ import {
} from '../types'
import { Props as ProviderProps } from './Provider'
import { SnapshotName } from '../Form/Snapshot'
+import { SharedStateId } from '../../../shared/helpers/useSharedState'
export type MountState = {
isPreMounted?: boolean
@@ -85,7 +85,7 @@ export type FieldConnections = {
}
export interface ContextState {
- id?: Identifier
+ id?: SharedStateId
hasContext: boolean
/** The dataset for the form / form wizard */
data: any
diff --git a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx
index 8d7660d0a9a..4817b0db407 100644
--- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx
@@ -33,7 +33,11 @@ import { debounce } from '../../../../shared/helpers'
import FieldPropsProvider from '../../Field/Provider'
import useUpdateEffect from '../../../../shared/helpers/useUpdateEffect'
import { isAsync } from '../../../../shared/helpers/isAsync'
-import { useSharedState } from '../../../../shared/helpers/useSharedState'
+import {
+ SharedStateId,
+ createReferenceKey,
+ useSharedState,
+} from '../../../../shared/helpers/useSharedState'
import SharedContext, { ContextProps } from '../../../../shared/Context'
import useTranslation from '../../hooks/useTranslation'
import DataContext, {
@@ -74,7 +78,7 @@ export interface Props
/**
* Unique ID to communicate with the hook Form.useData
*/
- id?: string
+ id?: SharedStateId
/**
* Unique ID to connect with a GlobalStatus
*/
@@ -618,10 +622,10 @@ export default function Provider(
// - Shared state
const sharedData = useSharedState(id)
const sharedAttachments = useSharedState>(
- id + '-attachments'
+ createReferenceKey(id, 'attachments')
)
const sharedDataContext = useSharedState(
- id + '-data-context'
+ createReferenceKey(id, 'data-context')
)
const setSharedData = sharedData.set
diff --git a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/ProviderDocs.ts b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/ProviderDocs.ts
index 564b61c28f7..dece5c43721 100644
--- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/ProviderDocs.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/ProviderDocs.ts
@@ -13,7 +13,7 @@ export const ProviderProperties: PropertiesTableProps = {
},
id: {
doc: 'Unique id for connecting Form.Handler and helper tools such as Form.useData.',
- type: 'string',
+ type: ['string', 'Function', 'Object', 'React.Context'],
status: 'optional',
},
schema: {
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Element/__tests__/Element.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Element/__tests__/Element.test.tsx
index 9d87326d7c1..49c25c84137 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Form/Element/__tests__/Element.test.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/Element/__tests__/Element.test.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/ban-ts-comment */
import React from 'react'
import { fireEvent, render } from '@testing-library/react'
import { Form, DataContext, Field } from '../../..'
@@ -151,4 +152,17 @@ describe('Form.Element', () => {
expect(attributes).toEqual(['class', 'aria-label'])
expect(formElement.getAttribute('aria-label')).toBe('Aria Label')
})
+
+ it('should ensure that only a string can be set as the id', () => {
+ const myId = () => null
+ render(
+ // @ts-expect-error
+
+ Submit
+
+ )
+
+ const formElement = document.querySelector('form')
+ expect(formElement).not.toHaveAttribute('id')
+ })
})
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/data-context/__tests__/useData.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/data-context/__tests__/useData.test.tsx
index 2987e3c3719..65bf6dfcf09 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Form/data-context/__tests__/useData.test.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/data-context/__tests__/useData.test.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { createContext } from 'react'
import { renderHook, act, render, fireEvent } from '@testing-library/react'
import { makeUniqueId } from '../../../../../shared/component-helper'
import { Field, Form, Wizard } from '../../..'
@@ -124,6 +124,89 @@ describe('Form.useData', () => {
expect(result.current.data).toEqual({ key: 'changed value' })
})
+ it('should get data with a string as the id', () => {
+ const { result } = renderHook(() => useData(identifier), {
+ wrapper: ({ children }) => (
+ <>
+
+
+
+
+
+ {children}
+ >
+ ),
+ })
+
+ expect(result.current.data).toEqual({
+ foo: 'foo',
+ bar: 'bar',
+ })
+ })
+
+ it('should get data with a function reference as the id', () => {
+ const myId = () => null
+ const { result } = renderHook(() => useData(myId), {
+ wrapper: ({ children }) => (
+ <>
+
+
+
+
+
+ {children}
+ >
+ ),
+ })
+
+ expect(result.current.data).toEqual({
+ foo: 'foo',
+ bar: 'bar',
+ })
+ })
+
+ it('should get data with an object reference as the id', () => {
+ const myId = {}
+ const { result } = renderHook(() => useData(myId), {
+ wrapper: ({ children }) => (
+ <>
+
+
+
+
+
+ {children}
+ >
+ ),
+ })
+
+ expect(result.current.data).toEqual({
+ foo: 'foo',
+ bar: 'bar',
+ })
+ })
+
+ it('should get data with a React Context as the id', () => {
+ const myId = createContext(null)
+ const { result } = renderHook(() => useData(myId), {
+ wrapper: ({ children }) => (
+ <>
+
+
+
+
+
+ {children}
+ >
+ ),
+ })
+
+ expect(result.current.data).toEqual({
+ foo: 'foo',
+ bar: 'bar',
+ })
+ })
+
describe('remove', () => {
it('should remove the data', () => {
const { result } = renderHook(() => useData(), {
@@ -177,6 +260,35 @@ describe('Form.useData', () => {
})
expect(result.current.data).not.toHaveProperty('foo')
})
+
+ it('should remove data with handler id', () => {
+ const { result } = renderHook(() => useData(identifier), {
+ wrapper: ({ children }) => (
+ <>
+
+
+
+
+
+ {children}
+ >
+ ),
+ })
+
+ expect(result.current.data).toEqual({
+ foo: 'foo',
+ bar: 'bar',
+ })
+
+ act(() => {
+ result.current.remove('/foo')
+ })
+
+ expect(result.current.data).toEqual({
+ bar: 'bar',
+ })
+ expect(result.current.data).not.toHaveProperty('foo')
+ })
})
it('"update" should only re-render when value has changed', () => {
@@ -225,40 +337,71 @@ describe('Form.useData', () => {
expect(result.current.data).toEqual({ key: 'changed value' })
})
- it('should sync two hooks by using "update"', () => {
- const props = { key: 'value' }
+ describe('update', () => {
+ it('should sync two hooks by using "update"', () => {
+ const props = { key: 'value' }
- const { result: A } = renderHook(() => useData(identifier))
- const { result: B } = renderHook(() => useData(identifier, props))
+ const { result: A } = renderHook(() => useData(identifier))
+ const { result: B } = renderHook(() => useData(identifier, props))
- expect(A.current.data).toEqual({ key: 'value' })
- expect(B.current.data).toEqual({ key: 'value' })
+ expect(A.current.data).toEqual({ key: 'value' })
+ expect(B.current.data).toEqual({ key: 'value' })
- act(() => {
- B.current.update('/key', (value) => {
- return 'changed ' + value
+ act(() => {
+ B.current.update('/key', (value) => {
+ return 'changed ' + value
+ })
})
+
+ expect(A.current.data).toEqual({ key: 'changed value' })
+ expect(B.current.data).toEqual({ key: 'changed value' })
})
- expect(A.current.data).toEqual({ key: 'changed value' })
- expect(B.current.data).toEqual({ key: 'changed value' })
- })
+ it('should support update without a function', () => {
+ const props = { key: 'value' }
- it('should support update without a function', () => {
- const props = { key: 'value' }
+ const { result: A } = renderHook(() => useData(identifier))
+ const { result: B } = renderHook(() => useData(identifier, props))
- const { result: A } = renderHook(() => useData(identifier))
- const { result: B } = renderHook(() => useData(identifier, props))
+ expect(A.current.data).toEqual({ key: 'value' })
+ expect(B.current.data).toEqual({ key: 'value' })
- expect(A.current.data).toEqual({ key: 'value' })
- expect(B.current.data).toEqual({ key: 'value' })
+ act(() => {
+ B.current.update('/key', 'new value')
+ })
- act(() => {
- B.current.update('/key', 'new value')
+ expect(A.current.data).toEqual({ key: 'new value' })
+ expect(B.current.data).toEqual({ key: 'new value' })
})
- expect(A.current.data).toEqual({ key: 'new value' })
- expect(B.current.data).toEqual({ key: 'new value' })
+ it('should update data with handler id', () => {
+ const { result } = renderHook(() => useData(identifier), {
+ wrapper: ({ children }) => (
+ <>
+
+
+
+
+
+ {children}
+ >
+ ),
+ })
+
+ expect(result.current.data).toEqual({
+ foo: 'foo',
+ bar: 'bar',
+ })
+
+ act(() => {
+ result.current.update('/foo', 'updated')
+ })
+
+ expect(result.current.data).toEqual({
+ foo: 'updated',
+ bar: 'bar',
+ })
+ })
})
it('should rerender when shared state calls "set"', () => {
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/data-context/clearData.ts b/packages/dnb-eufemia/src/extensions/forms/Form/data-context/clearData.ts
index 9e79c2047b8..4fd934c85e5 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Form/data-context/clearData.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/data-context/clearData.ts
@@ -1,9 +1,13 @@
-import { createSharedState } from '../../../../shared/helpers/useSharedState'
+import {
+ SharedStateId,
+ createReferenceKey,
+ createSharedState,
+} from '../../../../shared/helpers/useSharedState'
import { SharedAttachments } from '../../DataContext/Provider'
-export default function clearData(id: string) {
+export default function clearData(id: SharedStateId) {
const sharedAttachments = createSharedState>(
- id + '-attachments'
+ createReferenceKey(id, 'attachments')
)
sharedAttachments.data.clearData?.()
}
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/data-context/getData.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/data-context/getData.tsx
index 3265f595264..f1bca9147b1 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Form/data-context/getData.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/data-context/getData.tsx
@@ -1,6 +1,7 @@
import pointer from '../../utils/json-pointer'
import {
SharedStateId,
+ createReferenceKey,
createSharedState,
} from '../../../../shared/helpers/useSharedState'
import { SharedAttachments } from '../../DataContext/Provider'
@@ -23,7 +24,7 @@ export default function getData(
): SetDataReturn {
const sharedState = createSharedState(id)
const sharedAttachments = createSharedState>(
- id + '-attachments'
+ createReferenceKey(id, 'attachments')
)
const data = sharedState.get() as Data
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/data-context/useData.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/data-context/useData.tsx
index ba8ed93217c..8e386c358b5 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Form/data-context/useData.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/data-context/useData.tsx
@@ -8,6 +8,7 @@ import {
import pointer, { JsonObject } from '../../utils/json-pointer'
import {
SharedStateId,
+ createReferenceKey,
useSharedState,
} from '../../../../shared/helpers/useSharedState'
import useMountEffect from '../../../../shared/helpers/useMountEffect'
@@ -89,7 +90,7 @@ export default function useData(
)
sharedAttachmentsRef.current = useSharedState>(
- id + '-attachments',
+ createReferenceKey(id, 'attachments'),
{ rerenderUseDataHook: forceUpdate }
)
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/data-context/useValidation.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/data-context/useValidation.tsx
index 6b75302953a..e6b00106e04 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Form/data-context/useValidation.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/data-context/useValidation.tsx
@@ -1,6 +1,7 @@
import { useCallback, useContext, useMemo } from 'react'
import {
SharedStateId,
+ createReferenceKey,
useSharedState,
} from '../../../../shared/helpers/useSharedState'
import DataContext, { ContextState } from '../../DataContext/Context'
@@ -19,7 +20,7 @@ export default function useValidation(
): UseDataReturn {
const { data } = useSharedState<
UseDataReturn & SharedAttachments
- >(id + '-attachments')
+ >(createReferenceKey(id, 'attachments'))
const fallback = useCallback(() => false, [])
@@ -62,7 +63,7 @@ type UseConnectionsSharedState = {
function useConnections(id: SharedStateId = undefined) {
const { get } = useSharedState(
- id + '-attachments'
+ createReferenceKey(id, 'attachments')
)
const dataContext = useContext(DataContext)
diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useDataContext.tsx b/packages/dnb-eufemia/src/extensions/forms/hooks/useDataContext.tsx
index a8116150377..9c2f020983a 100644
--- a/packages/dnb-eufemia/src/extensions/forms/hooks/useDataContext.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useDataContext.tsx
@@ -1,6 +1,7 @@
import { useCallback, useContext } from 'react'
import {
SharedStateId,
+ createReferenceKey,
useSharedState,
} from '../../../shared/helpers/useSharedState'
import DataContext, { ContextState } from '../DataContext/Context'
@@ -10,7 +11,7 @@ export default function useDataContext(id: SharedStateId = undefined): {
getContext: () => ContextState
} {
const sharedDataContext = useSharedState(
- id + '-data-context'
+ createReferenceKey(id, 'data-context')
)
const dataContext = useContext(DataContext)
diff --git a/packages/dnb-eufemia/src/shared/helpers/__tests__/useSharedState.test.ts b/packages/dnb-eufemia/src/shared/helpers/__tests__/useSharedState.test.ts
index 104ee5ffc67..5fe8b8e566a 100644
--- a/packages/dnb-eufemia/src/shared/helpers/__tests__/useSharedState.test.ts
+++ b/packages/dnb-eufemia/src/shared/helpers/__tests__/useSharedState.test.ts
@@ -1,9 +1,15 @@
import { renderHook, act } from '@testing-library/react'
import { makeUniqueId } from '../../component-helper'
-import { useSharedState, createSharedState } from '../useSharedState'
+import {
+ useSharedState,
+ createSharedState,
+ SharedStateId,
+ createReferenceKey,
+} from '../useSharedState'
+import { createContext } from 'react'
describe('useSharedState', () => {
- let identifier: string
+ let identifier: SharedStateId
beforeEach(() => {
identifier = makeUniqueId()
@@ -47,6 +53,50 @@ describe('useSharedState', () => {
expect(result.current.data).toEqual({ test: 'changed' })
})
+ it('should update the shared state with a function reference as id', () => {
+ const identifier = () => null
+ const { result } = renderHook(() =>
+ useSharedState(identifier, { test: 'initial' })
+ )
+ act(() => {
+ result.current.update({ test: 'updated' })
+ })
+ expect(result.current.data).toEqual({ test: 'updated' })
+ })
+
+ it('should update the shared state with an async function reference as id', () => {
+ const identifier = async () => null
+ const { result } = renderHook(() =>
+ useSharedState(identifier, { test: 'initial' })
+ )
+ act(() => {
+ result.current.update({ test: 'updated' })
+ })
+ expect(result.current.data).toEqual({ test: 'updated' })
+ })
+
+ it('should update the shared state with an object reference as id', () => {
+ const identifier = {}
+ const { result } = renderHook(() =>
+ useSharedState(identifier, { test: 'initial' })
+ )
+ act(() => {
+ result.current.update({ test: 'updated' })
+ })
+ expect(result.current.data).toEqual({ test: 'updated' })
+ })
+
+ it('should update the shared state with a React context reference as id', () => {
+ const identifier = createContext(null)
+ const { result } = renderHook(() =>
+ useSharedState(identifier, { test: 'initial' })
+ )
+ act(() => {
+ result.current.update({ test: 'updated' })
+ })
+ expect(result.current.data).toEqual({ test: 'updated' })
+ })
+
it('should unsubscribe from the shared state when the component unmounts', () => {
const { result, unmount } = renderHook(() =>
useSharedState(identifier, { test: 'initial' })
@@ -171,3 +221,66 @@ describe('useSharedState', () => {
expect(resultB.current.data).toEqual({ foo: 'baz' })
})
})
+
+describe('createReferenceKey', () => {
+ it('should return the same object for the same references', () => {
+ const ref1 = {}
+ const ref2 = () => null
+
+ const key1 = createReferenceKey(ref1, ref2)
+ const key2 = createReferenceKey(ref1, ref2)
+
+ expect(key1).toBe(key2)
+ })
+
+ it('should return the same object for the same string references', () => {
+ const ref1 = {}
+ const ref2 = 'unique'
+
+ const key1 = createReferenceKey(ref1, ref2)
+ const key2 = createReferenceKey(ref1, ref2)
+
+ expect(key1).toBe(key2)
+ })
+
+ it('should return different objects for different references', () => {
+ const ref1 = {}
+ const ref2 = {}
+ const ref3 = {}
+
+ const key1 = createReferenceKey(ref1, ref2)
+ const key2 = createReferenceKey(ref1, ref3)
+
+ expect(key1).not.toBe(key2)
+ })
+
+ it('should return different objects for different first references', () => {
+ const ref1 = {}
+ const ref2 = {}
+ const ref3 = {}
+
+ const key1 = createReferenceKey(ref1, ref2)
+ const key2 = createReferenceKey(ref3, ref2)
+
+ expect(key1).not.toBe(key2)
+ })
+
+ it('should cache the combined reference', () => {
+ const ref1 = {}
+ const ref2 = () => null
+
+ const key1 = createReferenceKey(ref1, ref2)
+ const key2 = createReferenceKey(ref1, ref2)
+
+ expect(key1).toBe(key2)
+ })
+
+ it('should create a new reference if it does not exist', () => {
+ const ref1 = {}
+ const ref2 = {}
+
+ const key1 = createReferenceKey(ref1, ref2)
+
+ expect(key1).toBeDefined()
+ })
+})
diff --git a/packages/dnb-eufemia/src/shared/helpers/useSharedState.tsx b/packages/dnb-eufemia/src/shared/helpers/useSharedState.tsx
index 4fb9fe76bdf..6d837bceb3f 100644
--- a/packages/dnb-eufemia/src/shared/helpers/useSharedState.tsx
+++ b/packages/dnb-eufemia/src/shared/helpers/useSharedState.tsx
@@ -12,20 +12,22 @@ import useMountEffect from './useMountEffect'
const useLayoutEffect =
typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect
-export type SharedStateId = string
+export type SharedStateId =
+ | string
+ | (() => void)
+ | Promise<() => void>
+ | React.Context
+ | Record
/**
* Custom hook that provides shared state functionality.
- *
- * @template Data - The type of data stored in the shared state.
- * @param {SharedStateId} id - The identifier for the shared state.
- * @param {Data} initialData - The initial data for the shared state.
- * @param {Function} onChange - Optional callback function to be called when the shared state is set from another instance/component.
- * @returns {Object} - An object containing the shared state data, update function, extend function, and set function.
*/
export function useSharedState(
- id: SharedStateId,
+ /** The identifier for the shared state. */
+ id: SharedStateId | undefined,
+ /** The initial data for the shared state. */
initialData: Data = undefined,
+ /** Optional callback function to be called when the shared state is set from another instance/component. */
onChange = null
) {
const [, forceUpdate] = useReducer(() => ({}), {})
@@ -53,7 +55,7 @@ export function useSharedState(
}, [id, initialData])
const sharedAttachment = useMemo(() => {
if (id) {
- return createSharedState(id + '-oc', { onChange })
+ return createSharedState(createReferenceKey(id, 'oc'), { onChange })
}
}, [id, onChange])
@@ -141,26 +143,27 @@ interface SharedStateInstance extends SharedStateReturn {
hadInitialData: boolean
}
-const sharedStates: Record> = {}
+const sharedStates: Map<
+ SharedStateId,
+ SharedStateInstance
+> = new Map()
/**
* Creates a shared state instance with the specified ID and initial data.
- * @template Data The type of data stored in the shared state.
- * @param id The ID of the shared state.
- * @param initialData The initial data for the shared state.
- * @returns The created shared state instance.
*/
export function createSharedState(
+ /** The identifier for the shared state. */
id: SharedStateId,
+ /** The initial data for the shared state. */
initialData?: Data
): SharedStateInstance {
- if (!sharedStates[id]) {
+ if (!sharedStates.get(id)) {
let subscribers: Subscriber[] = []
- const get = () => sharedStates[id].data
+ const get = () => sharedStates.get(id).data
const set = (newData: Partial) => {
- sharedStates[id].data = { ...newData }
+ sharedStates.get(id).data = { ...newData }
}
const update = (newData: Partial) => {
@@ -169,7 +172,10 @@ export function createSharedState(
}
const extend = (newData: Data) => {
- sharedStates[id].data = { ...sharedStates[id].data, ...newData }
+ sharedStates.get(id).data = {
+ ...sharedStates.get(id).data,
+ ...newData,
+ }
sync()
}
@@ -187,7 +193,7 @@ export function createSharedState(
subscribers.forEach((subscriber) => subscriber())
}
- sharedStates[id] = {
+ sharedStates.set(id, {
data: undefined,
get,
set,
@@ -196,17 +202,36 @@ export function createSharedState(
subscribe,
unsubscribe,
hadInitialData: Boolean(initialData),
- } as SharedStateInstance
+ } as SharedStateInstance)
if (initialData) {
extend(initialData)
}
} else if (
- sharedStates[id].data === undefined &&
+ sharedStates.get(id).data === undefined &&
initialData !== undefined
) {
- sharedStates[id].data = { ...initialData }
+ sharedStates.get(id).data = { ...initialData }
+ }
+
+ return sharedStates.get(id)
+}
+
+/**
+ * Creates a reference key for the shared state.
+ * You can pass any JavaScript instance as the reference.
+ */
+export function createReferenceKey(ref1, ref2) {
+ if (!cache.has(ref1)) {
+ cache.set(ref1, new Map())
+ }
+
+ const innerMap = cache.get(ref1)
+
+ if (!innerMap.has(ref2)) {
+ innerMap.set(ref2, {})
}
- return sharedStates[id]
+ return innerMap.get(ref2)
}
+const cache = new Map()