Skip to content

Instantly share code, notes, and snippets.

@romgrk
Last active March 12, 2024 14:12
Show Gist options
  • Save romgrk/28a6106c56c05f5f932d5d53dbcecfef to your computer and use it in GitHub Desktop.
Save romgrk/28a6106c56c05f5f932d5d53dbcecfef to your computer and use it in GitHub Desktop.
DataGrid event-based reactivity
//
// Event-based reactivity with events corresponding 1:1 with state properties
// I use "path" and "event" interchangeably.
//
// Example state model
type State = {
virtualization: {
offsetTop: number,
renderContext: {
firstRow: number,
firstColumn: number,
}
}
}
// Our new selector interface: a function with its dependencies attached
interface Selector<T = unknown> {
(state: State): T;
// Dependencies as a list of paths like "virtualization.renderContext"
dependencies: string[];
}
// An example path selector
// NOTE: This is the biggest change, we need to wrap our selectors with this helper.
const renderContextSelector = createPathSelector(state => state.virtualization.selector)
// An example derived selector, like we already have
const renderContextColumnSelector = createSelectorMemoized(
renderContextSelector,
(context) => ({
firstRow: -1,
firstColumn: context.firstColumn,
})
)
// Path selector helper, attaches the path for a given selector
function createPathSelector<T>(selector: (state: any) => T) {
function proxyFor(path: string[] = []) {
return new Proxy({}, {
get: (_, part: string) => part === '__path__' ? path : proxyFor(path.concat(part)),
})
}
const state = proxyFor([])
const path = (selector(state) as any).__path__
const result = selector as Selector
result.dependencies = [path.join('.')]
return result
}
// Existing function, only difference is we accumulate dependencies from sources to the
// newly created selectors.
function createSelectorMemoized(...args: Function[]) {
const sources = args.slice(0, -1) as Selector[]
const selector = args.slice(-2, -1)[0] as Selector
selector.dependencies = Array.from(new Set(sources.map(s => s.dependencies).flat()))
function memoizeSelector(..._) { /* same logic we already have */ }
return memoizeSelector(...sources, selector)
}
// Next our store is adapted to have a map of all listeners
type Listener<T> = (value: T) => void;
class Store<T = State> {
value: T;
// Map of listeners, in the format:
// "virtualization" => Set<(value: T['virtualization']) => void>
// "virtualization.renderContext" => Set<(value: T['virtualization']['renderContext']) => void>
listeners: Map<string, Set<Listener<T>>>;
constructor(value: T) {
this.value = value;
this.listeners = new Map();
}
// Subscribe to a given path/event
subscribeToPath = (path: string[], fn: Listener<T>) => {
const pathKey = path.join('.')
let listeners = this.listeners.get(pathKey);
if (!listeners) {
listeners = new Set();
this.listeners.set(pathKey, listeners);
}
listeners.add(fn);
return () => { listeners!.delete(fn) }
}
// Update a path
// This would replace `.setState` calls, because `.setState` isn't aware of which paths inside the
// state are updated.
updatePath = (path: string[], value: any) => {
this.value = setValueByPath(this.value, value, path)
// Dispatch the event along the ancestor listeners, e.g. if someone is listening
// to "virtualization" and we just updated "virtualization.renderContext", then
// we need to emit the event for "virtualization" as well.
for (let i = 0; i < path.length; i++) {
const currentPath = path.slice(0, i + 1)
const listeners = this.listeners.get(currentPath.join('.') as any)
if (listeners) {
const currentValue = getValueByPath(this.value, currentPath)
listeners.forEach((l) => l(currentValue));
}
}
}
}
// And finally the new `useGridSelector` function.
function useGridSelector(apiRef: any, selector: Selector) {
const [state, setState] = useState(() => selector(apiRef.current.state))
useOnMount(() => {
const disposables =
selector.dependencies.map(path =>
apiRef.current.store.subscribeToPath(path, setState))
return () => disposables.forEach(f => f())
})
return state
}
@cherniavskii
Copy link

Btw are pipe processors part of the public API?

It's private. The only public thing is unstable_applyPipeProcessors, but it's unstable and we can change it if necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment