diff --git a/.storybook/stories/Helper.stories.tsx b/.storybook/stories/Helper.stories.tsx
new file mode 100644
index 000000000..a5cc8b5b6
--- /dev/null
+++ b/.storybook/stories/Helper.stories.tsx
@@ -0,0 +1,71 @@
+import { useFrame } from '@react-three/fiber'
+import * as React from 'react'
+import * as THREE from 'three'
+import { BoxHelper, CameraHelper } from 'three'
+import { VertexNormalsHelper } from 'three-stdlib'
+import { Helper, PerspectiveCamera, Sphere } from '../../src'
+import { Setup } from '../Setup'
+
+export default {
+ title: 'Gizmos/Helper',
+ component: Helper,
+ decorators: [(storyFn) => {storyFn()}],
+ args: {
+ showHelper: true,
+ },
+ argTypes: {
+ showHelper: {
+ type: 'boolean',
+ },
+ },
+}
+
+type StoryProps = {
+ showHelper: boolean
+}
+
+const Scene: React.FC = ({ showHelper }) => {
+ const mesh = React.useRef(null!)
+
+ return (
+
+
+
+ {showHelper && (
+ <>
+
+
+ >
+ )}
+
+ )
+}
+
+export const DefaultStory = (args: StoryProps) =>
+DefaultStory.storyName = 'Default'
+
+const CameraScene: React.FC = ({ showHelper }) => {
+ const camera = React.useRef()
+
+ useFrame(({ clock }) => {
+ const t = clock.getElapsedTime()
+
+ if (camera.current) {
+ camera.current.lookAt(0, 0, 0)
+
+ camera.current.position.x = Math.sin(t) * 4
+ camera.current.position.z = Math.cos(t) * 4
+ }
+ })
+
+ return (
+
+
+
+ {showHelper && }
+
+ )
+}
+
+export const CameraStory = (args: StoryProps) =>
+CameraStory.storyName = 'Camera Helper'
diff --git a/README.md b/README.md
index b61cb50e8..1ee9df8b9 100644
--- a/README.md
+++ b/README.md
@@ -935,6 +935,22 @@ useHelper(condition && mesh, BoxHelper, 'red') // you can pass false instead of
```
+#### Helper
+
+[![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/gizmos-helper--default-story)
+
+A component for declaratively adding helpers to existing nodes in the scene. It handles removal of the helper on unmount and auto-updates it by default.
+
+```jsx
+
+
+
+
+
+
+
+```
+
# Shapes
#### Plane, Box, Sphere, Circle, Cone, Cylinder, Tube, Torus, TorusKnot, Ring, Tetrahedron, Polyhedron, Icosahedron, Octahedron, Dodecahedron, Extrude, Lathe, Shape
diff --git a/src/core/Helper.tsx b/src/core/Helper.tsx
new file mode 100644
index 000000000..d1601190d
--- /dev/null
+++ b/src/core/Helper.tsx
@@ -0,0 +1,47 @@
+import { useFrame, useThree } from '@react-three/fiber'
+import * as React from 'react'
+import { Object3D } from 'three'
+
+type HelperType = Object3D & { update: () => void; dispose: () => void }
+type HelperConstructor = new (...args: any[]) => HelperType
+type HelperArgs = T extends [infer _, ...infer R] ? R : never
+
+export type HelperProps = {
+ type: T
+ args?: HelperArgs>
+}
+
+export const Helper = ({
+ type: helperConstructor,
+ args = [] as never,
+}: HelperProps) => {
+ const objectRef = React.useRef(null!)
+ const helperRef = React.useRef()
+
+ const scene = useThree((state) => state.scene)
+
+ React.useLayoutEffect(() => {
+ const parent = objectRef.current?.parent
+
+ if (!helperConstructor || !parent) return
+
+ const helper = new helperConstructor(parent, ...args)
+
+ helperRef.current = helper
+
+ // Prevent the helpers from blocking rays
+ helper.traverse((child) => (child.raycast = () => null))
+
+ scene.add(helper)
+
+ return () => {
+ helperRef.current = undefined
+ scene.remove(helper)
+ helper.dispose?.()
+ }
+ }, [scene, helperConstructor, ...args])
+
+ useFrame(() => void helperRef.current?.update?.())
+
+ return
+}
diff --git a/src/core/index.ts b/src/core/index.ts
index 3eaa59981..916db284a 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -57,6 +57,7 @@ export * from './useVideoTexture'
export * from './useFont'
// Misc
+export * from './Helper'
export * from './Stats'
export * from './StatsGl'
export * from './useDepthBuffer'