├── src ├── server │ └── main.server.ts ├── shared │ └── constants.ts └── client │ ├── dev.ts │ ├── components │ ├── app.tsx │ ├── layer.tsx │ ├── counter.tsx │ └── button.tsx │ ├── stories │ └── app.story.tsx │ ├── utils │ ├── fonts.ts │ ├── springs.ts │ ├── palette.ts │ └── color-utils.ts │ ├── main.client.tsx │ └── hooks │ ├── use-motion.ts │ └── use-px.ts ├── .npmrc ├── .gitignore ├── .prettierrc ├── aftman.toml ├── README.md ├── .eslintrc ├── tsconfig.json ├── default.project.json ├── package.json ├── LICENSE.md └── pnpm-lock.yaml /src/server/main.server.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | node-linker=hoisted 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /out 3 | /include 4 | *.tsbuildinfo 5 | yarn-error.log 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "trailingComma": "all", 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/constants.ts: -------------------------------------------------------------------------------- 1 | import { RunService } from "@rbxts/services"; 2 | 3 | export const IS_PLUGIN = RunService.IsStudio() && !RunService.IsRunning(); 4 | -------------------------------------------------------------------------------- /src/client/dev.ts: -------------------------------------------------------------------------------- 1 | import { RunService } from "@rbxts/services"; 2 | 3 | declare const _G: Record; 4 | 5 | if (RunService.IsStudio()) { 6 | _G.__DEV__ = true; 7 | } 8 | -------------------------------------------------------------------------------- /src/client/components/app.tsx: -------------------------------------------------------------------------------- 1 | import React from "@rbxts/react"; 2 | 3 | import { Counter } from "./counter"; 4 | import { Layer } from "./layer"; 5 | 6 | export function App() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/client/stories/app.story.tsx: -------------------------------------------------------------------------------- 1 | import "../dev"; 2 | 3 | import { hoarcekat } from "@rbxts/pretty-react-hooks"; 4 | import React from "@rbxts/react"; 5 | import { App } from "client/components/app"; 6 | 7 | export = hoarcekat(() => { 8 | return ; 9 | }); 10 | -------------------------------------------------------------------------------- /src/client/utils/fonts.ts: -------------------------------------------------------------------------------- 1 | export const fonts = { 2 | inter: { 3 | regular: new Font("rbxassetid://12187365364"), 4 | medium: new Font("rbxassetid://12187365364", Enum.FontWeight.Medium), 5 | bold: new Font("rbxassetid://12187365364", Enum.FontWeight.Bold), 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /aftman.toml: -------------------------------------------------------------------------------- 1 | # This file lists tools managed by Aftman, a cross-platform toolchain manager. 2 | # For more information, see https://github.com/LPGhatguy/aftman 3 | 4 | # To add a new tool, add an entry to this table. 5 | [tools] 6 | rojo = "rojo-rbx/rojo@7.4.0" 7 | # rojo = "rojo-rbx/rojo@6.2.0" -------------------------------------------------------------------------------- /src/client/utils/springs.ts: -------------------------------------------------------------------------------- 1 | import { config, SpringOptions } from "@rbxts/ripple"; 2 | 3 | export const springs = { 4 | ...config.spring, 5 | bubbly: { tension: 300, friction: 20, mass: 1.2 }, 6 | responsive: { tension: 600, friction: 34, mass: 0.7 }, 7 | } satisfies { [config: string]: SpringOptions }; 8 | -------------------------------------------------------------------------------- /src/client/utils/palette.ts: -------------------------------------------------------------------------------- 1 | export const palette = { 2 | white: Color3.fromRGB(255, 255, 255), 3 | black: Color3.fromRGB(0, 0, 0), 4 | blue: Color3.fromRGB(56, 67, 214), 5 | purple: Color3.fromRGB(122, 57, 202), 6 | red: Color3.fromRGB(218, 56, 84), 7 | yellow: Color3.fromRGB(243, 163, 88), 8 | green: Color3.fromRGB(0, 174, 126), 9 | }; 10 | -------------------------------------------------------------------------------- /src/client/main.client.tsx: -------------------------------------------------------------------------------- 1 | import "./dev"; 2 | 3 | import React, { StrictMode } from "@rbxts/react"; 4 | import { createPortal, createRoot } from "@rbxts/react-roblox"; 5 | import { Players } from "@rbxts/services"; 6 | 7 | import { App } from "./components/app"; 8 | 9 | const root = createRoot(new Instance("Folder")); 10 | const target = Players.LocalPlayer.WaitForChild("PlayerGui"); 11 | 12 | root.render({createPortal(, target)}); 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ⚛️ Roblox React Example 2 | 3 | A simple counter app built with **roblox-ts + React**. [Check out the package here.](https://github.com/littensy/rbxts-react) 4 | 5 | https://github.com/littensy/rbxts-react-example/assets/56808540/7fba0c43-1efe-4622-b67b-7a40f3035982 6 | 7 | ### 🔥 Demo 8 | 9 | https://www.roblox.com/games/14747634789/ 10 | 11 | ### 📖 Pre-requisites 12 | 13 | - Install the [roblox-ts Extension](https://marketplace.visualstudio.com/items?itemName=Roblox-TS.vscode-roblox-ts) 14 | - Install the [Rojo Extension](https://marketplace.visualstudio.com/items?itemName=evaera.vscode-rojo) 15 | -------------------------------------------------------------------------------- /src/client/components/layer.tsx: -------------------------------------------------------------------------------- 1 | import React from "@rbxts/react"; 2 | import { IS_PLUGIN } from "shared/constants"; 3 | 4 | interface LayerProps { 5 | displayOrder?: number; 6 | children?: React.ReactNode; 7 | } 8 | 9 | export function Layer({ displayOrder, children }: LayerProps) { 10 | return IS_PLUGIN ? ( 11 | 17 | {children} 18 | 19 | ) : ( 20 | 26 | {children} 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "jsx": true, 5 | "useJSXTextNode": true, 6 | "ecmaVersion": 2018, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "ignorePatterns": ["/out"], 11 | "plugins": ["@typescript-eslint", "roblox-ts", "prettier", "unused-imports", "simple-import-sort"], 12 | "extends": [ 13 | "eslint:recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "plugin:roblox-ts/recommended", 16 | "plugin:prettier/recommended" 17 | ], 18 | "rules": { 19 | "prettier/prettier": "warn", 20 | "unused-imports/no-unused-imports": "warn", 21 | "simple-import-sort/imports": "warn", 22 | "simple-import-sort/exports": "warn" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // required 4 | "allowSyntheticDefaultImports": true, 5 | "downlevelIteration": true, 6 | "jsx": "react", 7 | "jsxFactory": "React.createElement", 8 | "jsxFragmentFactory": "React.Fragment", 9 | "module": "commonjs", 10 | "moduleResolution": "Node", 11 | "noLib": true, 12 | "resolveJsonModule": true, 13 | "experimentalDecorators": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "moduleDetection": "force", 16 | "strict": true, 17 | "target": "ESNext", 18 | "typeRoots": ["node_modules/@rbxts"], 19 | 20 | // configurable 21 | "rootDir": "src", 22 | "outDir": "out", 23 | "baseUrl": "src", 24 | "incremental": true, 25 | "tsBuildInfoFile": "out/tsconfig.tsbuildinfo" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roblox-ts-game", 3 | "globIgnorePaths": ["**/package.json", "**/tsconfig.json"], 4 | "tree": { 5 | "$className": "DataModel", 6 | "ServerScriptService": { 7 | "$className": "ServerScriptService", 8 | "TS": { 9 | "$path": "out/server" 10 | } 11 | }, 12 | "ReplicatedStorage": { 13 | "$className": "ReplicatedStorage", 14 | "rbxts_include": { 15 | "$path": "include", 16 | "node_modules": { 17 | "$className": "Folder", 18 | "@rbxts": { 19 | "$path": "node_modules/@rbxts" 20 | } 21 | } 22 | }, 23 | "TS": { 24 | "$path": "out/shared" 25 | } 26 | }, 27 | "StarterPlayer": { 28 | "$className": "StarterPlayer", 29 | "StarterPlayerScripts": { 30 | "$className": "StarterPlayerScripts", 31 | "TS": { 32 | "$path": "out/client" 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/client/hooks/use-motion.ts: -------------------------------------------------------------------------------- 1 | import { useEventListener } from "@rbxts/pretty-react-hooks"; 2 | import { createMotion, Motion, MotionGoal } from "@rbxts/ripple"; 3 | import { Binding, useBinding, useMemo } from "@rbxts/react"; 4 | import { RunService } from "@rbxts/services"; 5 | 6 | export function useMotion(initialValue: number): LuaTuple<[Binding, Motion]>; 7 | 8 | export function useMotion(initialValue: T): LuaTuple<[Binding, Motion]>; 9 | 10 | export function useMotion(initialValue: T) { 11 | const motion = useMemo(() => { 12 | return createMotion(initialValue); 13 | }, []); 14 | 15 | const [binding, setValue] = useBinding(initialValue); 16 | 17 | useEventListener(RunService.Heartbeat, (delta) => { 18 | const value = motion.step(delta); 19 | 20 | if (value !== binding.getValue()) { 21 | setValue(value); 22 | } 23 | }); 24 | 25 | return $tuple(binding, motion); 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rbxts-react-example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "rbxtsc", 8 | "watch": "rbxtsc -w" 9 | }, 10 | "devDependencies": { 11 | "@rbxts/compiler-types": "2.2.0-types.0", 12 | "@rbxts/types": "^1.0.754", 13 | "@typescript-eslint/eslint-plugin": "^6.21.0", 14 | "@typescript-eslint/parser": "^6.21.0", 15 | "eslint": "^8.56.0", 16 | "eslint-config-prettier": "^9.1.0", 17 | "eslint-plugin-prettier": "^5.1.3", 18 | "eslint-plugin-roblox-ts": "^0.0.36", 19 | "eslint-plugin-simple-import-sort": "^10.0.0", 20 | "eslint-plugin-unused-imports": "^3.1.0", 21 | "prettier": "^3.2.5", 22 | "roblox-ts": "2.3.0-dev-eb0ff9c", 23 | "typescript": "^5.3.3" 24 | }, 25 | "dependencies": { 26 | "@rbxts/pretty-react-hooks": "^0.4.1", 27 | "@rbxts/react": "^0.1.0", 28 | "@rbxts/react-roblox": "^0.3.0", 29 | "@rbxts/ripple": "^0.7.1", 30 | "@rbxts/services": "^1.5.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/client/components/counter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "@rbxts/react"; 2 | import { usePx } from "client/hooks/use-px"; 3 | import { fonts } from "client/utils/fonts"; 4 | import { palette } from "client/utils/palette"; 5 | 6 | import { Button } from "./button"; 7 | 8 | const COLORS = [palette.purple, palette.blue, palette.green, palette.yellow, palette.red]; 9 | 10 | export function Counter() { 11 | const px = usePx(); 12 | const [count, setCount] = useState(0); 13 | const [colorIndex, setColorIndex] = useState(0); 14 | 15 | return ( 16 |