-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore: add zustand and persisted state management #64
Closed
Closed
Changes from 10 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
1566ea2
chore: adds library
ErikSin 5a3bd44
chore: create persisted state creator
ErikSin 0e5fe3a
chore: create persisted id provider
ErikSin 5f65843
chore: seperates context from router
ErikSin 5d3cfc0
chore: update index.tsx with app wrapper
ErikSin 1dd0cb3
chore: update root and index route
ErikSin 1a6d6da
chore: simplify route structures
ErikSin 8184249
chore: update route typing from simplified structure
ErikSin 9e26980
remove unnecessary component
ErikSin 527917b
chore: update test
ErikSin 90e1225
chore: created reuseable createPersistedState
ErikSin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { RouterProvider, createRouter } from '@tanstack/react-router' | ||
|
||
import { usePersistedProjectIdStore } from './contexts/persistedState/PersistedProjectId' | ||
import { useDeviceInfo } from './queries/deviceInfo' | ||
import { routeTree } from './routeTree.gen' | ||
|
||
export const router = createRouter({ | ||
routeTree, | ||
context: { hasDeviceName: undefined!, persistedProjectId: undefined! }, | ||
}) | ||
|
||
declare module '@tanstack/react-router' { | ||
interface Register { | ||
router: typeof router | ||
} | ||
} | ||
|
||
export const App = () => { | ||
const { data } = useDeviceInfo() | ||
const hasDeviceName = data?.name !== undefined | ||
const persistedProjectId = !!usePersistedProjectIdStore( | ||
(store) => store.projectId, | ||
) | ||
return ( | ||
<RouterProvider | ||
router={router} | ||
context={{ hasDeviceName, persistedProjectId }} | ||
/> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { ThemeProvider } from '@emotion/react' | ||
import { CssBaseline } from '@mui/material' | ||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' | ||
|
||
import { App } from './App' | ||
import { theme } from './Theme' | ||
import { ApiProvider } from './contexts/ApiContext' | ||
import { IntlProvider } from './contexts/IntlContext' | ||
import { PersistedProjectIdProvider } from './contexts/persistedState/PersistedProjectId' | ||
|
||
const queryClient = new QueryClient() | ||
|
||
export const AppWrapper = () => { | ||
return ( | ||
<ThemeProvider theme={theme}> | ||
<CssBaseline /> | ||
<IntlProvider> | ||
<QueryClientProvider client={queryClient}> | ||
<ApiProvider> | ||
<PersistedProjectIdProvider> | ||
<App /> | ||
</PersistedProjectIdProvider> | ||
</ApiProvider> | ||
</QueryClientProvider> | ||
</IntlProvider> | ||
</ThemeProvider> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
src/renderer/src/contexts/persistedState/PersistedProjectId.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { type StateCreator } from 'zustand' | ||
|
||
import { createPersistedStoreWithProvider } from './createPersistedState' | ||
|
||
type ProjectIdSlice = { | ||
projectId: string | undefined | ||
setProjectId: (id?: string) => void | ||
} | ||
|
||
const projectIdSlice: StateCreator<ProjectIdSlice> = (set) => ({ | ||
projectId: undefined, | ||
setProjectId: (projectId) => set({ projectId }), | ||
}) | ||
|
||
export const { | ||
Provider: PersistedProjectIdProvider, | ||
useStoreHook: usePersistedProjectIdStore, | ||
Context: PersistedProjectIdContext, | ||
nonPersistedStore: nonPersistedProjectIdStore, | ||
} = createPersistedStoreWithProvider(projectIdSlice, 'ActiveProjectId') |
62 changes: 62 additions & 0 deletions
62
src/renderer/src/contexts/persistedState/createPersistedState.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { createContext, useContext, useState, type ReactNode } from 'react' | ||
import { createStore, useStore, type StateCreator } from 'zustand' | ||
import { persist } from 'zustand/middleware' | ||
|
||
type PersistedStoreKey = 'ActiveProjectId' | ||
|
||
/** | ||
* Follows the pattern of injecting persisted state with a context. See | ||
* https://tkdodo.eu/blog/zustand-and-react-context. Allows for easier testing | ||
*/ | ||
export function createPersistedStoreWithProvider<T>( | ||
slice: StateCreator<T>, | ||
persistedStoreKey: PersistedStoreKey, | ||
) { | ||
const persistedStore = createPersistedStore(slice, persistedStoreKey) | ||
// used for testing and injecting values into testing environment | ||
const nonPersistedStore = createStore(slice) | ||
// type persistedStore is a subset type of type nonPersistedStore | ||
const Context = createContext<typeof nonPersistedStore | null>(null) | ||
|
||
const Provider = ({ children }: { children: ReactNode }) => { | ||
const [storeInstance] = useState(() => persistedStore) | ||
|
||
return <Context.Provider value={storeInstance}>{children}</Context.Provider> | ||
} | ||
|
||
const useStoreHook = <Selected,>( | ||
selector: (state: T) => Selected, | ||
): Selected => { | ||
const contextStore = useContext(Context) | ||
if (!contextStore) { | ||
throw new Error( | ||
`Missing provider for persisted store: ${persistedStoreKey}`, | ||
) | ||
} | ||
|
||
return useStore(contextStore, selector) | ||
} | ||
|
||
return { Provider, useStoreHook, Context, nonPersistedStore } | ||
} | ||
|
||
function createPersistedStore<T>( | ||
...args: Parameters<typeof createPersistMiddleware<T>> | ||
) { | ||
const store = createStore<T>()(createPersistMiddleware(...args)) | ||
store.setState((state) => ({ | ||
...state, | ||
...args[0], | ||
})) | ||
|
||
return store | ||
} | ||
|
||
function createPersistMiddleware<State>( | ||
slice: StateCreator<State>, | ||
persistedStoreKey: PersistedStoreKey, | ||
) { | ||
return persist(slice, { | ||
name: persistedStoreKey, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { AppWrapper } from './AppWrapper' | ||
|
||
import './index.css' | ||
|
||
const router = createRouter({ routeTree }) | ||
|
||
declare module '@tanstack/react-router' { | ||
interface Register { | ||
router: typeof router | ||
} | ||
} | ||
|
||
const root = createRoot(document.getElementById('app') as HTMLElement) | ||
|
||
root.render(<RouterProvider router={router} />) | ||
root.render(<AppWrapper />) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm exposing the
Context
and theProvider
here. The provider should be used by the app, the Context on the other hand can be used for testing to simulate the provider. This allows set up the same provider, without having to touch the persisted state, and injecting test values.You can see an example of that here. The Value of persisted state determines where the user is redirected to. So I can easily simulate that state with a non persisted value by using the context and nonPersistedStore
There could be an argument that we should use persisted state in the tests, to better mimic the app. The problem is that we would be overwriting the actual persisted state, which might not be ideal for developer experience. We could use a different "key". Therefore testing environments would have its own persisted state, separate from the dev environment. This could get a little buggy as there could be things in persisted state that the test environment is using that we don't realize. We could make sure to clear persisted state at the end of every test file, but there would be some overhead with that.
@gmaclennan, @achou11, and @cimigree I'm curious if you have any insight or opinions of the best approach for testing persisted state.