react-dock-system
v0.1.3
Published
Headless components for managing multi-dock layouts
Readme
React Dock System
Usage
Getting Started
This library provides helpers and headless components to help you define a system for managing and rendering dock instances in a consistent, extensible way.
- You define the kinds of docks that should be available in your application. For each kind, the minimum you need to provide is a React component for rendering a dock instance. Additional configuration is available to support features like deeplinking and integrating with navigation state.
- The library provides ready-to-use components, hooks, and interfaces to control everything about the system you've defined. For most functionalities, you can also choose to implement your own custom logic instead if the out-of-the-box behaviors do not work for you.
Defining your system
Everything starts with what you need in your application - the kinds of docks you want to render. You can define them like this:
import { defineSystem } from 'react-dock-system'
defineSystem({
// Define the different kinds of dock element you want to use here.
docks: {
// This config will not sync docks with navigation state
document: {
// render: ...,
},
// This config will sync currently open docks with the `user` search param,
// so that users can use deeplinks and back button navigation for example
userProfile: {
paramName: 'user',
// render: ...,
},
// This config will use a singleton pattern, where only one dock of this kind
// can exist at most. Instead of additional docks being created as the user interacts
// with the app, the existing dock will be updated in-place.
notes: {
isSingleton: true,
// render: ...,
},
},
})Writing render components
For each dock kind, you must provide the React component that should be used to render the dock instances.
- The provided component must accept a
ref. - The provided component will receive the following props:
args: the payload associated with the dock. This uniquely identifies the dock relative to other docks of the same kind, for example you can define a "user profile" dock kind that expects a payload with the shape{ userId: number }. When opening the profile of user123in a dock, theargsprop would have the value{ userId: 123 }.remove: a callback for destroying the dock. This takes no arguments.
import { forwardRef } from 'react'
import type { DockComponentProps, DockRef } from 'react-dock-system'
// The component should expect a `ref` of type DockRef.
// It doesn't have to set the ref, but it must accept it.
// For props, you can use the generic interface DockComponentProps,
// and provide as the type parameter the typing for the `args` payload.
const UserProfileDock = forwardRef<DockRef, DockComponentProps<{ userId: number }>>((props, ref) => {
const { args, remove } = props
// You can use any business logic you might want here, e.g. fetching data
return (
<div>
<div>User ID: {args.userId}</div>
<button type='button' onClick={remove}>Close</button>
</div>
)
})import { forwardRef, useImperativeHandle } from 'react'
import type { DockComponentProps, DockRef } from 'react-dock-system'
const DocumentDock = forwardRef<DockRef, DockComponentProps<string>>((props, ref) => {
const [isExpanded, setIsExpanded] = useState(true)
// You can define the ref imperatively to implement additional functionality:
useImperativeHandle(ref, () => ({
// This `onSelect` handler will be called when a user attempts to open a dock that already exists.
// It can be useful if you want to implement minimization, and open the existing dock in that scenario
// if it had been minimized:
onSelect: () => setIsExpanded(true),
}), [])
return (
// ...
)
})
Then, provide these components when defining your system:
import { defineSystem } from 'react-dock-system'
defineSystem({
// Define the different kinds of dock element you want to use here.
docks: {
// This config will not sync docks with navigation state
document: {
render: DocumentDock,
},
// This config will sync currently open docks with the `user` search param,
// so that users can use deeplinks and back button navigation for example
userProfile: {
paramName: 'user',
render: UserProfileDock,
}
},
})Integrating with your application
When defining your system, the library will return an object ready to be passed to the library's DockSystemProvider component, which should wrap your application.
import { defineSystem } from 'react-dock-system'
export const system = defineSystem({
docks: {
document: { ... },
userProfile: { ... },
},
})import { DockSystemProvider } from 'react-dock-system'
const App = () => {
return (
<DockSystemProvider system={system}>
// Your app contents
</DockSystemProvider>
)
}The library can generate named hooks that you can use to open content in docks in your application:
import { hooksFactory } from 'react-dock-system'
// A hook will be available for each dock kind in the array passed to the factory
export const { useDocumentDock, useUserProfileDock } = hooksFactory()(['document', 'userProfile'])With Typescript:
import { hooksFactory } from 'react-dock-system'
// Define a mapping between each dock kind and the args for that dock
export interface DocksMapping {
document: string
userProfile: { userId: number }
}
// Then pass the mapping as a type parameter to the factory, so that the hooks are fully typed
export const { useDocumentDock, useUserProfileDock } = hooksFactory<DocksMapping>()(['document', 'userProfile'])
// Note that if you define a mapping type here, you'll likely want to pass it as a type parameter
// when defining the system to ensure they are in sync with each other:
// export const system = defineSystem<DocksMapping>({ ... })Then, you can use the hooks anywhere within the provider (including inside of your dock components, i.e. you can enable users to open a dock from within another dock):
import { useCallback } from 'react'
import { DockSystemProvider } from 'react-dock-system'
const AppContents = () => {
// Each hook provides a stable handler for opening a dock
const [openUserProfileDock] = useUserProfileDock()
const openUserProfile = useCallback(() => {
// The handler expects to receive the `args` for that dock.
// This is typed based on the config you defined.
openUserProfileDock({ userId: 123 })
}, [openUserProfileDock])
return (
<button type='button' onClick={openUserProfile}>View user</button>
)
}
const App = () => {
return (
<DockSystemProvider config={config}>
<AppContents />
</DockSystemProvider>
)
}Rendering current docks
Finally, the library provides utilities for accessing and rendering the list of current docks:
- To access the current docks context, use the
useDockshook. - To render a given dock instance, pass it to the
Dockcomponent.
import { Dock, useDocks } from 'react-dock-system'
const DockBar = () => {
const docks = useDocks()
return (
<div>
{docks.map(({ key, ref, ...dockInstance }) => (
<Dock key={key} ref={ref} {...dockInstance} />
))}
</div>
)
}Advanced usage
Custom params serialization
By default, the library uses Rison to serialize dock arguments to search params. You can provide your own encoder/decoder by using system options:
import { defineSystem } from 'react-dock-system'
const { config, hooks } = defineSystem({
docks: { ... },
options: {
decode: <T>(serializedValue: string): T => ...,
encode: <T>(argsValue: T): string => ...,
},
})Custom router integration
By default, the library uses the experimental Navigation API to listen for navigation state changes and ensure docks are synced with the current state. Two flavors are available: useWindowRouter (default; this expects search params to be located in window.location.search) and useHashWindowRouter (this expects search params to be located inside of window.location.hash; it's an unconventional scenario, but you may need this if you use certain hash-based routers like the one implemented by React Router).
If you want to use this default behavior, you may want to use a polyfill in your application (like @virtualstate/navigation) to extend the range of browsers that are supported.
If you use a router library that provides its own API for subscribing to and setting the navigation state, you can simply write your own hook so that react-dock-system integrates directly with the stack you're already using. See the playground example for a reference.
Development
Source files are located under /src. A playground project is setup under /example to demonstrate how the library can be used in an application.
Setup
To install the project:
npm install # main project
cd example && npm install # playground projectTo use the playground, build the project then start the application:
# in the root directory
npm run build
# then, switch to the playground project
cd example && npm startMake sure to run the build step each time you want to test changes to the source code using the playground.
Tests
You can run the library's tests with Vitest:
npm testGit hooks
The project uses Lefthook to manage useful Git hooks. There are no additional binaries to install, simply run npm install and everything will be configured.
