Skip to content

Commit

Permalink
improve session sdk
Browse files Browse the repository at this point in the history
  • Loading branch information
tlgimenes committed Aug 16, 2021
1 parent 32edc1e commit bac0c7d
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 119 deletions.
4 changes: 3 additions & 1 deletion packages/store-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ export type {
Actions as SessionActions,
Effects as SessionEffects,
InitialState as SessionInitialState,
} from './ui/Provider'
Currency,
User,
} from './session/Provider'
export { useSession } from './session/useSession'
141 changes: 24 additions & 117 deletions packages/store-sdk/src/session/Provider.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import React, { createContext, useMemo, useReducer } from 'react'
import type { FC, Dispatch } from 'react'
import React, { createContext, useMemo } from 'react'
import type { FC } from 'react'

interface Currency {
import { useLocalStorage } from '../utils/useLocalStorage'

export interface Currency {
code: string // USD
symbol: string // $
}

interface User {
export interface User {
id: string // user id
}

interface BaseState {
interface Session {
locale: string // en-US
currency: Currency
country: string // BRA
Expand All @@ -21,69 +23,14 @@ interface BaseState {
user: User | null
}

type State = Record<string, any> & BaseState

interface BaseContextValue extends BaseState {
setLocale: (locale: string) => void
setCurrency: (currency: Currency) => void
setCountry: (country: string) => void
setChannel: (channel: string | null) => void
setRegion: (region: string | null) => void
setPriceTable: (table: string | null) => void
setPostalCode: (postalCode: string | null) => void
setUser: (user: User) => void
export interface ContextValue extends Session {
setSession: (session: Session) => void
}

export type ContextValue = Record<string, any> & BaseContextValue

type Action =
| {
type: 'SET_LOCALE'
data: string
}
| {
type: 'SET_CURRENCY'
data: Currency
}
| {
type: 'SET_COUNTRY'
data: string
}
| {
type: 'SET_CHANNEL'
data: string | null
}
| {
type: 'SET_REGION'
data: string | null
}
| {
type: 'SET_PRICE_TABLE'
data: string | null
}
| {
type: 'SET_POSTAL_CODE'
data: string | null
}
| {
type: 'SET_USER'
data: User | null
}
| {
type: string
data?: any
}

export const Context = createContext<ContextValue | undefined>(undefined)
Context.displayName = 'StoreSessionContext'

export type Actions = Record<string, (state: State, data?: any) => State>

export type Effects = (
dispatch: Dispatch<Action>
) => Omit<ContextValue, keyof BaseContextValue>

const baseInitialState: State = {
const baseInitialState: Session = {
currency: {
code: 'USD',
symbol: '$',
Expand All @@ -97,72 +44,32 @@ const baseInitialState: State = {
user: null,
}

const reducer = (actions: Actions) => {
const allActions: Actions = {
SET_LOCALE: (state, locale) => ({ ...state, locale }),
SET_CURRENCY: (state, currency) => ({ ...state, currency }),
SET_COUNTRY: (state, country) => ({ ...state, country }),
SET_CHANNEL: (state, channel) => ({ ...state, channel }),
SET_REGION: (state, region) => ({ ...state, region }),
SET_PRICE_TABLE: (state, table) => ({ ...state, table }),
SET_POSTAL_CODE: (state, postalCode) => ({ ...state, postalCode }),
SET_USER: (state, user) => ({ ...state, user }),
...actions,
}

return (state: State, { type, data }: Action) => {
const maybeAction = allActions[type]

if (typeof maybeAction === 'function') {
return maybeAction(state, data)
}

throw new Error('Unknown Session state')
}
}

export type InitialState = Record<string, any>

interface Props {
actions?: Actions
effects?: Effects
initialState?: InitialState
initialState?: Session
namespace?: string
}

const defaultEffects: Effects = () => ({})

export const Provider: FC<Props> = ({
children,
actions = {},
effects = defaultEffects,
initialState = {},
initialState,
namespace = 'main',
}) => {
const [state, dispatch] = useReducer(reducer(actions), {
...baseInitialState,
...initialState,
})
const [session, setSession] = useLocalStorage<Session>(
`${namespace}::store::session`,
() => ({
...baseInitialState,
...initialState,
})
)

const value = useMemo(
() => ({
...state,
...effects(dispatch),
setLocale: (locale: string) =>
dispatch({ type: 'SET_LOCALE', data: locale }),
setCurrency: (currency: Currency) =>
dispatch({ type: 'SET_CURRENCY', data: currency }),
setCountry: (country: string) =>
dispatch({ type: 'SET_COUNTRY', data: country }),
setChannel: (channel: string | null) =>
dispatch({ type: 'SET_CHANNEL', data: channel }),
setRegion: (region: string | null) =>
dispatch({ type: 'SET_REGION', data: region }),
setPriceTable: (table: string | null) =>
dispatch({ type: 'SET_PRICE_TABLE', data: table }),
setPostalCode: (postalCode: string | null) =>
dispatch({ type: 'SET_POSTAL_CODE', data: postalCode }),
setUser: (user: User) => dispatch({ type: 'SET_USER', data: user }),
...session,
setSession: (data: Session) => setSession({ ...session, ...data }),
}),
[effects, state]
[session, setSession]
)

return <Context.Provider value={value}>{children}</Context.Provider>
Expand Down
3 changes: 2 additions & 1 deletion packages/store-sdk/src/session/useSession.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ContextValue } from './Provider'
import { Context } from './Provider'
import { useContext } from '../utils/useContext'

export const useSession = () => useContext(Context)
export const useSession = <T extends ContextValue>() => useContext(Context) as T
56 changes: 56 additions & 0 deletions packages/store-sdk/src/utils/useLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Safe localstorage interface. These try..catch are usefull because
* some browsers may block accesss to these APIs due to security policies
*
* Also, the local storage value is lazy-loaded to avoid hydration mimatch
* between server/browser. When state is 'hydrated', the in the heap is the
* same as the value in local storage
*/
import { useState, useEffect, useCallback } from 'react'

const getItem = <T>(key: string) => {
try {
return JSON.parse(window.localStorage.getItem(key) ?? 'null') as T | null
} catch (err) {
return null
}
}

const setItem = <T>(key: string, value: T | null) => {
try {
window.localStorage.setItem(key, JSON.stringify(value))
} catch (err) {
// noop
}
}

const isFunction = <T>(x: T | (() => T)): x is () => T =>
typeof x === 'function'

export const useLocalStorage = <T>(
key: string,
initialValue: T | (() => T)
) => {
const [data, setData] = useState(() => ({
payload: isFunction(initialValue) ? initialValue() : initialValue,
state: 'initial',
}))

useEffect(() => {
if (data.state === 'inital') {
const item = getItem<T>(key) ?? data.payload

setData({ payload: item, state: 'hydrated' })
setItem(key, item)
} else {
setItem(key, data.payload)
}
}, [data.payload, data.state, key])

const setPayload = useCallback(
(value: T) => setData((state) => ({ ...state, payload: value })),
[]
)

return [data.payload, setPayload] as [typeof data.payload, typeof setPayload]
}

0 comments on commit bac0c7d

Please sign in to comment.