20 Commits

Author SHA1 Message Date
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
26 changed files with 613 additions and 0 deletions

BIN
bun.lockb Executable file

Binary file not shown.

View File

@@ -5,5 +5,8 @@
"clean:cache": "rm -f tsconfig.tsbuildinfo",
"clean:dist": "rm -rf dist",
"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,34 @@
{
"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",
"@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"
}
}

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,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,20 @@
import { createRouter, RouterProvider } from "@tanstack/react-router"
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import "./index.css"
import { routeTree } from './routeTree.gen'
const router = createRouter({ routeTree })
declare module "@tanstack/react-router" {
interface Register {
router: typeof router
}
}
createRoot(document.getElementById("root")!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
)

View File

@@ -0,0 +1,5 @@
import { make } from "@thilawyn/reffuse/Reffuse"
import { Layer } from "effect"
export const Reffuse = make(Layer.empty)

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,23 @@
import { Reffuse } from "@/reffuse"
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
export const Route = createRootRoute({
component: Root
})
function Root() {
return (
<Reffuse.Provider>
<div className="container mx-auto flex-row justify-center items-center gap-2 mb-4">
<Link to="/">Index</Link>
<Link to="/time">Time</Link>
<Link to="/count">Count</Link>
</div>
<Outlet />
<TanStackRouterDevtools />
</Reffuse.Provider>
)
}

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 runtime = Reffuse.useRuntime()
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(runtime.runSync)}>
count is {count}
</button>
</div>
)
}

View File

@@ -0,0 +1,10 @@
import { createFileRoute } from "@tanstack/react-router"
export const Route = createFileRoute("/")({
component: Index
})
function Index() {
return <></>
}

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>
)
}

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"
},
"devDependencies": {
"@types/react": "^19.0.4",
"effect": "^3.12.1",
"react": "^19.0.0",
"typescript": "^5.7.3"
}
}

View File

@@ -0,0 +1,86 @@
import { Effect, Fiber, Layer, ManagedRuntime, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import React from "react"
export class Reffuse<R, ER> {
constructor(
runtime: ManagedRuntime.ManagedRuntime<R, ER>
) {
this.Context = React.createContext<ManagedRuntime.ManagedRuntime<R, ER>>(null!)
this.Provider = (props: { readonly children?: React.ReactNode }) => (
<this.Context
{...props}
value={runtime}
/>
)
const context = runtime.runtimeEffect.pipe(
Effect.map(r => Layer.succeedContext(r.context)),
runtime.runSync,
)
}
readonly Context: React.Context<ManagedRuntime.ManagedRuntime<R, ER>>
readonly Provider: React.FC<{ readonly children?: React.ReactNode }>
useRuntime(): ManagedRuntime.ManagedRuntime<R, ER> {
return React.useContext(this.Context)
}
useFork<A, E>(
self: Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: Runtime.RunForkOptions,
): void {
const runtime = this.useRuntime()
return React.useEffect(() => {
const fiber = runtime.runFork(Effect.scoped(self), options)
return () => { runtime.runFork(Fiber.interrupt(fiber)) }
}, [runtime, ...deps ?? []])
}
useRef<A>(value: A): SubscriptionRef.SubscriptionRef<A> {
const runtime = this.useRuntime()
return React.useMemo(() => runtime.runSync(SubscriptionRef.make(value)), [])
}
useRefFromEffect<A, E>(effect: Effect.Effect<A, E, R>): SubscriptionRef.SubscriptionRef<A> {
const runtime = this.useRuntime()
return React.useMemo(() => runtime.runSync(effect.pipe(
Effect.flatMap(SubscriptionRef.make)
)), [])
}
useRefState<A>(ref: SubscriptionRef.SubscriptionRef<A>): [A, React.Dispatch<React.SetStateAction<A>>] {
const runtime = this.useRuntime()
const initialState = React.useMemo(() => runtime.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>) =>
runtime.runSync(Ref.update(ref, previousState =>
typeof setStateAction === "function"
? (setStateAction as (prevState: A) => A)(previousState)
: setStateAction
)),
[ref])
return [reactStateValue, setValue]
}
}
export const make = <ROut, E>(layer: Layer.Layer<ROut, E, never>): Reffuse<ROut, E> =>
new Reffuse(ManagedRuntime.make(layer))

View File

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