React Three Fiber (R3F) and Poimandres ecosystem best practices. Use when writing, reviewing, or optimizing R3F code. Triggers on tasks involving @react-three/fiber, @react-three/drei, zustand, @react-three/postprocessing, @react-three/rapier, or leva.
58
66%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./skills/r3f-best-practices/SKILL.mdComprehensive guide for React Three Fiber and the Poimandres ecosystem. Contains 70+ rules across 12 categories, prioritized by impact.
Additional tips from 100 Three.js Tips by Utsubo
Reference these guidelines when:
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Performance & Re-renders | CRITICAL | perf- |
| 2 | useFrame & Animation | CRITICAL | frame- |
| 3 | Component Patterns | HIGH | component- |
| 4 | Canvas & Setup | HIGH | canvas- |
| 5 | Drei Helpers | MEDIUM-HIGH | drei- |
| 6 | Loading & Suspense | MEDIUM-HIGH | loading- |
| 7 | State Management | MEDIUM | state- |
| 8 | Events & Interaction | MEDIUM | events- |
| 9 | Post-processing | MEDIUM | postpro- |
| 10 | Physics (Rapier) | LOW-MEDIUM | physics- |
| 11 | Leva (Debug GUI) | LOW | leva- |
perf-never-set-state-in-useframe - NEVER call setState in useFrameperf-isolate-state - Isolate components that need React stateperf-zustand-selectors - Use Zustand selectors, not entire storeperf-transient-subscriptions - Use transient subscriptions for continuous valuesperf-memo-components - Memoize expensive componentsperf-keys-for-lists - Use stable keys for dynamic listsperf-avoid-inline-objects - Avoid creating objects/arrays in JSXperf-dispose-auto - Understand R3F auto-dispose behaviorperf-visibility-toggle - Toggle visibility instead of remountingperf-r3f-perf - Use r3f-perf for performance monitoringframe-priority - Use priority for execution orderframe-delta-time - Always use delta for animationsframe-conditional-subscription - Disable useFrame when not neededframe-destructure-state - Destructure only what you needframe-render-on-demand - Use invalidate() for on-demand renderingframe-avoid-heavy-computation - Move heavy work outside useFramecomponent-jsx-elements - Use JSX for Three.js objectscomponent-attach-prop - Use attach for non-standard propertiescomponent-primitive - Use primitive for existing objectscomponent-extend - Use extend() for custom classescomponent-forwardref - Use forwardRef for reusable componentscomponent-dispose-null - Set dispose={null} on shared resourcescanvas-size-container - Canvas fills parent containercanvas-camera-default - Configure camera via propcanvas-gl-config - Configure WebGL contextcanvas-shadows - Enable shadows at Canvas levelcanvas-frameloop - Choose appropriate frameloop modecanvas-events - Configure event handlingcanvas-linear-flat - Use linear/flat for correct colorsdrei-use-gltf - useGLTF with preloadingdrei-use-texture - useTexture for texture loadingdrei-environment - Environment for realistic lightingdrei-orbit-controls - OrbitControls from Dreidrei-html - Html for DOM overlaysdrei-text - Text for 3D textdrei-instances - Instances for optimized instancingdrei-use-helper - useHelper for debug visualizationdrei-bounds - Bounds to fit cameradrei-center - Center to center objectsdrei-float - Float for floating animationloading-suspense - Wrap async components in Suspenseloading-preload - Preload assets with useGLTF.preloadloading-use-progress - useProgress for loading UIloading-lazy-components - Lazy load heavy componentsloading-error-boundary - Handle loading errorsstate-zustand-store - Create focused Zustand storesstate-avoid-objects-in-store - Be careful with Three.js objectsstate-subscribeWithSelector - Fine-grained subscriptionsstate-persist - Persist state when neededstate-separate-concerns - Separate stores by concernevents-pointer-events - Use pointer events on meshesevents-stop-propagation - Prevent event bubblingevents-cursor-pointer - Change cursor on hoverevents-raycast-filter - Filter raycastingevents-event-data - Understand event data structurepostpro-effect-composer - Use EffectComposerpostpro-common-effects - Common effects referencepostpro-selective-bloom - SelectiveBloom for optimized glowpostpro-custom-shader - Create custom effectspostpro-performance - Optimize post-processingphysics-setup - Basic Rapier setupphysics-body-types - dynamic, fixed, kinematicphysics-colliders - Choose appropriate collidersphysics-events - Handle collision eventsphysics-api-ref - Use ref for physics APIphysics-performance - Optimize physicsleva-basic - Basic Leva usageleva-folders - Organize with foldersleva-conditional - Hide in productionRead individual rule files for detailed explanations and code examples:
rules/perf-never-set-state-in-useframe.md
rules/drei-use-gltf.md
rules/state-zustand-selectors.mdFor the complete guide with all rules expanded: ../R3F_BEST_PRACTICES.md
// BAD - 60 re-renders per second!
function BadComponent() {
const [position, setPosition] = useState(0);
useFrame(() => {
setPosition(p => p + 0.01); // NEVER DO THIS
});
return <mesh position-x={position} />;
}
// GOOD - Mutate refs directly
function GoodComponent() {
const meshRef = useRef();
useFrame(() => {
meshRef.current.position.x += 0.01;
});
return <mesh ref={meshRef} />;
}// BAD - Re-renders on ANY store change
const store = useGameStore();
// GOOD - Only re-renders when playerX changes
const playerX = useGameStore(state => state.playerX);
// BETTER - No re-renders, direct mutation
useFrame(() => {
const { value } = useStore.getState();
ref.current.position.x = value;
});import { useGLTF } from '@react-three/drei';
function Model() {
const { scene } = useGLTF('/model.glb');
return <primitive object={scene} />;
}
// Preload for instant loading
useGLTF.preload('/model.glb');function App() {
return (
<Canvas>
<Suspense fallback={<Loader />}>
<Model />
</Suspense>
</Canvas>
);
}import { Perf } from 'r3f-perf';
function App() {
return (
<Canvas>
<Perf position="top-left" />
<Scene />
</Canvas>
);
}// BAD: Remounting destroys and recreates
{showModel && <Model />}
// GOOD: Toggle visibility, keeps instance alive
<Model visible={showModel} />af7d491
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.