Skip to content

Commit

Permalink
feat: react philosophy (#2)
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <[email protected]>
  • Loading branch information
Innei authored Aug 19, 2023
1 parent d7afb69 commit 061870c
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 21 deletions.
16 changes: 11 additions & 5 deletions src/components/Counter.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useAtom } from 'jotai'
import { useAtomValue, useSetAtom } from 'jotai'
import { counterAtom } from '../atoms/counter'
import { useEventCallback } from '../hooks/useEventCallback'

export function Counter() {
const [count, setCount] = useAtom(counterAtom)
const increment = () => setCount(c => c + 1)
const decrement = () => setCount(c => c - 1)
const setCount = useSetAtom(counterAtom)
const increment = useEventCallback(() => setCount(c => c + 1))
const decrement = useEventCallback(() => setCount(c => c - 1))

return (
<div className="flex justify-center items-center mt-4 mb-8">
Expand All @@ -24,7 +25,7 @@ export function Counter() {
select-none
"
>
<samp>Count: {count}</samp>
<samp>Count: <Count /></samp>
</div>
<button
className="btn btn-ghost btn-circle text-[1.5rem] mx-2"
Expand All @@ -36,3 +37,8 @@ export function Counter() {
</div>
)
}

function Count() {
const count = useAtomValue(counterAtom)
return count
}
23 changes: 8 additions & 15 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useDarkMode } from '../hooks/useDarkMode'
import viteLogo from '/vite.svg'
import { locales } from '../i18n/locales'
import { useIsDark, useThemeActions } from '../providers/dark-mode'

function ThemeCheck() {
const { toggle } = useThemeActions()
const isDarkMode = useIsDark()

return <input type="checkbox" checked={isDarkMode} onChange={toggle} />
}
export function Footer() {
const { toggle, isDarkMode } = useDarkMode()
const { t, i18n } = useTranslation()

useEffect(() => {
if (isDarkMode) {
document.documentElement.dataset.theme = 'business'
document.documentElement.classList.toggle('dark', true)
}
else {
document.documentElement.dataset.theme = 'bumblebee'
document.documentElement.classList.toggle('dark', false)
}
}, [isDarkMode])

return (
<div className='flex flex-col items-center'>
<div className='flex justify-center items-center'>
Expand Down Expand Up @@ -62,7 +55,7 @@ export function Footer() {
</div>
<div className='flex justify-center items-center mt-4'>
<label className='swap btn btn-ghost btn-sm rounded-md mx-2'>
<input type="checkbox" checked={isDarkMode} onChange={toggle} />
<ThemeCheck />
<div className='swap-on flex justify-center items-center'>
<div className='i-carbon-moon' />
<span className='ml-2'>{t('dark-mode')}</span>
Expand Down
8 changes: 8 additions & 0 deletions src/hooks/useEventCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { useCallback, useRef } from 'react'

export function useEventCallback<T extends (...args: any[]) => any>(fn: T) {
const ref = useRef<T>(fn)
ref.current = fn

return useCallback((...args: any[]) => ref.current(...args), []) as T
}
11 changes: 11 additions & 0 deletions src/hooks/useRefValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useRef } from 'react'

// @see https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents
export function useRefValue<T>(value: () => T): T {
const ref = useRef<T>()

if (!ref.current)
ref.current = value()

return ref.current!
}
3 changes: 3 additions & 0 deletions src/lib/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createStore } from 'jotai/vanilla'

export const jotaiStore = createStore()
5 changes: 4 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { router } from './router'
import './i18n'
import './styles/tailwind.css'
import './styles/global.css'
import { Providers } from './providers'

function App() {
return (
Expand All @@ -19,6 +20,8 @@ function App() {

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
<Providers>
<App />
</Providers>
</React.StrictMode>,
)
67 changes: 67 additions & 0 deletions src/providers/dark-mode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { PropsWithChildren } from 'react'
import { createContext, useContext, useEffect } from 'react'
import { atomWithStorage } from 'jotai/utils'
import { useAtomValue } from 'jotai'
import { jotaiStore } from '../lib/store'

const darkModeAtom = atomWithStorage('dark-mode', false)

const actions = {
// TODO
toggle: () => jotaiStore.set(darkModeAtom, prev => !prev),
}

const DarkModeActionContext = createContext<
typeof actions
>(null!)
const COLOR_SCHEME_QUERY = '(prefers-color-scheme: dark)'

function getMatches(query: string): boolean {
// Prevents SSR issues
if (typeof window !== 'undefined')
return window.matchMedia(query).matches
return false
}

export function DarkModeProvider(props: PropsWithChildren) {
// const isOSDark = useMediaQuery(COLOR_SCHEME_QUERY)

useEffect(() => {
const matchMedia = window.matchMedia(COLOR_SCHEME_QUERY)
// Triggered at the first client-side load and if query changes
const handleChange = () => {
jotaiStore.set(darkModeAtom, getMatches(COLOR_SCHEME_QUERY))
}
// Listen matchMedia
matchMedia.addEventListener('change', handleChange)
return () => {
matchMedia.removeEventListener('change', handleChange)
}
}, [])
return <DarkModeActionContext.Provider value={actions}>
{props.children}
<ThemeObserver />
</DarkModeActionContext.Provider>
}

function ThemeObserver() {
const isDarkMode = useAtomValue(darkModeAtom)
useEffect(() => {
if (isDarkMode) {
document.documentElement.dataset.theme = 'business'
document.documentElement.classList.toggle('dark', true)
}
else {
document.documentElement.dataset.theme = 'bumblebee'
document.documentElement.classList.toggle('dark', false)
}
}, [isDarkMode])

return null
}

export function useThemeActions() {
return useContext(DarkModeActionContext)
}

export const useIsDark = () => useAtomValue(darkModeAtom)
21 changes: 21 additions & 0 deletions src/providers/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client'

import type { FC, PropsWithChildren } from 'react'
import React from 'react'
import { useRefValue } from '../hooks/useRefValue'
import { JotaiStoreProvider } from './jotai'
import { DarkModeProvider } from './dark-mode'

const ProviderComposer: FC<{
contexts: JSX.Element[]
} & PropsWithChildren> = ({ contexts, children }) => {
return contexts.reduceRight((kids: any, parent: any) => {
return React.cloneElement(parent, { children: kids })
}, children)
}

export function Providers(props: PropsWithChildren) {
return <ProviderComposer contexts={useRefValue(() => [<JotaiStoreProvider key={'JotaiStoreProvider'} />, <DarkModeProvider key={'DarkModeProvider'} />])}>
{props.children}
</ProviderComposer>
}
9 changes: 9 additions & 0 deletions src/providers/jotai.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Provider } from 'jotai'
import type { PropsWithChildren } from 'react'
import { jotaiStore } from '../lib/store'

export const JotaiStoreProvider: React.FC<PropsWithChildren> = ({ children }) => {
return <Provider store={jotaiStore}>
{children}
</Provider>
}

0 comments on commit 061870c

Please sign in to comment.