From 9d0a517fa60c6c09af7ac64a4849fe5ca5638e04 Mon Sep 17 00:00:00 2001 From: jinhojang6 Date: Fri, 18 Oct 2024 01:32:07 +0900 Subject: [PATCH] feat: replace hookstate with jotai --- package.json | 4 +- src/components/Dropdown/Dropdown.tsx | 45 ++++++++---- src/containers/Explore/ExploreContainer.tsx | 25 ++++--- .../LSDThemeProvider/LSDThemeProvider.tsx | 8 +-- src/containers/LSDThemeProvider/themes.ts | 2 +- src/states/filterState/filter.state.ts | 24 ++++--- src/states/themeState/theme.state.ts | 72 ++++++++++++++----- yarn.lock | 20 ++---- 8 files changed, 120 insertions(+), 80 deletions(-) diff --git a/package.json b/package.json index 1e7eeeec99..980baa1961 100644 --- a/package.json +++ b/package.json @@ -25,16 +25,14 @@ "@acid-info/lsd-react": "^0.1.0-beta.3", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", - "@hookstate/core": "^4.0.1", - "@hookstate/localstored": "^4.0.2", "@mdx-js/loader": "^2.3.0", "@mdx-js/react": "^2.3.0", - "@next/font": "^14.2.13", "@next/mdx": "^13.5.5", "@tanstack/react-query": "^5.56.2", "@types/mdx": "^2.0.8", "@vercel/og": "^0.5.4", "axios": "^1.4.0", + "jotai": "^2.10.1", "lazysizes": "^5.3.2", "next": "^14.2.3", "next-mdx-remote": "^4.4.1", diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 0c63034070..0fb4495315 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -1,20 +1,21 @@ import { defaultFilterState } from '@/states/filterState' import styled from '@emotion/styled' -import { hookstate, useHookstate } from '@hookstate/core' +import { atom, useAtom } from 'jotai' import React, { useEffect, useRef, useState } from 'react' -import Checkbox from './Checkbox' // Import the CustomCheckbox +import Checkbox from './Checkbox' interface DropdownProps { title: string options: string[] - filterType: string - onSelectionChange: (selectedOptions: string[], filterType: string) => void + filterType: keyof typeof defaultFilterState + onSelectionChange: ( + selectedOptions: string[], + filterType: keyof typeof defaultFilterState, + ) => void prefill?: string[] } -const globalState = hookstate(() => - JSON.parse(JSON.stringify(defaultFilterState)), -) +const filterAtom = atom(defaultFilterState) const Dropdown: React.FC = ({ title, @@ -29,17 +30,17 @@ const Dropdown: React.FC = ({ const dropdownRef = useRef(null) - const state = useHookstate(globalState) + const [filter, setFilter] = useAtom(filterAtom) - const defualtState = state.get() + const defaultState = defaultFilterState useEffect(() => { - if (defualtState[filterType]?.length === selectedOptions?.length) { + if (defaultState[filterType]?.length === selectedOptions?.length) { setUpdated(false) } else { setUpdated(true) } - }, [defualtState, selectedOptions]) + }, [defaultState, selectedOptions, filterType]) useEffect(() => { if (prefill?.length) { @@ -56,16 +57,23 @@ const Dropdown: React.FC = ({ } const handleSelect = (option: string) => { - let newSelectedOptions = [] + let newSelectedOptions: any = [] + if (selectedOptions.includes(option)) { newSelectedOptions = selectedOptions.filter((o) => o !== option) } else { newSelectedOptions = [...selectedOptions, option] } + setSelectedOptions(newSelectedOptions) onSelectionChange(newSelectedOptions, filterType) handleUpdate(newSelectedOptions) + + setFilter((prev) => ({ + ...prev, + [filterType]: newSelectedOptions, + })) } const selectAll = () => { @@ -73,6 +81,11 @@ const Dropdown: React.FC = ({ onSelectionChange(options, filterType) setUpdated(false) + + setFilter((prev) => ({ + ...prev, + [filterType]: options, + })) } const clearAll = () => { @@ -80,6 +93,11 @@ const Dropdown: React.FC = ({ onSelectionChange([], filterType) setUpdated(true) + + setFilter((prev) => ({ + ...prev, + [filterType]: [], + })) } const toggleDropdown = () => { @@ -139,8 +157,6 @@ const Dropdown: React.FC = ({ ) } -// Additional styled components for Dropdown - const DropdownContainer = styled.div` position: relative; display: inline-block; @@ -219,7 +235,6 @@ const ScrollDiv = styled.div` overflow-x: hidden; box-sizing: border-box; - // white scrollbar &::-webkit-scrollbar { width: 10px; } diff --git a/src/containers/Explore/ExploreContainer.tsx b/src/containers/Explore/ExploreContainer.tsx index 96657f975f..8f03c1a378 100644 --- a/src/containers/Explore/ExploreContainer.tsx +++ b/src/containers/Explore/ExploreContainer.tsx @@ -1,8 +1,8 @@ import { Dropdown } from '@/components/Dropdown' import { OperatorGrid } from '@/components/Explore/OperatorGrid' -import { defaultFilterState } from '@/states/filterState' +import { defaultFilterState, FilterState } from '@/states/filterState' import styled from '@emotion/styled' -import { hookstate, useHookstate } from '@hookstate/core' +import { atom, useAtom } from 'jotai' import React, { useCallback, useMemo } from 'react' import useGetOperators from '../../../apis/operators/useGetOperators' import { @@ -17,15 +17,12 @@ import { processOperators, shuffleOperators } from '../../../utils/operators' interface ExploreSectionProps {} -const globalState = hookstate(() => - JSON.parse(JSON.stringify(defaultFilterState)), -) +const filterAtom = atom(defaultFilterState) const ExploreSection: React.FC = () => { const { data, isLoading } = useGetOperators() - const state = useHookstate(globalState) - const filter = state.get() + const [filter, setFilter] = useAtom(filterAtom) const processedOperators = processOperators( data as any, @@ -47,15 +44,17 @@ const ExploreSection: React.FC = () => { const handleFilterChange = ( selectedOptions: string[], - filterType: string, + filterType: keyof typeof filter, ) => { - // @ts-ignore - state[filterType].set(selectedOptions) + setFilter((prev) => ({ + ...prev, + [filterType]: selectedOptions, + })) } const handleResetAll = useCallback(() => { - state.set(JSON.parse(JSON.stringify(defaultFilterState))) - }, [filter]) + setFilter(JSON.parse(JSON.stringify(defaultFilterState))) + }, [setFilter]) return ( @@ -104,7 +103,7 @@ const ExploreSection: React.FC = () => { prefill={filter.background.slice()} /> - Reset All + Reset All close-black @@ -15,9 +15,9 @@ export const LSDThemeProvider: React.FC = ({ useEffect(() => { const html = document.querySelector('html') as HTMLElement - html.setAttribute('data-theme', mode.value) - html.setAttribute('data-font-family', genericFontFamily.value) - }, [mode.value, genericFontFamily.value]) + html.setAttribute('data-theme', mode) // Removed `.value` since Jotai provides direct values + html.setAttribute('data-font-family', genericFontFamily) // Removed `.value` + }, [mode, genericFontFamily]) // Removed `.value` in dependencies return ( diff --git a/src/containers/LSDThemeProvider/themes.ts b/src/containers/LSDThemeProvider/themes.ts index 1db3654db3..9888bf5465 100644 --- a/src/containers/LSDThemeProvider/themes.ts +++ b/src/containers/LSDThemeProvider/themes.ts @@ -33,7 +33,7 @@ const useThemeCssVars = (theme: Theme, colorMode: string) => ) export const useLSDTheme = () => { - const { genericFontFamily, mode } = useThemeState().get() + const { genericFontFamily, mode } = useThemeState() const themes = useMemo(() => { const options: CreateThemeProps = { diff --git a/src/states/filterState/filter.state.ts b/src/states/filterState/filter.state.ts index 06f91ff192..0678f550eb 100644 --- a/src/states/filterState/filter.state.ts +++ b/src/states/filterState/filter.state.ts @@ -1,5 +1,4 @@ -import { hookstate, State, useHookstate } from '@hookstate/core' -import { localstored } from '@hookstate/localstored' +import { atom, useAtom } from 'jotai' import { ARCHETYPE, BACKGROUND, @@ -28,17 +27,20 @@ export const defaultFilterState: FilterState = { background: BACKGROUND, } -const filterState = - typeof window === 'undefined' - ? hookstate(defaultFilterState) - : hookstate(defaultFilterState, localstored({ key: 'filter' })) +const filterAtom = atom(defaultFilterState) -const wrapFilterState = (state: State) => ({ - filter: { ...state.value }, - get: () => state.value, - setFilter: (value: FilterState) => state.set(value), +const wrapFilterState = ( + filter: FilterState, + setFilter: (value: FilterState) => void, +) => ({ + filter, + get: () => filter, + setFilter: (value: FilterState) => setFilter(value), }) -export const useFilterState = () => wrapFilterState(useHookstate(filterState)) +export const useFilterState = () => { + const [filter, setFilter] = useAtom(filterAtom) + return wrapFilterState(filter, setFilter) +} export default useFilterState diff --git a/src/states/themeState/theme.state.ts b/src/states/themeState/theme.state.ts index 27eafbd316..35b6d6d61a 100644 --- a/src/states/themeState/theme.state.ts +++ b/src/states/themeState/theme.state.ts @@ -1,6 +1,5 @@ import { TypographyGenericFontFamily } from '@acid-info/lsd-react' -import { hookstate, State, useHookstate } from '@hookstate/core' -import { localstored } from '@hookstate/localstored' +import { atom, useAtom } from 'jotai' export type ThemeState = { mode: 'light' | 'dark' @@ -12,24 +11,61 @@ export const defaultThemeState: ThemeState = { genericFontFamily: 'Inter', } -const themeState = - typeof window === 'undefined' - ? hookstate(defaultThemeState) - : hookstate(defaultThemeState, localstored({ key: 'theme' })) - -const wrapThemeState = (state: State) => ({ - mode: state.mode, - genericFontFamily: state.genericFontFamily, - get: () => state.value, - setMode: (value: ThemeState['mode']) => state.mode.set(value), - setGenericFontFamily: (value: ThemeState['genericFontFamily']) => - state.genericFontFamily.set('Inter'), - toggleMode: () => - state.mode.set(state.mode.get() === 'dark' ? 'light' : 'dark'), +const themeAtom = atom(() => { + if (typeof window !== 'undefined') { + const storedTheme = localStorage.getItem('theme') + return storedTheme ? JSON.parse(storedTheme) : defaultThemeState + } + return defaultThemeState }) -export const useThemeState = () => wrapThemeState(useHookstate(themeState)) +const persistentThemeAtom = atom( + (get) => get(themeAtom), + (get, set, newTheme: ThemeState) => { + set(JSON.parse(JSON.stringify(newTheme))) + if (typeof window !== 'undefined') { + localStorage.setItem('theme', JSON.stringify(newTheme)) + } + }, +) -export const useIsDarkTheme = () => useThemeState().mode.get() === 'dark' +export const useThemeState = () => { + const [theme, setTheme] = useAtom(persistentThemeAtom) + + const setMode = (mode: ThemeState['mode']) => { + setTheme({ + ...theme, + mode, + }) + } + + const setGenericFontFamily = ( + fontFamily: ThemeState['genericFontFamily'], + ) => { + setTheme({ + ...theme, + genericFontFamily: fontFamily, + }) + } + + const toggleMode = () => { + setTheme({ + ...theme, + mode: theme.mode === 'dark' ? 'light' : 'dark', + }) + } + + return { + ...theme, + setMode, + setGenericFontFamily, + toggleMode, + } +} + +export const useIsDarkTheme = () => { + const theme = useThemeState() + return theme.mode === 'dark' +} export default useThemeState diff --git a/yarn.lock b/yarn.lock index deb43300f4..a35719412a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -267,16 +267,6 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.51.0.tgz#6d419c240cfb2b66da37df230f7e7eef801c32fa" integrity sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg== -"@hookstate/core@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@hookstate/core/-/core-4.0.1.tgz#6744380e96ce13fe3488c926c1cbae93bbea0ff6" - integrity sha512-Uh2D8Z0z/pqOJ7t+SfC+2sj13JQcB4yFhtL+T1choCaBxTSlgOS/CKRBohgJ4cjTKoxOmTT8uSQysu3gUjX+Gw== - -"@hookstate/localstored@^4.0.2": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@hookstate/localstored/-/localstored-4.0.2.tgz#6f0ef6b71baa4c28e5e8835431585500eb6a9532" - integrity sha512-81dbUH6xnwbePby21NQN8zrmjvMA8tOYeOOLZXqRmRGKnhtcokjTJoRNMZugpC9P3/NnacllBZ5ZUt43nmrKkA== - "@humanwhocodes/config-array@^0.11.11": version "0.11.11" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844" @@ -364,11 +354,6 @@ dependencies: glob "10.3.10" -"@next/font@^14.2.13": - version "14.2.13" - resolved "https://registry.yarnpkg.com/@next/font/-/font-14.2.13.tgz#cfe0903654e65b58e7b5cdc600399a45e028bbdb" - integrity sha512-xtleJ172yNVSVoQoMIwPHnM9zSPOzNB964mZMOFmRaLWgWMgmlhCVI42dlDSGc6d9nQ/WHCmCZaJMloS1HHGXg== - "@next/mdx@^13.5.5": version "13.5.5" resolved "https://registry.yarnpkg.com/@next/mdx/-/mdx-13.5.5.tgz#ce1ff29ab424f2335a02ac6a662f56938c275b5d" @@ -2562,6 +2547,11 @@ jackspeak@^2.3.5: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jotai@^2.10.1: + version "2.10.1" + resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.10.1.tgz#8d5598d06fa295110de0914f10bd1d10ea229723" + integrity sha512-4FycO+BOTl2auLyF2Chvi6KTDqdsdDDtpaL/WHQMs8f3KS1E3loiUShQzAzFA/sMU5cJ0hz/RT1xum9YbG/zaA== + js-cookie@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"