Skip to content

Commit 6299b86

Browse files
authored
fix: allow explore state to be updated (#457)
1 parent 20c36d5 commit 6299b86

File tree

5 files changed

+113
-85
lines changed

5 files changed

+113
-85
lines changed

dev/devPage.tsx

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { createRoot } from 'react-dom/client'
66
import { I18nextProvider, useTranslation } from 'react-i18next'
77
import 'tachyons'
88
import i18n from '../src/i18n.js'
9-
import { ExplorePage, StartExploringPage, IpldExploreForm, IpldCarExploreForm, ExploreProvider, HeliaProvider } from '../src/index.js'
9+
import { ExplorePage, StartExploringPage, IpldExploreForm, IpldCarExploreForm, ExploreProvider, HeliaProvider, useExplore } from '../src/index.js'
1010

1111
globalThis.Buffer = globalThis.Buffer ?? Buffer
1212

@@ -64,27 +64,22 @@ const HeaderComponent: React.FC = () => {
6464
}
6565

6666
const PageRenderer = (): React.ReactElement => {
67-
const [route, setRoute] = useState(window.location.hash.slice(1) ?? '/')
67+
const { setExplorePath, exploreState: { path } } = useExplore()
6868

6969
useEffect(() => {
70-
const onHashChange = (): void => { setRoute(window.location.hash.slice(1) ?? '/') }
70+
const onHashChange = (): void => {
71+
const newRoute = window.location.hash ?? null
72+
setExplorePath(newRoute)
73+
}
7174
window.addEventListener('hashchange', onHashChange)
7275
return () => { window.removeEventListener('hashchange', onHashChange) }
73-
}, [])
76+
}, [setExplorePath])
7477

75-
const RenderPage: React.FC = () => {
76-
switch (true) {
77-
case route.startsWith('/explore'):
78-
return <ExplorePage />
79-
case route === '/':
80-
default:
81-
return <StartExploringPage />
82-
}
78+
if (path == null || path === '') {
79+
return <StartExploringPage />
8380
}
8481

85-
return (
86-
<RenderPage />
87-
)
82+
return <ExplorePage />
8883
}
8984

9085
const App = (): React.ReactElement => {

src/components/ExplorePage.stories.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ const defaultState: ExploreState = {
88
path: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
99
canonicalPath: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
1010
error: null,
11-
explorePathFromHash: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
1211
targetNode: {
1312
type: 'dag-pb',
1413
format: 'unixfs',

src/components/ExplorePage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ export const ExplorePage = ({
2626
const { t, ready: tReady } = useTranslation('explore')
2727

2828
const { exploreState, doExploreLink } = useExplore()
29-
const { explorePathFromHash } = exploreState
29+
const { path } = exploreState
3030

31-
if (explorePathFromHash == null) {
31+
if (path == null) {
3232
// No IPLD path to explore so show the intro page
3333
console.warn('[IPLD Explorer] ExplorePage loaded without a path to explore')
3434
return null

src/components/loader/loader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react'
22
import styles from './loader.module.css'
33

4-
export const Loader: React.FC<{ color: string }> = ({ color = 'light', ...props }) => {
4+
export const Loader: React.FC<{ color?: string }> = ({ color = 'light', ...props }) => {
55
const className = `dib ${styles.laBallTrianglePath} la-${color} la-sm`
66
return (
77
<div {...props}>

src/providers/explore.tsx

Lines changed: 100 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import type { NormalizedDagNode } from '../types.js'
1212

1313
interface ExploreContextProps {
1414
exploreState: ExploreState
15-
// explorePathFromHash: string | null
15+
explorePathPrefix: string
16+
isLoading: boolean
17+
setExplorePath(path: string | null): void
1618
doExploreLink(link: any): void
1719
doExploreUserProvidedPath(path: string): void
1820
doUploadUserProvidedCar(file: File, uploadImage: string): Promise<void>
@@ -37,7 +39,6 @@ export interface ExploreState {
3739
nodes: any[]
3840
pathBoundaries: any[]
3941
error: IpldExploreError | null
40-
explorePathFromHash: string | null
4142
}
4243

4344
export const ExploreContext = createContext<ExploreContextProps | undefined>(undefined)
@@ -58,51 +59,108 @@ const getCidFromCidOrFqdn = (cidOrFqdn: CID | string): CID => {
5859
return CID.parse(cidOrFqdn)
5960
}
6061

62+
const processPath = (path: string | null, pathPrefix: string): string | null => {
63+
let newPath = path
64+
if (newPath != null) {
65+
if (newPath.includes(pathPrefix)) {
66+
newPath = newPath.slice(pathPrefix.length)
67+
}
68+
if (newPath.startsWith('/')) {
69+
newPath = newPath.slice(1)
70+
}
71+
if (newPath === '') {
72+
newPath = null
73+
} else {
74+
newPath = decodeURIComponent(newPath)
75+
}
76+
}
77+
return newPath
78+
}
79+
6180
const defaultState: ExploreState = {
6281
path: null,
6382
targetNode: null,
6483
canonicalPath: '',
6584
localPath: '',
6685
nodes: [],
6786
pathBoundaries: [],
68-
error: null,
69-
explorePathFromHash: null
87+
error: null
88+
}
89+
90+
export interface ExploreProviderProps {
91+
children?: ReactNode | ReactNode[]
92+
state?: Partial<ExploreState>
93+
explorePathPrefix?: string
7094
}
7195

72-
export const ExploreProvider = ({ children, state = defaultState }: { children?: ReactNode, state?: ExploreState }): React.ReactNode => {
73-
const [exploreState, setExploreState] = useState<ExploreState>({ ...state, explorePathFromHash: window.location.hash.slice('#/explore'.length) })
96+
export const ExploreProvider = ({ children, state, explorePathPrefix = '#/explore' }: ExploreProviderProps): React.ReactNode => {
97+
if (state == null) {
98+
state = {
99+
path: processPath(window.location.hash, explorePathPrefix)
100+
}
101+
} else {
102+
if (state.path === '') {
103+
state.path = null
104+
} else if (state.path != null) {
105+
state.path = processPath(state.path, explorePathPrefix)
106+
}
107+
}
108+
const [exploreState, setExploreState] = useState<ExploreState>({ ...defaultState, ...state })
74109
const { helia } = useHelia()
75-
const { explorePathFromHash } = exploreState
110+
const [isLoading, setIsLoading] = useState<boolean>(false)
111+
const { path } = exploreState
76112

77-
const fetchExploreData = useCallback(async (path: string): Promise<void> => {
78-
// Clear the target node when a new path is requested
79-
setExploreState((exploreState) => ({
80-
...exploreState,
81-
targetNode: null
82-
}))
83-
const pathParts = parseIpldPath(path)
84-
if (pathParts == null || helia == null) return
113+
useEffect(() => {
114+
setIsLoading(true);
85115

86-
const { cidOrFqdn, rest } = pathParts
87-
try {
88-
const cid = getCidFromCidOrFqdn(cidOrFqdn)
89-
const { targetNode, canonicalPath, localPath, nodes, pathBoundaries } = await resolveIpldPath(helia, cid, rest)
90-
91-
setExploreState(({ explorePathFromHash }) => ({
92-
explorePathFromHash,
93-
path,
94-
targetNode,
95-
canonicalPath,
96-
localPath,
97-
nodes,
98-
pathBoundaries,
99-
error: null
116+
(async () => {
117+
if (path == null || helia == null) {
118+
return
119+
}
120+
// Clear the target node when a new path is requested
121+
setExploreState((exploreState) => ({
122+
...exploreState,
123+
targetNode: null
100124
}))
101-
} catch (error: any) {
102-
console.warn('Failed to resolve path', path, error)
103-
setExploreState((prevState) => ({ ...prevState, error }))
125+
const pathParts = parseIpldPath(path)
126+
if (pathParts == null || helia == null) return
127+
128+
const { cidOrFqdn, rest } = pathParts
129+
try {
130+
const cid = getCidFromCidOrFqdn(cidOrFqdn)
131+
const { targetNode, canonicalPath, localPath, nodes, pathBoundaries } = await resolveIpldPath(helia, cid, rest)
132+
133+
setExploreState((curr) => ({
134+
...curr,
135+
targetNode,
136+
canonicalPath,
137+
localPath,
138+
nodes,
139+
pathBoundaries,
140+
error: null
141+
}))
142+
} catch (error: any) {
143+
console.warn('Failed to resolve path', path, error)
144+
setExploreState((prevState) => ({ ...prevState, error }))
145+
}
146+
})().catch((err) => {
147+
console.error('Error fetching explore data', err)
148+
setExploreState((prevState) => ({ ...prevState, error: err }))
149+
}).finally(() => {
150+
setIsLoading(false)
151+
})
152+
}, [helia, path])
153+
154+
const setExplorePath = (path: string | null): void => {
155+
const newPath = processPath(path, explorePathPrefix)
156+
if (newPath != null && !window.location.href.includes(newPath)) {
157+
throw new Error('setExplorePath should only be used to update the state, not the URL. If you are using a routing library that doesn\'t allow you to listen to hashchange events, ensure the URL is updated prior to calling setExplorePath.')
104158
}
105-
}, [helia])
159+
setExploreState((exploreState) => ({
160+
...exploreState,
161+
path: newPath
162+
}))
163+
}
106164

107165
const doExploreLink = (link: LinkObject): void => {
108166
const { nodes, pathBoundaries } = exploreState
@@ -114,12 +172,16 @@ export const ExploreProvider = ({ children, state = defaultState }: { children?:
114172
}
115173
pathParts.unshift(cid)
116174
const path = pathParts.map((part) => encodeURIComponent(part)).join('/')
117-
const hash = `#/explore/${path}`
175+
const hash = `${explorePathPrefix}/${path}`
118176
window.location.hash = hash
177+
setExplorePath(path)
119178
}
120179

180+
/**
181+
* @deprecated - use setExplorePath instead
182+
*/
121183
const doExploreUserProvidedPath = (path: string): void => {
122-
const hash = path != null ? `#/explore${ensureLeadingSlash(path)}` : '#/explore'
184+
const hash = path != null ? `${explorePathPrefix}${ensureLeadingSlash(path)}` : explorePathPrefix
123185
window.location.hash = hash
124186
}
125187

@@ -130,7 +192,7 @@ export const ExploreProvider = ({ children, state = defaultState }: { children?:
130192
}
131193
try {
132194
const rootCid = await importCar(file, helia)
133-
const hash = rootCid.toString() != null ? `#/explore${ensureLeadingSlash(rootCid.toString())}` : '#/explore'
195+
const hash = rootCid.toString() != null ? `${explorePathPrefix}${ensureLeadingSlash(rootCid.toString())}` : explorePathPrefix
134196
window.location.hash = hash
135197

136198
const imageFileLoader = document.getElementById('car-loader-image') as HTMLImageElement
@@ -140,42 +202,14 @@ export const ExploreProvider = ({ children, state = defaultState }: { children?:
140202
} catch (err) {
141203
console.error('Could not import car file', err)
142204
}
143-
}, [helia])
144-
145-
useEffect(() => {
146-
const handleHashChange = (): void => {
147-
const explorePathFromHash = window.location.hash.slice('#/explore'.length)
148-
149-
setExploreState((state) => ({
150-
...state,
151-
explorePathFromHash
152-
}))
153-
}
154-
155-
window.addEventListener('hashchange', handleHashChange)
156-
handleHashChange()
157-
158-
return () => {
159-
window.removeEventListener('hashchange', handleHashChange)
160-
}
161-
}, [])
162-
163-
useEffect(() => {
164-
// if explorePathFromHash or helia change and are not null, fetch the data
165-
// We need to check for helia because the helia provider is async and may not be ready yet
166-
if (explorePathFromHash != null && helia != null) {
167-
void (async () => {
168-
await fetchExploreData(decodeURIComponent(explorePathFromHash))
169-
})()
170-
}
171-
}, [helia, explorePathFromHash])
205+
}, [explorePathPrefix, helia])
172206

173207
if (helia == null) {
174208
return <Loader color='dark' />
175209
}
176210

177211
return (
178-
<ExploreContext.Provider value={{ exploreState, doExploreLink, doExploreUserProvidedPath, doUploadUserProvidedCar }}>
212+
<ExploreContext.Provider value={{ exploreState, explorePathPrefix, isLoading, doExploreLink, doExploreUserProvidedPath, doUploadUserProvidedCar, setExplorePath }} key={path}>
179213
{children}
180214
</ExploreContext.Provider>
181215
)

0 commit comments

Comments
 (0)