Merge pull request #26928 from overleaf/td-remove-use-scope-value

Remove useScopeValue and its associated store

GitOrigin-RevId: 439d6eb16343f65695ef615a9ff697d0cc5ad2c7
This commit is contained in:
Tim Down
2025-07-10 08:53:47 +01:00
committed by Copybot
parent e4a9b13e9a
commit bfa0459e72
7 changed files with 5 additions and 161 deletions

View File

@@ -42,33 +42,8 @@ export const IdeReactContext = createContext<IdeReactContextValue | undefined>(
undefined
)
function populateIdeReactScope(store: ReactScopeValueStore) {
store.set('settings', {})
}
function populatePdfScope(store: ReactScopeValueStore) {
store.allowNonExistentPath('pdf', true)
}
export function createReactScopeValueStore() {
const scopeStore = new ReactScopeValueStore()
// Populate the scope value store with default values that will be used by
// nested contexts that refer to scope values. The ideal would be to leave
// initialization of store values up to the nested context, which would keep
// initialization code together with the context and would only populate
// necessary values in the store, but this is simpler for now
populateIdeReactScope(scopeStore)
populatePdfScope(scopeStore)
scopeStore.allowNonExistentPath('hasLintingError')
return scopeStore
}
export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
const projectId = getMeta('ol-project_id')
const [scopeStore] = useState(() => createReactScopeValueStore())
const [eventEmitter] = useState(createIdeEventEmitter)
const [permissionsLevel, setPermissionsLevel] =
useState<PermissionsLevel>('readOnly')
@@ -146,8 +121,6 @@ export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
joinProject(project as unknown as ProjectMetadata)
setPermissionsLevel(permissionsLevel)
// Make watchers update immediately
scopeStore.flushUpdates()
eventEmitter.emit('project:joined', { project, permissionsLevel })
setProjectJoined(true)
}
@@ -157,7 +130,7 @@ export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
return () => {
socket.removeListener('joinProjectResponse', handleJoinProjectResponse)
}
}, [socket, eventEmitter, scopeStore, joinProject])
}, [socket, eventEmitter, joinProject])
const ide = useMemo(() => {
return {
@@ -194,7 +167,6 @@ export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
<IdeReactContext.Provider value={value}>
<IdeProvider
ide={ide}
scopeStore={scopeStore}
scopeEventEmitter={scopeEventEmitter}
unstableStore={unstableStore}
>

View File

@@ -8,7 +8,6 @@ export type Ide = {
}
type IdeContextValue = Ide & {
scopeStore: ScopeValueStore
scopeEventEmitter: ScopeEventEmitter
unstableStore: ScopeValueStore
}
@@ -18,11 +17,10 @@ export const IdeContext = createContext<IdeContextValue | undefined>(undefined)
export const IdeProvider: FC<
React.PropsWithChildren<{
ide: Ide
scopeStore: ScopeValueStore
scopeEventEmitter: ScopeEventEmitter
unstableStore: ScopeValueStore
}>
> = ({ ide, scopeStore, scopeEventEmitter, unstableStore, children }) => {
> = ({ ide, scopeEventEmitter, unstableStore, children }) => {
/**
* Expose unstableStore via `window.overleaf.unstable.store`, so it can be accessed by external extensions.
*
@@ -49,11 +47,10 @@ export const IdeProvider: FC<
const value = useMemo<IdeContextValue>(() => {
return {
...ide,
scopeStore,
scopeEventEmitter,
unstableStore,
}
}, [ide, scopeStore, scopeEventEmitter, unstableStore])
}, [ide, scopeEventEmitter, unstableStore])
return <IdeContext.Provider value={value}>{children}</IdeContext.Provider>
}

View File

@@ -1,38 +0,0 @@
import {
type Dispatch,
type SetStateAction,
useCallback,
useState,
} from 'react'
import { useIdeContext } from '../context/ide-context'
import _ from 'lodash'
/**
* Similar to `useScopeValue`, but instead of creating a two-way binding, only
* changes in react-> angular direction are propagated, with `value` remaining
* local and independent of its value in the Angular scope.
*
* The interface is compatible with React.useState(), including
* the option of passing a function to the setter.
*/
export default function useScopeValueSetterOnly<T = any>(
path: string, // dot '.' path of a property in the Angular scope.
defaultValue?: T
): [T | undefined, Dispatch<SetStateAction<T | undefined>>] {
const { scopeStore } = useIdeContext()
const [value, setValue] = useState<T | undefined>(defaultValue)
const scopeSetter = useCallback(
(newValue: SetStateAction<T | undefined>) => {
setValue(val => {
const actualNewValue = _.isFunction(newValue) ? newValue(val) : newValue
scopeStore.set(path, actualNewValue)
return actualNewValue
})
},
[path, scopeStore]
)
return [value, scopeSetter]
}

View File

@@ -1,46 +0,0 @@
import {
type Dispatch,
type SetStateAction,
useCallback,
useEffect,
useState,
} from 'react'
import _ from 'lodash'
import { useIdeContext } from '../context/ide-context'
/**
* Binds a property in an Angular scope making it accessible in a React
* component. The interface is compatible with React.useState(), including
* the option of passing a function to the setter.
*
* The generic type is not an actual guarantee because the value for a path is
* returned as undefined when there is nothing in the scope store for that path.
*/
export default function useScopeValue<T = any>(
path: string // dot '.' path of a property in the Angular scope
): [T, Dispatch<SetStateAction<T>>] {
const { scopeStore } = useIdeContext()
const [value, setValue] = useState<T>(() => scopeStore.get(path))
useEffect(() => {
return scopeStore.watch<T>(path, (newValue: T) => {
// NOTE: this is deliberately wrapped in a function,
// to avoid calling setValue directly with a value that's a function
setValue(() => newValue)
})
}, [path, scopeStore])
const scopeSetter = useCallback(
(newValue: SetStateAction<T>) => {
setValue(val => {
const actualNewValue = _.isFunction(newValue) ? newValue(val) : newValue
scopeStore.set(path, actualNewValue)
return actualNewValue
})
},
[path, scopeStore]
)
return [value, scopeSetter]
}

View File

@@ -10,10 +10,7 @@ import useFetchMock from '../hooks/use-fetch-mock'
import { useMeta } from '../hooks/use-meta'
import SocketIOShim, { SocketIOMock } from '@/ide/connection/SocketIoShim'
import { IdeContext } from '@/shared/context/ide-context'
import {
IdeReactContext,
createReactScopeValueStore,
} from '@/features/ide-react/context/ide-react-context'
import { IdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter'
import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
@@ -57,27 +54,6 @@ const project: Project = {
],
}
const initialScope = {
user,
project,
settings: {
pdfViewer: 'js',
syntaxValidation: true,
},
editor: {
richText: false,
// FIXME: This is pretty useless because the editor relies on a much more fleshed-out document, so we rely on overriding it in individual stories
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
hasBufferedOps: () => false,
},
open_doc_name: 'testfile.tex',
},
permissionsLevel: 'owner',
}
const socket = new SocketIOShim.SocketShimNoop(
new SocketIOMock()
) as unknown as Socket
@@ -195,10 +171,6 @@ const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
}))
const [ideContextValue] = useState(() => {
const scopeStore = createReactScopeValueStore()
for (const [key, value] of Object.entries(initialScope)) {
scopeStore.set(key, value)
}
const scopeEventEmitter = new ReactScopeEventEmitter(new IdeEventEmitter())
const unstableStore = new ReactScopeValueStore()
@@ -212,7 +184,6 @@ const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
return {
socket,
scopeStore,
scopeEventEmitter,
unstableStore,
}

View File

@@ -27,8 +27,5 @@ export const mockScope = (
refreshResolvedCommentsDropdown: cy.stub(() => sleep(1000)),
onlineUserCursorHighlights: {},
permissionsLevel: 'owner',
$on: cy.stub().log(false),
$broadcast: cy.stub().log(false),
$emit: cy.stub().log(false),
}
}

View File

@@ -11,10 +11,7 @@ import React, {
type FC,
type PropsWithChildren,
} from 'react'
import {
createReactScopeValueStore,
IdeReactContext,
} from '@/features/ide-react/context/ide-react-context'
import { IdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter'
import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
@@ -300,11 +297,6 @@ const makeIdeReactProvider = (
}))
const [ideContextValue] = useState(() => {
const scopeStore = createReactScopeValueStore()
for (const [key, value] of Object.entries(scope)) {
// TODO: path for nested entries
scopeStore.set(key, value)
}
const scopeEventEmitter = new ReactScopeEventEmitter(
new IdeEventEmitter()
)
@@ -312,7 +304,6 @@ const makeIdeReactProvider = (
return {
socket,
scopeStore,
scopeEventEmitter,
unstableStore,
}