Skip to content

Commit

Permalink
chore: creates persisted store for Active Project Id using Zustand (#67)
Browse files Browse the repository at this point in the history
* chore: add zustand

* chore: createPersistedStateFunction

* chore: persisted active project Id

* chore: utility function to createStoreHooks

* chore: create active project Id provider

* chore: update app with new provider

* chore: update use of hook

* chore: remove unneccesary set state

* chore: create example use of zustand store with active project id

* chore: remove duplicate providers

* chore: remove console log

* chore: change function casing

* chore: update casing
  • Loading branch information
ErikSin authored Dec 19, 2024
1 parent c76a334 commit 2a8d85b
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 37 deletions.
33 changes: 32 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@
"typescript-eslint": "8.18.1",
"vite": "^5.4.11",
"vite-plugin-svgr": "^4.3.0",
"vitest": "2.1.8"
"vitest": "2.1.8",
"zustand": "5.0.2"
},
"overrides": {
"better-sqlite3": "11.5.0"
Expand Down
41 changes: 41 additions & 0 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { CssBaseline, ThemeProvider } from '@mui/material'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { RouterProvider, createRouter } from '@tanstack/react-router'

import { theme } from './Theme'
import {
ActiveProjectIdProvider,
createActiveProjectIdStore,
} from './contexts/ActiveProjectIdProvider'
import { ApiProvider } from './contexts/ApiContext'
import { IntlProvider } from './contexts/IntlContext'
import { routeTree } from './routeTree.gen'

const queryClient = new QueryClient()

const router = createRouter({ routeTree })

declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}

const PersistedProjectIdStore = createActiveProjectIdStore({
persist: true,
})

export const App = () => (
<ThemeProvider theme={theme}>
<CssBaseline />
<IntlProvider>
<QueryClientProvider client={queryClient}>
<ActiveProjectIdProvider store={PersistedProjectIdStore}>
<ApiProvider>
<RouterProvider router={router} />
</ApiProvider>
</ActiveProjectIdProvider>
</QueryClientProvider>
</IntlProvider>
</ThemeProvider>
)
61 changes: 61 additions & 0 deletions src/renderer/src/contexts/ActiveProjectIdProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { createContext, type ReactNode } from 'react'
import { createStore, type StoreApi } from 'zustand'
import { persist as zustandPersist } from 'zustand/middleware'

import { createHooks } from './createStoreHooks'

const PERSISTED_ACTIVE_PROJECT_ID_KEY = 'ActiveProjectId'

type ActiveProjectId = { activeProjectId: string | undefined }

const initialActiveProjectId: ActiveProjectId = {
activeProjectId: undefined,
}

type ActiveProjectIdStore = ReturnType<typeof createActiveProjectIdStore>

type ActiveProjectIdProviderProps = {
children: ReactNode
store: ActiveProjectIdStore
}

const ActiveProjectIdContext = createContext<ActiveProjectIdStore | null>(null)

export const ActiveProjectIdProvider = ({
children,
store,
}: ActiveProjectIdProviderProps) => {
return (
<ActiveProjectIdContext.Provider value={store}>
{children}
</ActiveProjectIdContext.Provider>
)
}

const { useStoreActions, useStoreState } = createHooks(ActiveProjectIdContext)

export {
useStoreActions as useActiveProjectIdStoreActions,
useStoreState as useActiveProjectIdStoreState,
}

export function createActiveProjectIdStore({ persist }: { persist: boolean }) {
let store: StoreApi<ActiveProjectId>

if (!persist) {
store = createStore(() => initialActiveProjectId)
} else {
store = createStore(
zustandPersist(() => initialActiveProjectId, {
name: PERSISTED_ACTIVE_PROJECT_ID_KEY,
}),
)
}

const actions = {
setActiveProjectId: (newProjectId: string) =>
store.setState({ activeProjectId: newProjectId }),
}

return { store, actions }
}
31 changes: 31 additions & 0 deletions src/renderer/src/contexts/createStoreHooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useContext } from 'react'
import { useStore, type StoreApi } from 'zustand'

export function createHooks<TStoreState, TStoreActions>(
context: React.Context<{
store: StoreApi<TStoreState>
actions: TStoreActions
} | null>,
) {
function useContextValue() {
const value = useContext(context)
if (!value) {
throw new Error('Must set up the provider first')
}
return value
}

function useStoreState(): TStoreState
function useStoreState<S>(selector: (state: TStoreState) => S): S
function useStoreState<S>(selector?: (state: TStoreState) => S) {
const { store } = useContextValue()
return useStore(store, selector!)
}

function useStoreActions() {
const { actions } = useContextValue()
return actions
}

return { useStoreState, useStoreActions }
}
13 changes: 2 additions & 11 deletions src/renderer/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import { RouterProvider, createRouter } from '@tanstack/react-router'
import { createRoot } from 'react-dom/client'

import { routeTree } from './routeTree.gen'

import './index.css'

const router = createRouter({ routeTree })

declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
import { App } from './App'

const root = createRoot(document.getElementById('app') as HTMLElement)

root.render(<RouterProvider router={router} />)
root.render(<App />)
5 changes: 4 additions & 1 deletion src/renderer/src/routes/Onboarding/CreateProjectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from '../../components/Onboarding/onboardingLogic'
import { Text } from '../../components/Text'
import { PROJECT_NAME_MAX_LENGTH_GRAPHEMES } from '../../constants'
import { useActiveProjectIdStoreActions } from '../../contexts/ActiveProjectIdProvider'
import { useCreateProject } from '../../hooks/mutations/projects'
import ProjectImage from '../../images/add_square.png'

Expand Down Expand Up @@ -110,6 +111,7 @@ function CreateProjectScreenComponent() {
const [error, setError] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const setProjectNameMutation = useCreateProject()
const { setActiveProjectId } = useActiveProjectIdStoreActions()

const [configFileName, setConfigFileName] = useState<string | null>(null)

Expand All @@ -134,7 +136,8 @@ function CreateProjectScreenComponent() {
return
}
setProjectNameMutation.mutate(projectName, {
onSuccess: () => {
onSuccess: (projectId) => {
setActiveProjectId(projectId)
navigate({ to: '/tab1' })
},
onError: (error) => {
Expand Down
23 changes: 3 additions & 20 deletions src/renderer/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,11 @@
import { Suspense } from 'react'
import { CssBaseline, ThemeProvider } from '@mui/material'
import CircularProgress from '@mui/material/CircularProgress'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Outlet, createRootRoute } from '@tanstack/react-router'

import { theme } from '../Theme'
import { ApiProvider } from '../contexts/ApiContext'
import { IntlProvider } from '../contexts/IntlContext'

const queryClient = new QueryClient()

export const Route = createRootRoute({
component: () => (
<ThemeProvider theme={theme}>
<CssBaseline />
<IntlProvider>
<QueryClientProvider client={queryClient}>
<ApiProvider>
<Suspense fallback={<CircularProgress />}>
<Outlet />
</Suspense>
</ApiProvider>
</QueryClientProvider>
</IntlProvider>
</ThemeProvider>
<Suspense fallback={<CircularProgress />}>
<Outlet />
</Suspense>
),
})
17 changes: 14 additions & 3 deletions src/renderer/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { useLayoutEffect } from 'react'
import { useOwnDeviceInfo } from '@comapeo/core-react'
import { createFileRoute, useNavigate } from '@tanstack/react-router'

import { useActiveProjectIdStoreState } from '../contexts/ActiveProjectIdProvider'

export const Route = createFileRoute('/')({
component: RouteComponent,
})
Expand All @@ -12,14 +14,23 @@ export const Route = createFileRoute('/')({
function RouteComponent() {
const navigate = useNavigate()
const { data } = useOwnDeviceInfo()
const hasCreatedDeviceName = data?.name !== undefined
const hasCreatedDeviceName = data.name !== undefined
const activeProjectId = useActiveProjectIdStoreState(
(store) => store.activeProjectId,
)

useLayoutEffect(() => {
if (!hasCreatedDeviceName) {
navigate({ to: '/Onboarding' })
} else {
navigate({ to: '/tab1' })
return
}

if (!activeProjectId) {
navigate({ to: '/Onboarding/CreateJoinProjectScreen' })
return
}

navigate({ to: '/tab1' })
})

return null
Expand Down

0 comments on commit 2a8d85b

Please sign in to comment.