Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/state/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"clean": "rimraf dist",
"build": "tsc -b",
"dev": "pnpm build -w",
"test": "echo 1"
"test": "vitest run"
},
"keywords": [
"react",
Expand All @@ -49,9 +49,9 @@
},
"devDependencies": {
"@types/react": "^19.0.0",
"ava": "^6.1.3",
"react": "^19.0.0",
"rimraf": "^3.0.2",
"typescript": "^5.4.5"
"typescript": "^5.4.5",
"vitest": "^2.1.8"
}
}
9 changes: 9 additions & 0 deletions packages/state/src/__tests__/logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { test, expect } from 'vitest'

import logger from '../middleware/logger'

test('using logger middleware should log', () => {
expect(true).toBe(true)
})


9 changes: 9 additions & 0 deletions packages/state/src/__tests__/persistence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { test, expect } from 'vitest'

import persistence from '../middleware/persistence'

test('using persistence middleware should work', () => {
expect(true).toBe(true)
})


9 changes: 9 additions & 0 deletions packages/state/src/__tests__/useStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { test, expect } from 'vitest'

import useStore from '../useStore'

test('using stores should work', () => {
expect(true).toBe(true)
})


9 changes: 9 additions & 0 deletions packages/state/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
export * from './useStoreFactory'
export { default as useStore } from './useStore'
export { default as useStoreWithMiddleware } from './useStoreWithMiddleware'
export { default as useOptimisticState } from './useOptimisticState'
export { default as useOptimisticStore } from './useOptimisticStore'
export { default as useOptimisticStoreWithMiddleware } from './useOptimisticStoreWithMiddleware'

// Middleware
export { default as logger } from './middleware/logger'
export { default as persistence } from './middleware/persistence'
32 changes: 32 additions & 0 deletions packages/state/src/middleware/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { T_Middleware, T_MiddlewareContext } from '../useStoreFactory'

type Config = {
prefix?: string
}

const defaultConfig = {
prefix: 'ALVERON:',
}
export default function logger(userConfig: Config = {}): T_Middleware {
const config = {
...defaultConfig,
...userConfig,
}

function middleware(
nextState: any,
{ action, payload, prevState }: T_MiddlewareContext
) {
console.log(config.prefix + action, {
payload,
prevState,
nextState,
})

return nextState
}

return {
middleware,
}
}
78 changes: 78 additions & 0 deletions packages/state/src/middleware/persistence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { T_Middleware, T_MiddlewareContext } from '../useStoreFactory'

type SyntheticStorage<T = any> = {
getItem: (key: string) => Promise<T>
setItem: (key: string, value: T) => void
}
type Config<T> = {
key: string
getStorage: () => Storage | SyntheticStorage
actions?: Array<string>
onHydrated?: (data?: T) => void
encode?: (data?: T) => any
decode?: (data: any) => T
}

export default function persistence<T>({
key,
getStorage,
actions,
onHydrated,
encode = JSON.stringify,
decode = JSON.parse,
}: Config<T>): T_Middleware {
function middleware(nextState: any, { action }: T_MiddlewareContext) {
if (actions && Array.isArray(actions) && !actions.includes(action)) {
return nextState
}

const storage = getStorage()

if (storage) {
try {
storage.setItem(key, encode(nextState))
} catch (e) {}
}

return nextState
}

function effect(setState: any) {
const storage = getStorage()

if (storage) {
const isAsync = storage.getItem.constructor.name === 'AsyncFunction'

async function getData() {
if (isAsync) {
return await storage.getItem(key)
} else {
return storage.getItem(key)
}
}

async function hydrate() {
const data = await getData()

let parsedData
if (data) {
try {
parsedData = decode(data)
setState(parsedData)
} catch (e) {}
}

if (onHydrated) {
onHydrated(parsedData)
}
}

hydrate()
}
}

return {
middleware,
effect,
}
}
5 changes: 5 additions & 0 deletions packages/state/src/useOptimisticStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import useOptimisticStoreWithMiddleware from './useOptimisticStoreWithMiddleware'

export default useOptimisticStoreWithMiddleware()


27 changes: 27 additions & 0 deletions packages/state/src/useOptimisticStoreWithMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// @ts-ignore
import { useOptimistic } from 'react'

import useStoreFactory, { T_Middleware } from './useStoreFactory'

export default function useOptimisticStoreWithMiddleware<Model>(
middleware: Array<T_Middleware<Model>> = []
) {
function useState(initialState: Model): [Model, (newState: Model) => void] {
const [state, setState] = useOptimistic(
initialState,
(_: Model, update: Model) => update
)

function setFunctionalState(newState: Model) {
if (typeof newState === 'function') {
return setState(newState(state))
}

return setState(newState)
}

return [state, setFunctionalState]
}

return useStoreFactory<Model>(useState)(middleware)
}
5 changes: 5 additions & 0 deletions packages/state/src/useStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import useStoreWithMiddleware from './useStoreWithMiddleware'

export default useStoreWithMiddleware()


101 changes: 101 additions & 0 deletions packages/state/src/useStoreFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { useEffect } from 'react'

type StoreContext = Record<string, any>

export type T_MiddlewareContext = {
action: string
payload: any
prevState: any
}

export type T_Middleware<T = any> = {
middleware: (state: T, context: T_MiddlewareContext) => any
effect?: (setState: any) => void
}

export type T_ResolvedActions<Actions, Model> = {
[Property in keyof Actions]: Actions[Property] extends (
state: Model,
...payload: infer Payload
) => any
? (...payload: Payload) => void
: never
}

export type T_Effect<Actions, Model> = (
actions: T_ResolvedActions<Actions, Model>
) => void
export type T_ActionReturn<Actions, Model> = [Model, T_Effect<Actions, Model>?]

type UseState<T> = (initialState: T) => [T, any]

export default function useStoreFactory<T>(useState: UseState<T>) {
return function useStoreWithMiddleware(
middleware: Array<T_Middleware<T>> = []
) {
return function useStore<Model extends T, Actions, Context = StoreContext>(
actions: Actions & Record<string, (state: Model, ...payload: any) => any>,
initialState: Model,
context?: Context
): [Model, T_ResolvedActions<Actions, Model>] {
const [state, setState] = useState(initialState)

useEffect(
() => middleware.forEach(({ effect }) => effect && effect(setState)),
[]
)

type ActionName = keyof Actions
const actionNames = Object.keys(actions) as Array<ActionName>

const resolvedActions = actionNames.reduce(
(resolved: T_ResolvedActions<Actions, Model>, name: ActionName) => {
const fn = (...payload: any) => {
const action = actions[name] as any

setState((prevState: Model) => {
const result = action(prevState, ...payload)

// safety checks for a more convenient DX
if (!Array.isArray(result)) {
throw Error(
`Wrong format returned from action ("${name.toString()}"). Expected a tuple of [newState, effect], but got ${typeof result}. Make sure to wrap your state with an additional array. See https://stack.weser.io/state/concepts/action`
)
}

const [newState, effect, ...rest] = result

// safety checks for a more convenient DX
if (rest.length > 0) {
throw Error(
`Too many values return from an action ("${name.toString()}"). Expected a tuple of [newState, effect]. If your state is an array, make sure to wrap it with an additional array when you return it. See https://stack.weser.io/state/concepts/action`
)
}

if (effect && typeof effect === 'function') {
effect(resolvedActions, context)
}

return middleware.reduce(
(newState, { middleware }) =>
middleware(newState, {
action: name.toString(),
payload,
prevState,
}),
newState
)
})
}

resolved[name] = fn as any

return resolved
},
{} as T_ResolvedActions<Actions, Model>
)

return [state as Model, resolvedActions]
}
}
}
9 changes: 9 additions & 0 deletions packages/state/src/useStoreWithMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useState } from 'react'

import useStoreFactory, { T_Middleware } from './useStoreFactory'

export default function useStoreWithMiddleware<Model>(
middleware: Array<T_Middleware<Model>> = []
) {
return useStoreFactory<Model>(useState<Model>)(middleware)
}
13 changes: 13 additions & 0 deletions packages/state/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
include: ['**/__tests__/**/*.{js,ts}'],
globals: false,
},
esbuild: {
target: 'esnext',
},
})


11 changes: 0 additions & 11 deletions packages/theme/src/__tests__/alpha.test.js

This file was deleted.

Loading