Skip to content

Commit

Permalink
feat(url-state): try implement use-url-state hook
Browse files Browse the repository at this point in the history
  • Loading branch information
baurine committed Jul 8, 2024
1 parent 4bc642d commit a639781
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 59 deletions.
13 changes: 6 additions & 7 deletions packages/playground/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { ThemeProvider } from '@/components/darkmode-toggle/theme-provider'
import { StatementProvider } from '@/contexts/statement-context-provider'
import { FilesProvider } from '@/contexts/files-context-provider'
import { SchemaProvider } from '@/contexts/schema-context-provider'
import { ChatProvider } from '@/contexts/chat-context-provider'

import { EditorExampleWithSelect } from '@/examples/editor-example-with-select'
import { EditorExample } from '@/examples/editor-example'
import { ChatProvider } from './contexts/chat-context-provider'
import { EditorExampleWithSelect } from './examples/editor-example-with-select'
import { UrlStateProvider } from '@/hooks/use-url-state'

const queryClient = new QueryClient({
defaultOptions: {
Expand Down Expand Up @@ -44,17 +45,15 @@ function App() {
const params = new URLSearchParams(window.location.search)
const example = params.get('example')
const withSelect = params.get('with_select')

const editorTheme = params.get('theme') ?? 'oneDark'

if (example !== null) {
if (withSelect !== null) {
return (
<ThemeProvider>
<EditorExampleWithSelect
defExample={example}
defTheme={editorTheme}
/>
<UrlStateProvider>
<EditorExampleWithSelect />
</UrlStateProvider>
</ThemeProvider>
)
}
Expand Down
8 changes: 4 additions & 4 deletions packages/playground/src/contexts/files-context-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo } from 'react'
import React, { useMemo, useState } from 'react'
import {
addFile,
delFile,
Expand All @@ -18,9 +18,9 @@ function FilesLoader() {
}

export function FilesProvider(props: { children: React.ReactNode }) {
const [allFiles, setAllFiles] = React.useState<IFile[]>([])
const [openedFiles, setOpenedFiles] = React.useState<IFile[]>([])
const [activeFileId, setActiveFileId] = React.useState<string | null>(null)
const [allFiles, setAllFiles] = useState<IFile[]>([])
const [openedFiles, setOpenedFiles] = useState<IFile[]>([])
const [activeFileId, setActiveFileId] = useState<string | null>(null)

const ctxValue = useMemo(
() => ({
Expand Down
4 changes: 2 additions & 2 deletions packages/playground/src/contexts/schema-context-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo } from 'react'
import React, { useMemo, useState } from 'react'

import { getSchema } from '@/api/tidbcloud/schema-api'
import { SchemaContext, SchemaRes } from './schema-context'
Expand All @@ -10,7 +10,7 @@ function SchemaLoader() {
}

export function SchemaProvider(props: { children: React.ReactNode }) {
const [schema, setSchema] = React.useState<SchemaRes | null>(null)
const [schema, setSchema] = useState<SchemaRes | null>(null)

const ctxValue = useMemo(
() => ({ loadSchema: getSchema, schema, setSchema }),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { useMemo } from 'react'
import React, { useMemo, useState } from 'react'

import { SqlStatement } from '@tidbcloud/codemirror-extension-sql-parser'

import { runSQL } from '@/api/tidbcloud/statement-api'
import { StatementContext } from './statement-context'

export function StatementProvider(props: { children: React.ReactNode }) {
const [runResult, setRunResult] = React.useState<any>({})
const [runResult, setRunResult] = useState<any>({})

const runStatement = (_fileId: string, statement: SqlStatement) => {
return runSQL({ database: statement.database, sql: statement.content })
Expand Down
58 changes: 14 additions & 44 deletions packages/playground/src/examples/editor-example-with-select.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect } from 'react'

import { EnterFullScreenIcon, GitHubLogoIcon } from '@radix-ui/react-icons'

Expand All @@ -15,16 +15,13 @@ import { Button } from '@/components/ui/button'
import { useTheme } from '@/components/darkmode-toggle/theme-provider'

import { EditorExample } from './editor-example'
import { useExampleUrlState } from './url-state'

function ExampleSelect() {
const { example, setExample } = useExampleUrlState()

function ExampleSelect({
value,
onChange
}: {
value: string
onChange: (v: string) => void
}) {
return (
<Select value={value} onValueChange={onChange}>
<Select value={example} onValueChange={setExample}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select an extension" />
</SelectTrigger>
Expand All @@ -46,15 +43,11 @@ function ExampleSelect({
)
}

function ThemeSelect({
value,
onChange
}: {
value: string
onChange: (v: string) => void
}) {
function ThemeSelect() {
const { theme, setTheme } = useExampleUrlState()

return (
<Select value={value} onValueChange={onChange}>
<Select value={theme} onValueChange={setTheme}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="Select a theme" />
</SelectTrigger>
Expand All @@ -70,34 +63,11 @@ function ThemeSelect({
)
}

function updateUrlParam(key: string, value: string) {
const url = new URL(window.location.href)
const params = new URLSearchParams(url.search)
params.set(key, value)
window.history.replaceState({}, '', `${url.pathname}?${params.toString()}`)
}
export function EditorExampleWithSelect() {
const { example, theme: editorTheme } = useExampleUrlState()

export function EditorExampleWithSelect({
defExample,
defTheme
}: {
defExample: string
defTheme: string
}) {
const [example, setExample] = useState(defExample)
const [editorTheme, setEditorTheme] = useState(defTheme)
const { setTheme: setAppTheme } = useTheme()

function onExampleChange(v: string) {
setExample(v)
updateUrlParam('example', v)
}

function onThemeChange(v: string) {
setEditorTheme(v)
updateUrlParam('theme', v)
}

useEffect(() => {
setAppTheme(
editorTheme === 'oneDark' || editorTheme === 'dark' ? 'dark' : 'light'
Expand All @@ -113,8 +83,8 @@ export function EditorExampleWithSelect({
</h1>

<div className="mt-10 flex items-center gap-1">
<ExampleSelect value={example} onChange={onExampleChange} />
<ThemeSelect value={editorTheme} onChange={onThemeChange} />
<ExampleSelect />
<ThemeSelect />
<Button variant="ghost" size="icon">
<a
href={`/?example=${example}&theme=${editorTheme}`}
Expand Down
27 changes: 27 additions & 0 deletions packages/playground/src/examples/url-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useUrlState } from '@/hooks/use-url-state'
import { useCallback } from 'react'

type ExampleUrlState = Partial<
Record<'example' | 'with_select' | 'theme', string>
>

export function useExampleUrlState() {
const [queryParams, setQueryParams] = useUrlState<ExampleUrlState>()

// example
const example = queryParams.example
const setExample = useCallback((v: string) => {
setQueryParams({ example: v })
}, [])

// theme
const theme = queryParams.theme || 'oneDark'
const setTheme = useCallback((v: string) => {
setQueryParams({ theme: v })
}, [])

// with_select
const withSelect = !!queryParams.with_select

return { example, setExample, theme, setTheme, withSelect }
}
94 changes: 94 additions & 0 deletions packages/playground/src/hooks/use-url-state.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { createContext, useContext, useMemo, useState } from 'react'

type UrlStateCtxValue = {
urlSearch: string
setUrlSearch: (v: string) => void
}

const UrlStateContext = createContext<UrlStateCtxValue | null>(null)

const useUrlStateContext = () => {
const context = useContext(UrlStateContext)

if (!context) {
throw new Error('useUrlStateContext must be used within a provider')
}

return context
}

type UrlStateProviderOptions = {
getUrlParams: () => string
updateUrlParams: (p: string) => void
}

const defaultOptions: UrlStateProviderOptions = {
getUrlParams() {
const url = new URL(window.location.href)
return url.search
},
updateUrlParams(p) {
const url = new URL(window.location.href)
window.history.replaceState({}, '', `${url.pathname}?${p}`)
}
}

export function UrlStateProvider(props: {
children: React.ReactNode
options?: UrlStateProviderOptions
}) {
const opt = props.options || defaultOptions
const [urlSearch, _setUrlSearch] = useState(opt.getUrlParams())

const ctxValue = useMemo(
() => ({
urlSearch,
setUrlSearch: (v: string) => {
opt.updateUrlParams(v)
_setUrlSearch(v)
}
}),
[urlSearch]
)

return (
<UrlStateContext.Provider value={ctxValue}>
{props.children}
</UrlStateContext.Provider>
)
}

//----------------------

type UrlState = Record<string, string | undefined>
type UrlStateObj<T extends UrlState = UrlState> = Partial<{
[key in keyof T]: string
}>

export function useUrlState<T extends UrlState = UrlState>(): [
UrlStateObj<T>,
(s: UrlStateObj<T>) => void
] {
const { urlSearch, setUrlSearch } = useUrlStateContext()

const searchParams = new URLSearchParams(urlSearch)
const paramsObj: any = {}
searchParams.forEach((v, k) => {
paramsObj[k] = v
})
const queryParams = paramsObj as UrlStateObj<T>

function setQueryParams(s: UrlStateObj<T>) {
const searchParams = new URLSearchParams(urlSearch)
Object.keys(s).forEach((k) => {
if (s[k]) {
searchParams.set(k, s[k])
} else {
searchParams.delete(k)
}
})
setUrlSearch(searchParams.toString())
}

return [queryParams, setQueryParams] as const
}

0 comments on commit a639781

Please sign in to comment.