Skip to content

Write plain old JavaScript. Compile it into a reactive masterpiece. Subscribe to immutable snapshots.

License

Notifications You must be signed in to change notification settings

aleclarson/valtio-kit

Repository files navigation

valtio-kit

Valtio Kit is a Vite plugin and an optimized runtime that brings transparent reactivity to your plain JavaScript logic. It leverages Valtio to provide immutable snapshots and reactive subscriptions, allowing you to manage state with straightforward, imperative JavaScript or TypeScript. The key advantage is that your React components will efficiently rerender only when the specific state they depend on changes, optimizing performance and simplifying state management.

pnpm add valtio-kit

Usage

  1. Add the Vite plugin to your vite.config.ts file.
import { valtioKit } from 'valtio-kit/vite'

export default defineConfig({
  plugins: [
    // These are the default options.
    valtioKit({
      include: /\.state\.[jt]s$/,
      exclude: /\/node_modules\//,
      globals: false,
    }),
  ],
})
  1. Create a module with a .state.ts or .state.js extension.

  2. (Optional) Enable the “globals API” to skip importing the various runtime functions provided by this package.

Enabling the globals API
  • Keep your “state module” in a dedicated src/state folder. Then add a tsconfig.json with the following compiler option:
"compilerOptions": {
  "types": ["valtio-kit/globals"]
}
  • If you don't add a tsconfig.json file, you need to use a triple-slash directive instead:
/// <reference types="valtio-kit/globals" />
  • Finally, set globals: true in your vite.config.ts file:
export default defineConfig({
  plugins: [valtioKit({ globals: true })],
})
  1. Call createClass to define a reactive class. For example, here's a simple counter (please note that there's much, much more you can do with createClass):
export const Counter = createClass((initialCount = 0) => {
  let count = initialCount

  return {
    count,
    increment(amount = 1) {
      count += amount
    },
    decrement(amount = 1) {
      count -= amount
    },
  }
})

Note

The function passed to createClass is known as the factory function, which initializes a reactive instance by returning an object literal. The function returned by createClass is known as a reactive class.

  1. Initialize a reactive instance with the useInstance hook. Before using its data to render your component, you should first pass it to Valtio's useSnapshot hook.
import { useInstance, useSnapshot } from 'valtio-kit/react'
import { Counter } from './Counter.state'

export function App() {
  // Create a counter with an initial count of 100. Any persistent effects set up
  // by the instance will be cleaned up when the component unmounts.
  const counter = useInstance(Counter, 100)

  // Subscribe to the counter's data. Only the data you use will trigger re-renders.
  const { count, increment, decrement } = useSnapshot(counter)

  return (
    <div>
      Count: {count}
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}

The useInstance hook creates a reactive instance, which your React components can subscribe to using the useSnapshot hook.

Global state

In some cases, you may prefer to initialize a reactive instance outside of a React component. For example, you may want to initialize a global state object that can be accessed by any component.

import { Counter } from './Counter.state'

// Initialize a global counter with an initial count of 1.
export const counter = new Counter(1)

If your global instance sets up any persistent effects (i.e. computed, watch, on, etc.), you need to clean up the effects when Vite HMR is triggered.

import.meta.hot?.dispose(() => counter.release())

Terminology

This package borrows terminology from Valtio. For example, a snapshot is an immutable copy of a reactive instance, which can intelligently rerender your React components if an accessed property changes. You subscribe to a reactive instance (or its property) to be notified when it changes. In Valtio, a reactive instance is referred to as a proxy.

Rules

There are a few rules to keep in mind inside a createClass factory function:

  • You must return an object literal.
  • Root-level let and var declarations are deeply reactive by default.
  • If you re-assign a factory parameter, it becomes deeply reactive. Notably, this behavior does not apply to properties of object/array parameters (unless you also re-assign the parameter itself).
  • When you return a non-const variable as a property, a one-way binding is implicitly created, so assigning to the variable will re-render any components that use the property.
  • Certain objects are deeply reactive when assigned to root-level variables. This is even true when assignment occurs inside a nested function. Supported object types include:
    • plain objects
    • arrays
    • new Map()
    • new Set()
  • Any time you construct a new Map or Set anywhere inside your factory function, it will be made deeply reactive.
  • The factory function can have arguments. Any kind and any number of arguments are supported.
  • Passing a reactive instance into a factory function is not currently supported.
  • Variable shadowing is currently discouraged, as some edge cases have not yet been ironed out.

Further Reading

Check out the docs/ folder for more information.

It's also recommended to read the API reference below.

API

The following functions are implicitly available within every createClass factory function.

computed

computed is a function that subscribes to reactive values and returns a new reactive value.

Note

Computed values cannot be declared just anywhere. You can only call computed with the following syntax and it must be declared at the root level of a createClass factory function:

// This is a readonly, computed variable.
const xyz = computed(() => )

// This is a readonly, computed property.
const foo = { bar: computed(() => ) }

// This is a computed property assignment.
foo.bar = computed(() => )
const TacoExample = createClass(() => {
  let day = 'Monday'
  const taco = { type: 'beef' }
  const isTacoTuesday = computed(
    () => day === 'Tuesday' && taco.type === 'beef'
  )

  return {
    isTacoTuesday,
    setDay(newValue: string) {
      day = newValue
    },
    setTacoType(type: string) {
      taco.type = type
    },
  }
})

const example = new TacoExample()
example.isTacoTuesday // => false
example.setDay('Tuesday')
example.isTacoTuesday // => true
example.setTacoType('chicken')
example.isTacoTuesday // => false

watch

watch is a persistent effect that reruns when its reactive dependencies change.

const CatExample = createClass(() => {
  // Any of this data can be watched.
  const cat = { name: 'Fluffy' }
  let numLives = 9

  watch(() => {
    console.log(
      `The cat named ${cat.name} has ${numLives} lives remaining. Meow!`
    )
  })

  return {
    renameCat(name: string) {
      cat.name = name
    },
    fallFromTree() {
      numLives--
    },
    eatFish() {
      numLives++
    },
  }
})

const cat = new CatExample()
// Logs "The cat named Fluffy has 9 lives remaining. Meow!"

cat.fallFromTree()
// Logs "The cat named Fluffy has 8 lives remaining. Meow!"

cat.renameCat('Whiskers')
// Logs "The cat named Whiskers has 8 lives remaining. Meow!"

cat.eatFish()
// Logs "The cat named Whiskers has 9 lives remaining. Meow!"

on

on is a function that attaches an event listener to any EventTarget.

const ResizeExample = createClass(() => {
  let ratio = window.innerWidth / window.innerHeight
  on(window, 'resize', () => {
    ratio = window.innerWidth / window.innerHeight
  })
  return {
    ratio,
  }
})

const example = new ResizeExample()
example.ratio // Updates when the window is resized.

onMount

onMount is a function that runs a callback when a reactive instance is mounted. The callback must return a cleanup function, which gets called when the reactive instance is unmounted.

const StyleSheetExample = createClass(() => {
  const style = document.createElement('style')
  onMount(() => {
    document.head.appendChild(style)
    return () => {
      document.head.removeChild(style)
    }
  })
  return {
    style,
  }
})

const example = new StyleSheetExample()
example.style.textContent = 'body { background-color: red; }'

onUpdate

onUpdate is a function that runs a callback when a reactive instance has its update method called. It receives the latest factory arguments. This can happen one of two ways:

  • By calling the update method directly.
  • By a component re-rendering (but only if the useInstance(MyClass, ...args) hook signature is used). Notably, the useInstance(() => new MyClass(), deps) hook signature will never trigger onUpdate handlers.
const AudioPlayer = createClass((src: string) => {
  const audio = new HTMLAudioElement()
  // Update `audio.src` whenever `src` is changed.
  audio.src = computed(() => src)

  // By assigning to `src`, we make it reactive.
  onUpdate<typeof AudioPlayer>((...args) => ([src] = args))

  return {
    /* ... */
  }
})

subscribe

subscribe is a function that listens for changes to a given reactive object or even a reactive variable.

const SubscribeExample = createClass(() => {
  let a = 0
  const b = { c: 1 }

  // Listen for changes to the `a` variable.
  subscribe(a, () => {
    console.log('a changed to', a)
  })

  // Listen for changes to the `b` object.
  subscribe(b, () => {
    console.log('b changed to', b)
  })

  return {
    update(update: { a: number; b: { c: number } }) {
      a = update.a
      Object.assign(b, update.b)
    },
  }
})

const example = new SubscribeExample()

example.update({ a: 1, b: { c: 2 } })
// Logs "a changed to 1"
// Logs "b changed to { c: 2 }"

subscribeKey

subscribeKey is a function that listens for changes to a specific key of a reactive object.

const SubscribeKeyExample = createClass(() => {
  const b = { c: 1 }
  subscribeKey(b, 'c', () => {
    console.log('b.c changed to', b.c)
  })
  return {
    update(update: { b: { c: number } }) {
      Object.assign(b, update.b)
    },
  }
})

const example = new SubscribeKeyExample()

example.update({ b: { c: 1 } })
// Logs nothing since b.c is already 1

example.update({ b: { c: 2 } })
// Logs "b.c changed to 2"

snapshot

snapshot is a function that returns an immutable, deep copy of a reactive object. Learn more

ref

ref is a function that prevents an object from being made reactive. Learn more

getVersion

getVersion is a function that returns a “version” number that represents when a reactive object was last updated. Learn more

About

Write plain old JavaScript. Compile it into a reactive masterpiece. Subscribe to immutable snapshots.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published