Skip to content

Commit 1077bde

Browse files
committed
Trying to fix view alignment issues
1 parent cd42aa7 commit 1077bde

File tree

2 files changed

+142
-14
lines changed

2 files changed

+142
-14
lines changed

Eplant/state/URLStateProvider.tsx

Lines changed: 137 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,40 @@ import React, {
44
useCallback,
55
useContext,
66
useEffect,
7+
useMemo,
78
useRef,
89
useState,
910
} from 'react'
1011
import { debounce } from 'lodash'
11-
import { useSearchParams } from 'react-router-dom'
12+
import {
13+
useLocation,
14+
useNavigate,
15+
useParams,
16+
useSearchParams,
17+
} from 'react-router-dom'
1218
import { AnyZodObject } from 'zod'
1319

14-
import { flattenObject, getStateFromParams, getZodDefaults } from './stateUtils'
15-
import { useActiveViewId } from '.'
20+
import { useConfig } from '@eplant/config'
21+
import GeneInfoViewMetadata from '@eplant/views/GeneInfoView'
22+
23+
import {
24+
flattenObject,
25+
getStateFromParams,
26+
getViewIdFromPathname,
27+
getZodDefaults,
28+
} from './stateUtils'
29+
import {
30+
useActiveGeneId,
31+
useActiveViewId,
32+
useGeneticElements,
33+
useSpecies,
34+
} from '.'
1635

1736
interface URLStateContext<T> {
1837
state: T | null
1938
setState: (updatedState: T) => void
2039
initializeState: (schema: AnyZodObject) => void
40+
currentStateViewIdRef: React.MutableRefObject<string | null>
2141
}
2242

2343
const URLStateContext = createContext<URLStateContext<any>>({
@@ -28,26 +48,105 @@ const URLStateContext = createContext<URLStateContext<any>>({
2848
initializeState: () => {
2949
throw new Error('initializeState must be used within a URLStateProvider')
3050
},
51+
currentStateViewIdRef: { current: null },
3152
})
3253

3354
export const URLStateProvider = ({ children }: { children: ReactNode }) => {
34-
const [activeViewId] = useActiveViewId()
35-
const [searchParams, setSearchParams] = useSearchParams()
36-
const stateCache = useRef(new Map<string, any>())
3755
const [state, setState] = useState<any>(() => {
3856
null
3957
})
58+
const [activeViewId, setActiveViewId] = useActiveViewId()
59+
const [searchParams, setSearchParams] = useSearchParams()
60+
const stateCache = useRef(new Map<string, any>())
61+
const currentStateViewIdRef = useRef<string | null>(null)
62+
const { views } = useConfig()
63+
const location = useLocation()
64+
const navigate = useNavigate()
65+
const [speciesList] = useSpecies()
66+
const [genes, setGenes] = useGeneticElements()
67+
const [activeGeneId, setActiveGeneId] = useActiveGeneId()
68+
const [geneNotFound, setGeneNotFound] = useState(false)
69+
const params = useParams()
70+
71+
useEffect(() => {
72+
const loadGene = async (geneid: string) => {
73+
// TODO: This is super jank, should probably write some better utilities for loading genes
74+
const species = speciesList.find(
75+
(species) => species.name === 'Arabidopsis'
76+
)
77+
const newGene = await species?.api.searchGene(geneid)
78+
if (newGene) {
79+
setGenes([...genes, newGene])
80+
} else {
81+
setGeneNotFound(true)
82+
setActiveGeneId('')
83+
}
84+
}
85+
if (params.geneid) {
86+
if (params.geneid !== activeGeneId) {
87+
if (!genes.find((g) => g.id === params.geneid)) {
88+
loadGene(params.geneid)
89+
}
90+
if (!geneNotFound) setActiveGeneId(params.geneid)
91+
}
92+
} else {
93+
// Set active gene to first available if one is already loaded
94+
if (genes.length > 0) {
95+
setActiveGeneId(genes[0].id)
96+
} else {
97+
setActiveGeneId('')
98+
}
99+
}
100+
101+
// Set activeview
102+
const urlView =
103+
views.find((view) => view.id === location.pathname.split('/')[1]) ??
104+
GeneInfoViewMetadata
105+
106+
setActiveViewId(urlView.id)
107+
}, [])
108+
109+
// On when the activegene or view changes, update path
110+
useEffect(() => {
111+
const oldPathSegments = location.pathname
112+
.split('/')
113+
.filter((segment) => segment !== '')
114+
115+
const newPathSegments = []
116+
if (activeViewId) {
117+
newPathSegments.push(activeViewId)
118+
}
119+
if (activeGeneId) {
120+
newPathSegments.push(activeGeneId)
121+
}
122+
123+
if (newPathSegments.length > 0) {
124+
let newPath
125+
if (
126+
oldPathSegments.length > 0 &&
127+
oldPathSegments[0] == newPathSegments[0]
128+
) {
129+
// If the view is the same we want to retain query params in url, else we can wipe
130+
// them and have URLStateManager handle things
131+
newPath = '/' + newPathSegments.join('/') + location.search
132+
} else {
133+
newPath = '/' + newPathSegments.join('/')
134+
}
135+
navigate(newPath)
136+
}
137+
}, [activeGeneId, activeViewId])
40138

41139
const debouncedUpdateSearchParams = useCallback(
42140
debounce((updatedState: any) => {
43-
// Setting component state
44-
stateCache.current.set(activeViewId, updatedState)
141+
// Get current view ID from location to ensure correct caching
142+
const currentViewId = getViewIdFromPathname(location.pathname)
143+
stateCache.current.set(currentViewId, updatedState)
45144

46145
// Setting params
47146
const flattenedState = flattenObject(updatedState)
48147
setSearchParams(new URLSearchParams(flattenedState as any))
49148
}, 50),
50-
[activeViewId]
149+
[location.pathname, setSearchParams]
51150
)
52151

53152
useEffect(() => {
@@ -61,19 +160,25 @@ export const URLStateProvider = ({ children }: { children: ReactNode }) => {
61160

62161
const initializeState = useCallback(
63162
<T extends AnyZodObject>(schema: T) => {
64-
// Get validated state
65-
const defaultState = getZodDefaults(schema) // This returns the defaults defined by Zod Schema
163+
const currentViewId = getViewIdFromPathname(location.pathname)
164+
165+
const defaultState = getZodDefaults(schema)
66166
const paramsState = getStateFromParams(schema, searchParams)
67-
const cachedState = stateCache.current.get(activeViewId) || {}
167+
const cachedState = stateCache.current.get(currentViewId) || {}
168+
68169
// Merge precedence: search params > cached state > defaults
69170
const mergedState = {
70171
...defaultState,
71172
...cachedState,
72173
...paramsState,
73174
}
175+
176+
// Track which view ID this state belongs to
177+
currentStateViewIdRef.current = currentViewId
178+
setActiveViewId(currentViewId)
74179
setState(mergedState)
75180
},
76-
[searchParams, activeViewId]
181+
[searchParams, location.pathname]
77182
)
78183

79184
return (
@@ -82,6 +187,7 @@ export const URLStateProvider = ({ children }: { children: ReactNode }) => {
82187
state,
83188
setState: (updatedState) => setState(updatedState),
84189
initializeState,
190+
currentStateViewIdRef,
85191
}}
86192
>
87193
{children}
@@ -91,8 +197,25 @@ export const URLStateProvider = ({ children }: { children: ReactNode }) => {
91197

92198
export const useURLState = <T,>() => {
93199
const context = useContext(URLStateContext)
200+
const location = useLocation()
201+
94202
if (!context.setState) {
95203
throw new Error('useURLState must be used within a URLStateProvider')
96204
}
97-
return context as URLStateContext<T>
205+
206+
const currentUrlViewId = useMemo(
207+
() => getViewIdFromPathname(location.pathname),
208+
[location.pathname]
209+
)
210+
211+
// Clear state to avoid
212+
const safeState =
213+
currentUrlViewId === context.currentStateViewIdRef.current
214+
? context.state
215+
: null
216+
217+
return {
218+
...context,
219+
state: safeState,
220+
} as URLStateContext<T>
98221
}

Eplant/state/stateUtils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,8 @@ export const unflattenObject = (
111111
}
112112
return result
113113
}
114+
115+
export const getViewIdFromPathname = (pathname: string): string => {
116+
const pathParts = pathname.split('/').filter(Boolean)
117+
return pathParts[0] || 'gene-info'
118+
}

0 commit comments

Comments
 (0)