-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add view mounted suspense hook
Add an example usage for how to use the useViewReadySuspense hook.
- Loading branch information
Showing
10 changed files
with
214 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
export type EventListener = (...args: any[]) => void; | ||
|
||
function createEvent() { | ||
const callbacks: EventListener[] = []; | ||
|
||
const off = (callback: EventListener) => { | ||
const idx = callbacks.indexOf(callback); | ||
if (idx === -1) return; | ||
callbacks.splice(idx, 1); | ||
}; | ||
|
||
const on = (callback: EventListener) => { | ||
callbacks.push(callback); | ||
return () => off(callback); | ||
}; | ||
|
||
const once = (callback: EventListener) => { | ||
const stop = on(() => { | ||
stop(); | ||
callback(); | ||
}); | ||
}; | ||
|
||
const trigger = (...args: any[]) => { | ||
callbacks.forEach((cb) => { | ||
cb(...args); | ||
}); | ||
}; | ||
|
||
return { off, on, once, trigger }; | ||
} | ||
|
||
export const viewMountedEvent = createEvent(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './useViewReadySuspense'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { useContext } from 'react'; | ||
import { Contexts } from '..'; | ||
import { viewMountedEvent } from '../core/internal/events'; | ||
import { makeDeferred } from '../utils/deferred'; | ||
|
||
type Status = 'pending' | 'error' | 'success'; | ||
|
||
/** | ||
* A suspense-aware hook that waits for the containing View to be mounted before evaluating the getter. | ||
* @param getter | ||
* @returns | ||
*/ | ||
export function useViewReadySuspense<T>(getter: () => T): T { | ||
const view = useContext(Contexts.ViewContext); | ||
if (!view) throw new Error('No view context'); | ||
|
||
let status = 'pending' as Status; | ||
const deferred = makeDeferred<void>(); | ||
|
||
if (view.isMounted()) { | ||
status = 'success'; | ||
} else { | ||
viewMountedEvent.once(() => { | ||
status = 'success'; | ||
deferred.resolve(); | ||
}); | ||
} | ||
|
||
switch (status) { | ||
case 'success': | ||
return getter(); | ||
case 'pending': | ||
throw deferred.promise; | ||
case 'error': | ||
default: | ||
throw new Error('Unexpected unreachable code execution'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// eslint-disable-next-line @typescript-eslint/no-empty-function | ||
const empty = () => {}; | ||
|
||
export function makeDeferred<T>() { | ||
let resolve: (value: T) => void = empty; | ||
let reject: (error: unknown) => void = empty; | ||
const promise = new Promise<T>((_resolve, _reject) => { | ||
resolve = _resolve; | ||
reject = _reject; | ||
}); | ||
return { resolve, reject, promise }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { Suspense, useRef, useState } from 'react'; | ||
|
||
import { | ||
Algorithm, | ||
DataArray, | ||
GeometryRepresentation, | ||
PointData, | ||
PolyData, | ||
useViewContext, | ||
useViewReadySuspense, | ||
View, | ||
} from 'react-vtk-js'; | ||
|
||
import vtkConeSource from '@kitware/vtk.js/Filters/Sources/ConeSource'; | ||
|
||
const styles = { | ||
position: 'relative', | ||
zIndex: 10, | ||
textAlign: 'center', | ||
color: 'white', | ||
}; | ||
|
||
function Inner() { | ||
const view = useViewContext(); | ||
const renderer = view.getRenderer(); | ||
return ( | ||
<div style={styles}> | ||
Without Suspense: has renderer = {String(!!renderer)} | ||
</div> | ||
); | ||
} | ||
|
||
function InnerSuspense() { | ||
const useRenderer = () => useViewContext().getRenderer(); | ||
const renderer = useViewReadySuspense(useRenderer); | ||
return ( | ||
<div style={styles}>With Suspense: has renderer = {String(!!renderer)}</div> | ||
); | ||
} | ||
|
||
// React complains about unique key prop but I don't see why | ||
function Example() { | ||
const view = useRef(); | ||
const rep = useRef(); | ||
|
||
const [height, setHeight] = useState(1); | ||
const [colorByName, setColorByName] = useState('Temperature'); | ||
const [preset, setPreset] = useState('Black-Body Radiation'); | ||
const [range, setRange] = useState([0, 0.7]); | ||
|
||
const randomize = () => { | ||
setHeight(Math.random() * 3); | ||
setColorByName(Math.random() < 0.5 ? 'Temperature' : 'Pressure'); | ||
setPreset(Math.random() < 0.5 ? 'Black-Body Radiation' : 'Cool to Warm'); | ||
setRange([0, Math.random()]); | ||
}; | ||
|
||
return ( | ||
<div style={{ width: '100%', height: '100%', position: 'relative' }}> | ||
<View ref={view}> | ||
<Inner /> | ||
<Suspense> | ||
<InnerSuspense /> | ||
</Suspense> | ||
<GeometryRepresentation ref={rep}> | ||
<Algorithm | ||
vtkClass={vtkConeSource} | ||
state={{ | ||
height, | ||
}} | ||
/> | ||
</GeometryRepresentation> | ||
<GeometryRepresentation | ||
ref={rep} | ||
mapper={{ | ||
colorByArrayName: colorByName, | ||
scalarMode: colorByName === 'Temperature' ? 0 : 3, | ||
interpolateScalarsBeforeMapping: true, | ||
}} | ||
colorDataRange={range} | ||
colorMapPreset={preset} | ||
showScalarBar | ||
> | ||
<PolyData | ||
points={[0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0]} | ||
polys={[4, 0, 3, 2, 1]} | ||
> | ||
<PointData> | ||
<DataArray | ||
registration='setScalars' | ||
name='Temperature' | ||
values={[0, 0.7, 0.3, 1]} | ||
/> | ||
<DataArray name='Pressure' values={[0.5, 0.3, 0.7, 0]} /> | ||
</PointData> | ||
</PolyData> | ||
</GeometryRepresentation> | ||
</View> | ||
<div style={{ position: 'absolute', top: 0 }}> | ||
<button onClick={randomize}>Randomize</button> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export default Example; |