Skip to content

Commit

Permalink
feat: init zustand
Browse files Browse the repository at this point in the history
  • Loading branch information
simon-wicki committed Sep 16, 2021
1 parent a575bd4 commit 504f7a3
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 25 deletions.
35 changes: 15 additions & 20 deletions ui/packages/app/web/src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AppProps } from 'next/app'
import { Container } from 'react-bootstrap'
import 'react-dates/lib/css/_datepicker.css'
import { StoreProvider, useCreateStore } from 'store'
import 'tailwindcss/tailwind.css'
import '../style/file-input.css'
import '../style/globals.scss'
Expand All @@ -10,27 +11,21 @@ import '../style/sidenav.css'
import './App.scss'
import Header from './layouts/Header'

const MyApp = ({ Component, pageProps }: AppProps) => {
const App = ({ Component, pageProps }) => {
const { persistedState } = pageProps
// this is only point where persisted state can come in. it's either from:
// - cookies headers (server)
// - window.__NEXT_DATA__ (client)
const createStore = useCreateStore(persistedState?.state)

return (
<>
<Header />
<Container fluid>
<Component {...pageProps} />
</Container>
</>
<StoreProvider createStore={createStore}>
<Header />
<Container fluid>
<Component {...pageProps} />
</Container>
</StoreProvider>
)
}

// Only uncomment this method if you have blocking data requirements for
// every single page in your application. This disables the ability to
// perform automatic static optimization, causing every page in your app to
// be server-side rendered.
//
// MyApp.getInitialProps = async (appContext: AppContext) => {
// // calls page's `getInitialProps` and fills `appProps.pageProps`
// const appProps = await App.getInitialProps(appContext);

// return { ...appProps }
// }

export default MyApp
export default App
15 changes: 10 additions & 5 deletions ui/packages/app/web/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ProfileExplorer from 'components/ProfileExplorer'
import { NextRouter, withRouter } from 'next/router'
import { QueryServiceClient } from '@parca/client'
import Cookies from 'universal-cookie'

const apiEndpoint = process.env.NEXT_PUBLIC_API_ENDPOINT

Expand All @@ -10,11 +11,15 @@ interface ProfilesProps {

const Profiles = (_: ProfilesProps): JSX.Element => {
const queryClient = new QueryServiceClient(apiEndpoint === undefined ? '' : apiEndpoint)
return (
<ProfileExplorer
queryClient={queryClient}
/>
)
return <ProfileExplorer queryClient={queryClient} />
}

export default withRouter(Profiles)

export function getServerSideProps({ req }) {
const cookies = new Cookies(req ? req.headers.cookie : undefined)
const persistedState = cookies.get('parca') || {}
return {
props: { persistedState }
}
}
15 changes: 15 additions & 0 deletions ui/packages/app/web/src/store/cookie-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Cookies from 'universal-cookie'
import { StateStorage } from 'zustand/middleware'

const cookie = new Cookies()

const CookieStorage: StateStorage = {
setItem: (key, value) => {
cookie.set(key, value)
},
getItem: key => {
return cookie.get(key)
}
}

export default CookieStorage
62 changes: 62 additions & 0 deletions ui/packages/app/web/src/store/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useLayoutEffect } from 'react'
import create from 'zustand'
import createContext from 'zustand/context'
import { persist } from 'zustand/middleware'
import CookieStorage from './cookie-storage'
import NoopStorage from './noop-storage'
import createUiState from './ui.state'

let store

const whitelistPersist = ['ui']

const stateSlices = (set, get) => ({
...createUiState(set, get)
// add more slices
})

type AppState = ReturnType<typeof stateSlices>

const zustandContext = createContext<AppState>()

export const StoreProvider = zustandContext.Provider

export const useStore = zustandContext.useStore

export const initializeStore = (initialState = {}) => {
return create(
persist(
(set, get) => ({
...stateSlices(set, get),
...initialState
}),
{
name: 'parca',
whitelist: whitelistPersist,
getStorage: () => (typeof document !== 'undefined' ? CookieStorage : NoopStorage)
}
)
)
}

export function useCreateStore(initialState) {
// For SSR & SSG, always use a new store.
if (typeof window === 'undefined') {
return () => initializeStore(initialState)
}

// For CSR, always re-use same store.
store = store ?? initializeStore(initialState)
// And if initialState changes, then merge states in the next render cycle.
// @todo does initialState ever change?
useLayoutEffect(() => {
if (initialState && store) {
store.setState({
...store.getState(),
...initialState
})
}
}, [initialState])

return () => store
}
8 changes: 8 additions & 0 deletions ui/packages/app/web/src/store/noop-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { StateStorage } from 'zustand/middleware'

const NoopStorage: StateStorage = {
setItem: (_key, _value) => {},
getItem: key => key
}

export default NoopStorage
22 changes: 22 additions & 0 deletions ui/packages/app/web/src/store/ui.state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import produce from 'immer'

export type UiState = ReturnType<typeof createSlice>

export default function createSlice(set, _get) {
return {
// state
ui: {
darkMode: false
},
// actions
setDarkMode: (mode: boolean) => {
set(
produce<UiState>(state => {
state.ui.darkMode = mode
})
)
}
}
}

export const selectUi = (state: UiState) => state.ui

0 comments on commit 504f7a3

Please sign in to comment.