@@ -4,20 +4,40 @@ import React, {
44 useCallback ,
55 useContext ,
66 useEffect ,
7+ useMemo ,
78 useRef ,
89 useState ,
910} from 'react'
1011import { 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'
1218import { 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
1736interface URLStateContext < T > {
1837 state : T | null
1938 setState : ( updatedState : T ) => void
2039 initializeState : ( schema : AnyZodObject ) => void
40+ currentStateViewIdRef : React . MutableRefObject < string | null >
2141}
2242
2343const 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
3354export 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
92198export 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}
0 commit comments