A starter template to develop more Three.js experiences taking advantage of the amazing ecosystem built around Next.js and React-Three Fiber. This repo is derived from the excellent react-three-next starter, with some customizations:
- Next.js with app router support.
- React Three Fiber and pmndrs/drei to create Three.js experiences using a declarative syntax.
- Jotai as the state management solution, including some custom Tweakpane atoms for a nicer DX.
- TailwindCSS and Class Variance Authority primed to leverage shadcn/ui.
- Codebase organized around features and components.
- TypeScript for type safety and intellisense, with matching ESLint and Prettier configurations.
- Ejected the ShaderMaterial from upstream
drei
to better adapt it to a TypeScript codebase. - Cherry-picked some
3.x
features, such as Lenis support. - Extracted dynamic imports to barrel files for a more declarative syntax.
- Added various components, helpers, hooks, and utils.
- Added missing TypeScript types and reorganized the codebase.
I am figuring out how to integrate Tweakpane with Jotai to have a debugging DX that uses the same API as the central state.
In Next.js, it seems necessary to wait for the first layout render, so tweakpane can append components to the body during initialization. To solve it, I've created a Tweakpane component that handles the mounting logic. Jotai also recommends using a custom store in Next.js for SSR support.
In addition, there are a couple of atom helpers that make it very easy to add and use a reactive tweak. For example, an atomWithBinding is first declared outside the component, where it returns a regular jotai atom and a subscriber function that can be used anywhere in the app.
The tweak atom can be used like any other atom from Jotai. The binding will show up once the consuming component is mounted.
import { useAtomValue } from 'jotai';
export const [blobColorAtom, useBlobColor] = atomWithBinding(
'blobColor', // label
'#1fb2f5', // value
{
// tweakpane binding options
}
);
export function MyComponent() {
// blobColor is reactive
const blobColor = useAtomValue(blobColorAtom);
return (
// JSX
)
}
Another option is to use a non-reactive listener. The listener returns a RefObject
and also accepts a callback
function that will execute when the binding value changes, without causing a re-render.
While it is not strictly necessary to wrap the function in a useCallback
hook, doing so will prevent recreating the atom subscription when the component re-renders.
import { useBlobColor } from './MyComponent';
export function MyOtherComponent() {
// blobColor is a non-reactive ref
const blobColorRef = useBlobColor(
useCallback(({ get, set, value, prevValue }) => {
// get(anyAtom)
// set(anyAtom)
// value <- new value after the change
// prevVal <- previous atom value
}, [])
)
return (
// JSX
)
}