Last active
March 12, 2024 14:12
-
-
Save romgrk/28a6106c56c05f5f932d5d53dbcecfef to your computer and use it in GitHub Desktop.
DataGrid event-based reactivity
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
// | |
// 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 | |
} |
I think it should be ok. I have spotted a few tricky cases but it should be fine.
Btw are pipe processors part of the public API? Removing some of them might help, because they're kindof like selectors in some cases.
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
The changes seem to be non-breaking (unless you have custom state selectors – they will need to be wrapped with
createPathSelector
after this change).Do you think it's safe to do this in v7 stable?