64 Commits

Author SHA1 Message Date
Julien Valverdé
6526953c37 useSuspense
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-16 00:43:28 +01:00
Julien Valverdé
4d411bc8dc useFork refactoring
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-16 00:02:56 +01:00
Julien Valverdé
0e7f5d93bb Fix
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 23:54:38 +01:00
Julien Valverdé
ca961e9122 useLayoutEffect
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 23:44:36 +01:00
Julien Valverdé
e340ab0c8e Scope options
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 23:40:56 +01:00
Julien Valverdé
2adc6ed186 Scope fix
Some checks failed
Lint / lint (push) Failing after 11s
2025-01-15 23:30:02 +01:00
Julien Valverdé
9ff34addcd useEffect scope
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 23:22:43 +01:00
Julien Valverdé
010416f0b1 useEffect
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 22:12:33 +01:00
Julien Valverdé
14e028e8c8 Todo fix
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 22:05:21 +01:00
Julien Valverdé
53ed31be8d Todo work
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 21:46:13 +01:00
Julien Valverdé
b46ba311ae Fix
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 21:04:06 +01:00
Julien Valverdé
8e6adc6b85 Todo work
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 20:59:07 +01:00
Julien Valverdé
3813e63982 Todo work
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 20:09:00 +01:00
Julien Valverdé
f3f44d9abe Refactoring
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 18:07:40 +01:00
Julien Valverdé
9acf34ee4a Fix
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 17:54:56 +01:00
Julien Valverdé
4a525d5f5d Example refactoring
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 17:52:28 +01:00
Julien Valverdé
8b5c6169da Cleanup
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 17:42:37 +01:00
Julien Valverdé
e142010128 useMergeAllLayers
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 17:40:50 +01:00
Julien Valverdé
3cc34a2ed1 Working Reffuse
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 16:41:23 +01:00
Julien Valverdé
15c1fdd54c useMergeAll
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 02:06:46 +01:00
Julien Valverdé
cb798ad466 Tests
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 01:54:27 +01:00
Julien Valverdé
75c3ad31d0 Fix
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 01:32:18 +01:00
Julien Valverdé
102e8a12b6 ReffuseContext
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 01:09:22 +01:00
Julien Valverdé
52b7b071f4 Tests
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 00:37:00 +01:00
Julien Valverdé
9f08894b61 ReffuseContext
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 00:16:57 +01:00
Julien Valverdé
ec264e0381 State
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-14 22:22:10 +01:00
Julien Valverdé
18d94c77e2 Posts
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-14 22:11:13 +01:00
Julien Valverdé
4f091ae221 API work
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-14 21:29:38 +01:00
Julien Valverdé
671a80b6ff Post work
Some checks failed
Lint / lint (push) Failing after 8s
2025-01-14 17:09:04 +01:00
Julien Valverdé
249de93047 Fix
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-14 16:03:07 +01:00
Julien Valverdé
ae6bb410a3 Fix
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-14 15:53:42 +01:00
Julien Valverdé
2aaee4826b extend
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-14 15:53:20 +01:00
Julien Valverdé
f50adbf119 Done
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-14 15:40:02 +01:00
Julien Valverdé
cf0951039c Fix
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-14 15:29:09 +01:00
Julien Valverdé
4b6cf9a46e ReffuseReactContext provider split
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-14 15:16:51 +01:00
Julien Valverdé
12849d37da Work
Some checks failed
Lint / lint (push) Failing after 1m20s
2025-01-14 00:54:39 +01:00
Julien Valverdé
c60c396054 Inheritance work
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-13 23:38:46 +01:00
Julien Valverdé
1720266761 Fix
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-13 01:32:07 +01:00
Julien Valverdé
edec837a87 Fix
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-12 23:50:49 +01:00
Julien Valverdé
d8553e95e2 Working
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-12 23:41:32 +01:00
Julien Valverdé
f6dc7a0722 Reffuse work
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-12 23:36:28 +01:00
Julien Valverdé
ed85f9804c Reffuse work
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-12 23:17:28 +01:00
Julien Valverdé
cd2df017ec Working tests
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-12 19:55:31 +01:00
Julien Valverdé
79a3779005 Tests
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-12 19:14:01 +01:00
Julien Valverdé
b3ec1c4f49 Context
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-12 18:35:25 +01:00
Julien Valverdé
6aafadb4ad Fix
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-11 23:36:07 +01:00
Julien Valverdé
c4b902b110 Cleanup
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-11 21:36:48 +01:00
Julien Valverdé
7e239b0d1e useRefFromEffect
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-11 18:45:18 +01:00
Julien Valverdé
038f38d32c Time
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-11 18:26:43 +01:00
Julien Valverdé
e8580ec49e Time work
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-11 17:54:59 +01:00
Julien Valverdé
8ae59bdd93 Time fork test
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-11 17:51:36 +01:00
Julien Valverdé
e1a85fbb7e Teardown
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-11 16:55:48 +01:00
Julien Valverdé
4e0cec051f Fix
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-11 16:42:49 +01:00
Julien Valverdé
6373919fc4 Router setup
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-11 16:36:06 +01:00
Julien Valverdé
5f455295ad Tests
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-10 22:34:53 +01:00
Julien Valverdé
00bf5a3c63 Fix
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-10 22:21:54 +01:00
Julien Valverdé
9aa86f19f0 Fix
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-10 22:14:24 +01:00
Julien Valverdé
64c07a62e6 Fix
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-10 22:11:50 +01:00
Julien Valverdé
100169952c Example tests
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-10 22:00:14 +01:00
Julien Valverdé
8624a507b3 Example setup
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-10 21:06:32 +01:00
Julien Valverdé
fcd37a5910 Fix
Some checks failed
Lint / lint (push) Failing after 8s
2025-01-09 16:02:39 +01:00
Julien Valverdé
c91b538c97 Reffuse work
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-09 14:27:41 +01:00
Julien Valverdé
53232729c3 Reffuse work
Some checks failed
Lint / lint (push) Failing after 8s
2025-01-09 13:06:45 +01:00
Julien Valverdé
107d1ba359 Reffuse work
Some checks failed
Lint / lint (push) Failing after 8s
2025-01-09 11:51:14 +01:00
46 changed files with 1242 additions and 0 deletions

BIN
bun.lockb Executable file

Binary file not shown.

2
bunfig.toml Normal file
View File

@@ -0,0 +1,2 @@
[install.scopes]
"@thilawyn" = "https://git.valverde.cloud/api/packages/thilawyn/npm/"

View File

@@ -5,5 +5,8 @@
"clean:cache": "rm -f tsconfig.tsbuildinfo", "clean:cache": "rm -f tsconfig.tsbuildinfo",
"clean:dist": "rm -rf dist", "clean:dist": "rm -rf dist",
"clean:node": "rm -rf node_modules" "clean:node": "rm -rf node_modules"
},
"devDependencies": {
"npm-check-updates": "^17.1.13"
} }
} }

24
packages/example/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,50 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```

View File

@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,43 @@
{
"name": "example",
"version": "0.0.0",
"type": "module",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@tanstack/react-router": "^1.95.3",
"@tanstack/router-devtools": "^1.95.3",
"@tanstack/router-plugin": "^1.95.3",
"@thilawyn/thilaschema": "^0.1.4",
"@types/react": "^19.0.4",
"@types/react-dom": "^19.0.2",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"effect": "^3.12.1",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"postcss": "^8.4.49",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.2",
"vite": "^6.0.5"
},
"dependencies": {
"@effect/platform": "^0.73.1",
"@effect/platform-browser": "^0.52.1",
"@radix-ui/themes": "^3.1.6",
"@typed/id": "^0.17.1",
"lucide-react": "^0.471.1",
"mobx": "^6.13.5"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,8 @@
import { Schema } from "effect"
export class Post extends Schema.Class<Post>("Post")({
id: Schema.String,
title: Schema.String,
content: Schema.String,
}) {}

View File

@@ -0,0 +1,26 @@
import { ThSchema } from "@thilawyn/thilaschema"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Effect, Schema } from "effect"
export class Todo extends Schema.Class<Todo>("Todo")({
_tag: Schema.tag("Todo"),
id: Schema.String,
content: Schema.String,
completedAt: Schema.OptionFromSelf(Schema.DateTimeUtcFromSelf),
}) {}
export const TodoFromJsonStruct = Schema.Struct({
...Todo.fields,
completedAt: Schema.Option(Schema.DateTimeUtc),
}).pipe(
ThSchema.assertEncodedJsonifiable
)
export const TodoFromJson = TodoFromJsonStruct.pipe(Schema.compose(Todo))
export const generateUniqueID = makeUuid4.pipe(
Effect.provide(GetRandomValues.CryptoRandom)
)

View File

@@ -0,0 +1,2 @@
export * as Post from "./Post"
export * as Todo from "./Todo"

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,32 @@
import { createRouter, RouterProvider } from "@tanstack/react-router"
import { ReffuseRuntime } from "@thilawyn/reffuse"
import { Layer } from "effect"
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { GlobalContext } from "./reffuse"
import { routeTree } from "./routeTree.gen"
import { FetchData } from "./services"
const layer = Layer.empty.pipe(
Layer.provideMerge(FetchData.mockLayer)
)
const router = createRouter({ routeTree })
declare module "@tanstack/react-router" {
interface Register {
router: typeof router
}
}
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ReffuseRuntime.Provider>
<GlobalContext.Provider layer={layer}>
<RouterProvider router={router} />
</GlobalContext.Provider>
</ReffuseRuntime.Provider>
</StrictMode>
)

View File

@@ -0,0 +1,7 @@
import { ReffuseContext } from "@thilawyn/reffuse"
import { make } from "@thilawyn/reffuse/Reffuse"
import { FetchData } from "./services"
export const GlobalContext = ReffuseContext.make<FetchData.FetchData>()
export const Reffuse = make(GlobalContext)

View File

@@ -0,0 +1,134 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
// Import Routes
import { Route as rootRoute } from './routes/__root'
import { Route as TimeImport } from './routes/time'
import { Route as CountImport } from './routes/count'
import { Route as IndexImport } from './routes/index'
// Create/Update Routes
const TimeRoute = TimeImport.update({
id: '/time',
path: '/time',
getParentRoute: () => rootRoute,
} as any)
const CountRoute = CountImport.update({
id: '/count',
path: '/count',
getParentRoute: () => rootRoute,
} as any)
const IndexRoute = IndexImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/count': {
id: '/count'
path: '/count'
fullPath: '/count'
preLoaderRoute: typeof CountImport
parentRoute: typeof rootRoute
}
'/time': {
id: '/time'
path: '/time'
fullPath: '/time'
preLoaderRoute: typeof TimeImport
parentRoute: typeof rootRoute
}
}
}
// Create and export the route tree
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/count': typeof CountRoute
'/time': typeof TimeRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/count': typeof CountRoute
'/time': typeof TimeRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/count': typeof CountRoute
'/time': typeof TimeRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/count' | '/time'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/count' | '/time'
id: '__root__' | '/' | '/count' | '/time'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
CountRoute: typeof CountRoute
TimeRoute: typeof TimeRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
CountRoute: CountRoute,
TimeRoute: TimeRoute,
}
export const routeTree = rootRoute
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.tsx",
"children": [
"/",
"/count",
"/time"
]
},
"/": {
"filePath": "index.tsx"
},
"/count": {
"filePath": "count.tsx"
},
"/time": {
"filePath": "time.tsx"
}
}
}
ROUTE_MANIFEST_END */

View File

@@ -0,0 +1,27 @@
import { Container, Flex, Theme } from "@radix-ui/themes"
import "@radix-ui/themes/styles.css"
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
import "../index.css"
export const Route = createRootRoute({
component: Root
})
function Root() {
return (
<Theme>
<Container>
<Flex direction="row" justify="center" align="center" gap="2">
<Link to="/">Index</Link>
<Link to="/time">Time</Link>
<Link to="/count">Count</Link>
</Flex>
</Container>
<Outlet />
<TanStackRouterDevtools />
</Theme>
)
}

View File

@@ -0,0 +1,27 @@
import { Reffuse } from "@/reffuse"
import { createFileRoute } from "@tanstack/react-router"
import { Ref } from "effect"
export const Route = createFileRoute("/count")({
component: Count
})
function Count() {
const runSync = Reffuse.useRunSync()
const countRef = Reffuse.useRef(0)
const [count] = Reffuse.useRefState(countRef)
return (
<div className="container mx-auto">
{/* <button onClick={() => setCount((count) => count + 1)}> */}
<button onClick={() => Ref.update(countRef, count => count + 1).pipe(runSync)}>
count is {count}
</button>
</div>
)
}

View File

@@ -0,0 +1,34 @@
import { Reffuse } from "@/reffuse"
import { TodosContext } from "@/todos/reffuse"
import { TodosState } from "@/todos/services"
import { VTodos } from "@/todos/views/VTodos"
import { Container } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Layer } from "effect"
import { useMemo } from "react"
export const Route = createFileRoute("/")({
component: Index
})
function Index() {
const todosLayer = useMemo(() => Layer.empty.pipe(
Layer.provideMerge(TodosState.make("todos"))
), [])
Reffuse.useEffect(Effect.addFinalizer(() => Console.log("Effect cleanup")).pipe(
Effect.flatMap(() => Console.log("Effect recalculated"))
))
return (
<Container>
<TodosContext.Provider layer={todosLayer}>
<VTodos />
</TodosContext.Provider>
</Container>
)
}

View File

@@ -0,0 +1,49 @@
import { Reffuse } from "@/reffuse"
import { createFileRoute } from "@tanstack/react-router"
import { Console, DateTime, Effect, Ref, Schedule, Stream } from "effect"
const timeEverySecond = Stream.repeatEffectWithSchedule(
DateTime.now,
Schedule.intersect(Schedule.forever, Schedule.spaced("1 second")),
)
export const Route = createFileRoute("/time")({
component: Time
})
function Time() {
const timeRef = Reffuse.useRefFromEffect(DateTime.now)
Reffuse.useFork(Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
Effect.flatMap(() =>
Stream.runForEach(timeEverySecond, v => Ref.set(timeRef, v))
)
), [timeRef])
// Reffuse.useFork(Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
// Effect.flatMap(() => DateTime.now),
// Effect.flatMap(v => Ref.set(timeRef, v)),
// Effect.repeat(Schedule.intersect(
// Schedule.forever,
// Schedule.spaced("1 second"),
// )),
// ), [timeRef])
const [time] = Reffuse.useRefState(timeRef)
return (
<div className="container mx-auto">
<p className="text-center">
{DateTime.format(time, {
hour: "numeric",
minute: "numeric",
second: "numeric",
})}
</p>
</div>
)
}

View File

@@ -0,0 +1,24 @@
import { Post } from "@/domain"
import { Chunk, Context, Effect, Layer } from "effect"
export class FetchData extends Context.Tag("FetchData")<FetchData, {
readonly fetchPosts: Effect.Effect<Chunk.Chunk<Post.Post>>
}>() {}
export const mockLayer = Layer.succeed(FetchData, {
fetchPosts: Effect.succeed(Chunk.make(
Post.Post.make({
id: "1",
title: "Lorem ipsum dolor sit amet",
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eget lacus sit amet diam suscipit porttitor non at felis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla risus ligula, elementum nec scelerisque eget, volutpat vel sapien. Phasellus aliquam ac neque vitae sodales. Nunc sodales congue odio. Nulla eget nisl cursus, convallis lorem at, varius lectus. Aliquam vitae mauris vel mi dignissim condimentum. Proin sed dignissim sapien, ut cursus ex. Donec eget sapien sagittis, auctor metus vitae, fringilla lacus. Donec ut elit a quam aliquet consectetur interdum eu nisl. Etiam nec convallis purus, eu venenatis nulla. Phasellus non metus id mauris tincidunt cursus. Cras varius aliquet diam eu blandit. In hac habitasse platea dictumst.",
}),
Post.Post.make({
id: "2",
title: "Vestibulum non bibendum ligula",
content: "Vestibulum non bibendum ligula. Integer pellentesque, diam ac faucibus volutpat, nulla libero porttitor nunc, ac pulvinar tortor diam id ipsum. Sed id enim at odio euismod imperdiet et ac purus. Etiam tempus ipsum semper scelerisque mollis. Integer auctor, magna et tristique tempus, nisi mi euismod est, nec finibus quam nunc nec libero. Maecenas aliquet viverra magna, vitae blandit ligula pharetra id. Vestibulum vel lacus at nibh placerat tincidunt. Sed suscipit tellus vel felis euismod, et sollicitudin neque cursus. Curabitur dapibus eros vitae ligula suscipit, at facilisis risus venenatis. Sed pharetra blandit pulvinar. Vivamus vestibulum at ligula pulvinar fringilla. Suspendisse vel mattis libero, eget vulputate massa. Vivamus vehicula, lectus id tempor maximus, erat tortor blandit purus, at scelerisque nunc urna faucibus sapien.",
}),
))
})

View File

@@ -0,0 +1 @@
export * as FetchData from "./FetchData"

View File

@@ -0,0 +1,8 @@
import { GlobalContext } from "@/reffuse"
import { ReffuseContext } from "@thilawyn/reffuse"
import { make } from "@thilawyn/reffuse/Reffuse"
import { TodosState } from "./services"
export const TodosContext = ReffuseContext.make<TodosState.TodosState>()
export const Reffuse = make(GlobalContext, TodosContext)

View File

@@ -0,0 +1,69 @@
import { Todo } from "@/domain"
import { KeyValueStore } from "@effect/platform"
import { BrowserKeyValueStore } from "@effect/platform-browser"
import { PlatformError } from "@effect/platform/Error"
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, SubscriptionRef } from "effect"
export class TodosState extends Context.Tag("TodosState")<TodosState, {
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
readonly readFromLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly saveToLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly prepend: (todo: Todo.Todo) => Effect.Effect<void>
readonly replace: (index: number, todo: Todo.Todo) => Effect.Effect<void>
readonly remove: (index: number) => Effect.Effect<void>
// readonly moveUp: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
// readonly moveDown: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
}>() {}
export const make = (key: string) => Layer.effect(TodosState, Effect.gen(function*() {
const todos = yield* SubscriptionRef.make(Chunk.empty<Todo.Todo>())
const readFromLocalStorage = KeyValueStore.KeyValueStore.pipe(
Effect.flatMap(kv => kv.get(key)),
Effect.flatMap(identity),
Effect.flatMap(Schema.parseJson().pipe(
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
Schema.decode,
)),
Effect.flatMap(v => Ref.set(todos, v)),
Effect.catchTag("NoSuchElementException", () => Ref.set(todos, Chunk.empty())),
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
)
const saveToLocalStorage = Effect.all([KeyValueStore.KeyValueStore, todos]).pipe(
Effect.flatMap(([kv, values]) => values.pipe(
Schema.parseJson().pipe(
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
Schema.encode,
),
Effect.flatMap(v => kv.set(key, v)),
)),
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
)
const prepend = (todo: Todo.Todo) => Ref.update(todos, Chunk.prepend(todo))
const replace = (index: number, todo: Todo.Todo) => Ref.update(todos, Chunk.replace(index, todo))
const remove = (index: number) => Ref.update(todos, Chunk.remove(index))
// const moveUp = (index: number) => Effect.gen(function*() {
// })
yield* readFromLocalStorage
return {
todos,
readFromLocalStorage,
saveToLocalStorage,
prepend,
replace,
remove,
}
}))

View File

@@ -0,0 +1 @@
export * as TodosState from "./TodosState"

View File

@@ -0,0 +1,52 @@
import { Todo } from "@/domain"
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
import { Effect, Option } from "effect"
import { Reffuse } from "../reffuse"
import { TodosState } from "../services"
export function VNewTodo() {
const runSync = Reffuse.useRunSync()
const createEmptyTodo = Todo.generateUniqueID.pipe(
Effect.map(id => Todo.Todo.make({
id,
content: "",
completedAt: Option.none(),
}, true))
)
const todoRef = Reffuse.useRefFromEffect(createEmptyTodo)
const [todo, setTodo] = Reffuse.useRefState(todoRef)
return (
<Box>
<Card>
<Flex direction="column" align="stretch" gap="2">
<TextArea
value={todo.content}
onChange={e => setTodo(
Todo.Todo.make({ ...todo, content: e.target.value }, true)
)}
/>
<Flex direction="row" justify="center" align="center">
<Button
onClick={() => TodosState.TodosState.pipe(
Effect.flatMap(state => state.prepend(todo)),
Effect.flatMap(() => createEmptyTodo),
Effect.map(setTodo),
runSync,
)}
>
Add
</Button>
</Flex>
</Flex>
</Card>
</Box>
)
}

View File

@@ -0,0 +1,56 @@
import { Todo } from "@/domain"
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
import { Effect } from "effect"
import { Delete } from "lucide-react"
import { useState } from "react"
import { Reffuse } from "../reffuse"
import { TodosState } from "../services"
export interface VTodoProps {
readonly index: number
readonly todo: Todo.Todo
}
export function VTodo({ index, todo }: VTodoProps) {
const runSync = Reffuse.useRunSync()
const editorMode = useState(false)
return (
<Box>
<Card>
<Flex direction="column" align="stretch" gap="1">
<TextArea
value={todo.content}
onChange={e => TodosState.TodosState.pipe(
Effect.flatMap(state => state.replace(
index,
Todo.Todo.make({ ...todo, content: e.target.value }, true),
)),
runSync,
)}
disabled={!editorMode}
/>
<Flex direction="row" justify="between" align="center">
<Box></Box>
<Flex direction="row" align="center" gap="1">
<IconButton
onClick={() => TodosState.TodosState.pipe(
Effect.flatMap(state => state.remove(index)),
runSync,
)}
>
<Delete />
</IconButton>
</Flex>
</Flex>
</Flex>
</Card>
</Box>
)
}

View File

@@ -0,0 +1,36 @@
import { Box, Flex } from "@radix-ui/themes"
import { Chunk, Effect, Stream } from "effect"
import { Reffuse } from "../reffuse"
import { TodosState } from "../services"
import { VNewTodo } from "./VNewTodo"
import { VTodo } from "./VTodo"
export function VTodos() {
// Sync changes to the todos with the local storage
Reffuse.useFork(TodosState.TodosState.pipe(
Effect.flatMap(state =>
Stream.runForEach(state.todos.changes, () => state.saveToLocalStorage)
)
))
const todosRef = Reffuse.useMemo(TodosState.TodosState.pipe(Effect.map(state => state.todos)))
const [todos] = Reffuse.useRefState(todosRef)
return (
<Flex direction="column" align="center" gap="3">
<Box width="500px">
<VNewTodo />
</Box>
{Chunk.map(todos, (todo, index) => (
<Box key={todo.id} width="500px">
<VTodo index={index} todo={todo} />
</Box>
))}
</Flex>
)
}

View File

@@ -0,0 +1,34 @@
import { Post } from "@/domain"
import { Effect } from "effect"
import { PostsState } from "../posts/services"
import { Reffuse } from "./reffuse"
export interface VPostProps {
readonly index: number
readonly post: Post.Post
}
export function VPost({ post, index }: VPostProps) {
const runSync = Reffuse.useRunSync()
return (
<div className="flex-col gap-1 items-stretch">
<p>{post.title}</p>
<p>{post.content}</p>
<button
onClick={() => PostsState.PostsState.pipe(
Effect.flatMap(state => state.remove(index)),
runSync,
)}
>
X
</button>
</div>
)
}

View File

@@ -0,0 +1 @@
export { Reffuse } from "../posts/reffuse"

View File

@@ -0,0 +1,25 @@
import { Chunk } from "effect"
import { VPost } from "../post/VPost"
import { Reffuse } from "./reffuse"
import { PostsState } from "./services"
export function VPosts() {
const state = Reffuse.useMemo(PostsState.PostsState)
const [posts] = Reffuse.useRefState(state.posts)
return (
<div className="flex-col gap-2 items-stretch">
{Chunk.map(posts, (post, index) => (
<VPost
key={`${ index }-${ post.id }`}
index={index}
post={post}
/>
))}
</div>
)
}

View File

@@ -0,0 +1,8 @@
import { GlobalContext } from "@/reffuse"
import { ReffuseContext } from "@thilawyn/reffuse"
import { make } from "@thilawyn/reffuse/Reffuse"
import { PostsState } from "./services"
export const PostsContext = ReffuseContext.make<PostsState.PostsState>()
export const Reffuse = make(GlobalContext, PostsContext)

View File

@@ -0,0 +1,15 @@
import { Post } from "@/domain"
import { Chunk, Context, Effect, Layer, Ref, SubscriptionRef } from "effect"
export class PostsState extends Context.Tag("PostsState")<PostsState, {
readonly posts: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Post.Post>>
readonly remove: (index: number) => Effect.Effect<void>
}>() {}
export const make = (posts: Chunk.Chunk<Post.Post>) => Layer.effect(PostsState, SubscriptionRef.make(posts).pipe(
Effect.map(posts => ({
posts,
remove: (index: number) => Ref.update(posts, Chunk.remove(index)),
}))
))

View File

@@ -0,0 +1 @@
export * as PostsState from "./PostsState"

1
packages/example/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,11 @@
/** @type {import("tailwindcss").Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,19 @@
import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
import react from "@vitejs/plugin-react"
import path from "node:path"
import { defineConfig } from "vite"
// https://vite.dev/config/
export default defineConfig({
plugins: [
TanStackRouterVite(),
react(),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})

View File

@@ -24,5 +24,9 @@
"clean:node": "rm -rf node_modules" "clean:node": "rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.0.4",
"effect": "^3.12.1",
"react": "^19.0.0",
"typescript": "^5.7.3"
} }
} }

View File

@@ -0,0 +1,205 @@
import { Context, Effect, ExecutionStrategy, Exit, Fiber, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import React from "react"
import * as ReffuseContext from "./ReffuseContext.js"
import * as ReffuseRuntime from "./ReffuseRuntime.js"
export class Reffuse<R> {
constructor(
readonly contexts: readonly ReffuseContext.ReffuseContext<R>[]
) {}
useContext(): Context.Context<R> {
return ReffuseContext.useMergeAll(...this.contexts)
}
useRunSync() {
const runtime = ReffuseRuntime.useRuntime()
const context = this.useContext()
return React.useCallback(<A, E>(
effect: Effect.Effect<A, E, R>
): A => effect.pipe(
Effect.provide(context),
Runtime.runSync(runtime),
), [runtime, context])
}
useRunPromise() {
const runtime = ReffuseRuntime.useRuntime()
const context = this.useContext()
return React.useCallback(<A, E>(
effect: Effect.Effect<A, E, R>,
options?: { readonly signal?: AbortSignal },
): Promise<A> => effect.pipe(
Effect.provide(context),
effect => Runtime.runPromise(runtime)(effect, options),
), [runtime, context])
}
useRunFork() {
const runtime = ReffuseRuntime.useRuntime()
const context = this.useContext()
return React.useCallback(<A, E>(
effect: Effect.Effect<A, E, R>,
options?: Runtime.RunForkOptions,
): Fiber.RuntimeFiber<A, E> => effect.pipe(
Effect.provide(context),
effect => Runtime.runFork(runtime)(effect, options),
), [runtime, context])
}
useMemo<A, E>(
effect: Effect.Effect<A, E, R>,
deps?: React.DependencyList,
options?: RenderOptions,
): A {
const runSync = this.useRunSync()
return React.useMemo(() => runSync(effect), [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...(deps ?? []),
])
}
useEffect<A, E>(
effect: Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: RenderOptions & ScopeOptions,
): void {
const runSync = this.useRunSync()
return React.useEffect(() => {
const scope = Scope.make(options?.finalizerExecutionStrategy).pipe(
Effect.tap(scope => Effect.provideService(effect, Scope.Scope, scope)),
runSync,
)
return () => { runSync(Scope.close(scope, Exit.void)) }
}, [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...(deps ?? []),
])
}
useLayoutEffect<A, E>(
effect: Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: RenderOptions & ScopeOptions,
): void {
const runSync = this.useRunSync()
return React.useLayoutEffect(() => {
const scope = Scope.make(options?.finalizerExecutionStrategy).pipe(
Effect.tap(scope => Effect.provideService(effect, Scope.Scope, scope)),
runSync,
)
return () => { runSync(Scope.close(scope, Exit.void)) }
}, [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...(deps ?? []),
])
}
useSuspense<A, E>(
effect: Effect.Effect<A, E, R>,
deps?: React.DependencyList,
options?: { readonly signal?: AbortSignal } & RenderOptions,
): A {
const runPromise = this.useRunPromise()
const promise = React.useMemo(() => runPromise(effect, options), [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runPromise],
...(deps ?? []),
])
return React.use(promise)
}
useFork<A, E>(
effect: Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: Runtime.RunForkOptions & RenderOptions & ScopeOptions,
): void {
const runSync = this.useRunSync()
const runFork = this.useRunFork()
return React.useEffect(() => {
const scope = runSync(Scope.make(options?.finalizerExecutionStrategy))
const fiber = runFork(Effect.provideService(effect, Scope.Scope, scope), options)
return () => {
Fiber.interrupt(fiber).pipe(
Effect.flatMap(() => Scope.close(scope, Exit.void)),
runFork,
)
}
}, [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runFork],
...(deps ?? []),
])
}
useRef<A>(value: A): SubscriptionRef.SubscriptionRef<A> {
return this.useMemo(
SubscriptionRef.make(value),
[],
{ doNotReExecuteOnRuntimeOrContextChange: false }, // Do not recreate the ref when the context changes
)
}
useRefFromEffect<A, E>(effect: Effect.Effect<A, E, R>): SubscriptionRef.SubscriptionRef<A> {
return this.useMemo(
effect.pipe(Effect.flatMap(SubscriptionRef.make)),
[],
{ doNotReExecuteOnRuntimeOrContextChange: false }, // Do not recreate the ref when the context changes
)
}
useRefState<A>(ref: SubscriptionRef.SubscriptionRef<A>): [A, React.Dispatch<React.SetStateAction<A>>] {
const runSync = this.useRunSync()
const initialState = React.useMemo(() => runSync(ref), [ref])
const [reactStateValue, setReactStateValue] = React.useState(initialState)
this.useFork(Stream.runForEach(ref.changes, v => Effect.sync(() =>
setReactStateValue(v)
)), [ref])
const setValue = React.useCallback((setStateAction: React.SetStateAction<A>) =>
runSync(Ref.update(ref, previousState =>
typeof setStateAction === "function"
? (setStateAction as (prevState: A) => A)(previousState)
: setStateAction
)),
[ref])
return [reactStateValue, setValue]
}
}
export interface RenderOptions {
/** Prevents re-executing the effect when the Effect runtime or context changes. Defaults to `false`. */
readonly doNotReExecuteOnRuntimeOrContextChange?: boolean
}
export interface ScopeOptions {
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
}
export const make = <
const Contexts extends readonly ReffuseContext.ReffuseContext<any>[]
>(
...contexts: Contexts
): Reffuse<{ [K in keyof Contexts]: ReffuseContext.R<Contexts[K]> }[number]> =>
new Reffuse(contexts)

View File

@@ -0,0 +1,73 @@
import { Array, Context, Effect, Layer, Runtime } from "effect"
import React from "react"
import * as ReffuseRuntime from "./ReffuseRuntime.js"
export class ReffuseContext<R> {
readonly Context = React.createContext<Context.Context<R>>(null!)
readonly Provider: ReffuseContextReactProvider<R>
constructor() {
this.Provider = (props) => {
const runtime = ReffuseRuntime.useRuntime()
const value = React.useMemo(() => Effect.context<R>().pipe(
Effect.provide(props.layer),
Runtime.runSync(runtime),
), [runtime, props.layer])
return (
<this.Context
{...props}
value={value}
/>
)
}
this.Provider.displayName = "ReffuseContextReactProvider"
}
useContext(): Context.Context<R> {
return React.useContext(this.Context)
}
useLayer(): Layer.Layer<R> {
const context = this.useContext()
return React.useMemo(() => Layer.effectContext(Effect.succeed(context)), [context])
}
}
export type ReffuseContextReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown>
readonly children?: React.ReactNode
}>
export type R<T> = T extends ReffuseContext<infer R> ? R : never
export function make<R = never>() {
return new ReffuseContext<R>()
}
export function useMergeAll<
const Contexts extends readonly ReffuseContext<any>[]
>(
...contexts: Contexts
): Context.Context<{ [K in keyof Contexts]: R<Contexts[K]> }[number]> {
const values = contexts.map(v => React.use(v.Context))
return React.useMemo(() => Context.mergeAll(...values), values)
}
export function useMergeAllLayers<
const Contexts extends Array.NonEmptyReadonlyArray<ReffuseContext<any>>
>(
...contexts: Contexts
): Layer.Layer<{ [K in keyof Contexts]: R<Contexts[K]> }[number]> {
const values = Array.map(contexts, v => React.use(v.Context))
return React.useMemo(() => Layer.mergeAll(
...Array.map(values, context => Layer.effectContext(Effect.succeed(context)))
) as Layer.Layer<any>, values)
}

View File

@@ -0,0 +1,15 @@
import { Runtime } from "effect"
import React from "react"
export const Context = React.createContext<Runtime.Runtime<never>>(null!)
export const Provider = (props: { readonly children?: React.ReactNode }) => (
<Context
{...props}
value={Runtime.defaultRuntime}
/>
)
Provider.displayName = "ReffuseRuntimeReactProvider"
export const useRuntime = () => React.useContext(Context)

View File

@@ -0,0 +1,3 @@
export * as Reffuse from "./Reffuse.js"
export * as ReffuseContext from "./ReffuseContext.js"
export * as ReffuseRuntime from "./ReffuseRuntime.js"