diff --git a/packages/react/src/useFleurDomain.spec.tsx b/packages/react/src/useFleurDomain.spec.tsx
new file mode 100644
index 00000000..f422e09d
--- /dev/null
+++ b/packages/react/src/useFleurDomain.spec.tsx
@@ -0,0 +1,50 @@
+import React from 'react'
+import Fleur, { AppContext, minOps } from '@fleur/fleur'
+import { renderHook } from '@testing-library/react-hooks'
+import { FleurContext } from './ComponentReactContext'
+import { useFleurDomain } from './useFleurDomain'
+
+describe('useFleurDomain', () => {
+ // App
+ const [TestStore, testOps] = minOps('Test', {
+ ops: {
+ a(x, value: string) {
+ x.commit({ check: true, value })
+ },
+ b() {},
+ },
+ initialState: () => ({ check: false, value: '' }),
+ })
+
+ const app = new Fleur({ stores: [TestStore] })
+
+ const wrapperFactory = (context: AppContext) => {
+ return ({ children }: { children: React.ReactNode }) => (
+ {children}
+ )
+ }
+
+ it('Should work', async () => {
+ const context = app.createContext()
+ const { result, waitForNextUpdate, unmount } = renderHook(
+ () => useFleurDomain(TestStore, testOps),
+ {
+ wrapper: wrapperFactory(context),
+ },
+ )
+
+ expect(result.current[1].a).toBeInstanceOf(Function)
+ expect(result.current[1].b).toBeInstanceOf(Function)
+ expect(result.current[0]).toMatchObject({
+ check: false,
+ value: '',
+ })
+
+ result.current[1].a('hello')
+ await waitForNextUpdate()
+
+ expect(result.current[0]).toMatchObject(context.getStore(TestStore).state)
+
+ unmount()
+ })
+})
diff --git a/packages/react/src/useFleurDomain.ts b/packages/react/src/useFleurDomain.ts
new file mode 100644
index 00000000..c8ef0a93
--- /dev/null
+++ b/packages/react/src/useFleurDomain.ts
@@ -0,0 +1,77 @@
+import { Operation, OperationArgs, StoreClass } from '@fleur/fleur'
+import React, { useCallback, useMemo, useReducer, useRef } from 'react'
+import { useInternalFleurContext } from './useFleurContext'
+import {
+ useIsomorphicLayoutEffect,
+ canUseDOM,
+ bounce,
+ isEqual,
+} from './utils/utils'
+
+type StateOfStore> = S extends StoreClass
+ ? R
+ : never
+
+export const useFleurDomain = <
+ S extends StoreClass,
+ O extends { [k: string]: Operation }
+>(
+ Store: S,
+ ops: O,
+ checkEquality:
+ | ((prev: StateOfStore, next: StateOfStore) => boolean)
+ | null = isEqual,
+): [
+ StateOfStore,
+ { [K in keyof O]: (...args: OperationArgs) => void },
+] => {
+ const {
+ context: { getStore, executeOperation },
+ synchronousUpdate,
+ } = useInternalFleurContext()
+
+ const isMounted = useRef(false)
+ const latestState = useRef | null>(null)
+ const [, rerender] = useReducer((s) => s + 1, 0)
+
+ const bindedOps = useMemo(
+ () =>
+ Object.keys(ops).reduce(
+ (binded, key) =>
+ Object.assign(binded, {
+ [key]: (...args: any) => executeOperation(ops[key], ...args),
+ }),
+ Object.create(null),
+ ),
+ [
+ /* ignore ops set changing */
+ ],
+ )
+
+ const handleStoreMutation = useCallback(() => {
+ const nextState = getStore(Store).state
+
+ if (checkEquality?.(latestState.current!, nextState)) return
+ latestState.current = nextState
+
+ rerender()
+ }, [Store])
+
+ useIsomorphicLayoutEffect(() => {
+ isMounted.current = true
+
+ const bounced =
+ // Synchronous mapping on SSR
+ canUseDOM && !synchronousUpdate
+ ? bounce(handleStoreMutation, 10)
+ : handleStoreMutation
+
+ getStore(Store).on(bounced)
+
+ return () => {
+ getStore(Store).off(bounced)
+ }
+ }, [handleStoreMutation, synchronousUpdate])
+
+ return [getStore(Store).state, bindedOps]
+}
diff --git a/packages/react/src/useStore.ts b/packages/react/src/useStore.ts
index e3c40030..a7eac3a9 100644
--- a/packages/react/src/useStore.ts
+++ b/packages/react/src/useStore.ts
@@ -1,75 +1,15 @@
-import {
- useCallback,
- useEffect,
- useLayoutEffect,
- useReducer,
- useRef,
-} from 'react'
+import { useCallback, useReducer, useRef } from 'react'
import { StoreClass, StoreGetter } from '@fleur/fleur'
import { useInternalFleurContext } from './useFleurContext'
+import {
+ canUseDOM,
+ isEqual,
+ useIsomorphicLayoutEffect,
+ bounce,
+} from './utils/utils'
type StoreToPropMapper = (getStore: StoreGetter) => any
-const canUseDOM = typeof window !== 'undefined'
-
-const useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect
-
-const bounce = (fn: () => void, bounceTime: number) => {
- let lastExecuteTime = 0
-
- const bouncer = () => {
- const now = Date.now()
-
- if (now - lastExecuteTime > bounceTime) {
- fn()
- lastExecuteTime = now
- } else {
- setTimeout(bouncer, bounceTime)
- }
- }
-
- return bouncer
-}
-
-const hasOwnKey = Object.prototype.hasOwnProperty
-
-// Object.is polyfill
-const is = (x: any, y: any): boolean => {
- if (x === y) {
- return x !== 0 || y !== 0 || 1 / x === 1 / y
- } else {
- return x !== x && y !== y
- }
-}
-
-/** Shallow equality check */
-const isEqual = (prev: any, next: any) => {
- if (is(prev, next)) return true
- if (typeof prev !== typeof next) return false
-
- if (Array.isArray(prev) && Array.isArray(next)) {
- if (prev.length !== next.length) return false
-
- for (const idx in prev) {
- if (!is(prev[idx], next[idx])) return false
- }
- }
-
- if (
- typeof prev === 'object' &&
- typeof next === 'object' &&
- prev !== null &&
- next !== null
- ) {
- for (const key in prev) {
- if (!hasOwnKey.call(next, key)) continue
- if (!is(prev[key], next[key])) return false
- }
- }
-
- return false
-}
-
export const useStore = (
mapStoresToProps: Mapper,
checkEquality:
@@ -86,7 +26,7 @@ export const useStore = (
const referencedStores = useRef>(new Set())
const isMounted = useRef(false)
- const [, rerender] = useReducer(s => s + 1, 0)
+ const [, rerender] = useReducer((s) => s + 1, 0)
const getStoreInspector = useCallback(
(storeClass: T) => {
@@ -131,12 +71,12 @@ export const useStore = (
useIsomorphicLayoutEffect(() => {
isMounted.current = true
- referencedStores.current.forEach(store => {
+ referencedStores.current.forEach((store) => {
getStore(store).on(bouncedHandleStoreMutation)
})
return () => {
- referencedStores.current.forEach(store => {
+ referencedStores.current.forEach((store) => {
getStore(store).off(bouncedHandleStoreMutation)
})
}
diff --git a/packages/react/src/utils/utils.ts b/packages/react/src/utils/utils.ts
new file mode 100644
index 00000000..ae6054ee
--- /dev/null
+++ b/packages/react/src/utils/utils.ts
@@ -0,0 +1,65 @@
+import { useEffect, useLayoutEffect } from 'react'
+
+export const canUseDOM = typeof window !== 'undefined'
+
+export const useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect
+
+export const hasOwnKey = (o: O, v: string | symbol | number) =>
+ Object.prototype.hasOwnProperty.call(o, v)
+
+// Object.is polyfill
+export const is = (x: any, y: any): boolean => {
+ if (x === y) {
+ return x !== 0 || y !== 0 || 1 / x === 1 / y
+ } else {
+ return x !== x && y !== y
+ }
+}
+
+/** Shallow equality check */
+export const isEqual = (prev: any, next: any) => {
+ if (is(prev, next)) return true
+ if (typeof prev !== typeof next) return false
+
+ if (Array.isArray(prev) && Array.isArray(next)) {
+ if (prev.length !== next.length) return false
+
+ for (const idx in prev) {
+ if (!is(prev[idx], next[idx])) return false
+ }
+ }
+
+ if (
+ typeof prev === 'object' &&
+ typeof next === 'object' &&
+ prev !== null &&
+ next !== null
+ ) {
+ for (const key in prev) {
+ if (!hasOwnKey.call(next, key)) continue
+ if (!is(prev[key], next[key])) return false
+ }
+ }
+
+ return false
+}
+
+export const bounce = any>(
+ fn: T,
+ bounceTime: number,
+) => {
+ let lastExecuteTime = 0
+
+ const bouncer = (...args: Parameters) => {
+ const now = Date.now()
+
+ if (now - lastExecuteTime > bounceTime) {
+ fn(...args)
+ lastExecuteTime = now
+ } else {
+ setTimeout(bouncer, bounceTime, ...args)
+ }
+ }
+
+ return bouncer
+}