OneScript
UI

Hooks

Component state, Context, memoization, and effects.

Import Hooks from onescript/views to manage component state, Context, memoization, and effects.

Hooks add local state, context, and lifecycle effects to function components. Call Hooks at the top level of a component and keep their order and count stable between renders.

useState

useState stores local state for one mounted component instance.

function useState<T>(
  initialState: T | (() => T)
): readonly [T, (action: T | ((previous: T) => T)) => void]

Pass a function to calculate expensive initial state once. The setter accepts either the next value or a function that receives the previous value. OneScript batches multiple updates and skips a render when the next value is equal according to Object.is.

import { VStack, Text, TextField, useState } from "onescript/views"

function Profile() {
  const [name, setName] = useState("")

  return (
    <VStack spacing={8}>
      <TextField
        value={name}
        onChange={setName}
        prompt="Name"
      />
      <Text>Hello {name}</Text>
    </VStack>
  )
}

useReducer

useReducer centralizes component state updates in a reducer. The reducer receives the current state and an action and returns the next state; dispatch accepts one action and schedules a render with the updated state.

type Reducer<State, Action> = (state: State, action: Action) => State
type ReducerDispatch<Action> = (action: Action) => void

function useReducer<State, Action>(
  reducer: Reducer<State, Action>,
  initialState: State
): readonly [State, ReducerDispatch<Action>]

function useReducer<State, Action, InitialArg>(
  reducer: Reducer<State, Action>,
  initialArg: InitialArg,
  init: (initialArg: InitialArg) => State
): readonly [State, ReducerDispatch<Action>]
import { VStack, Text, TextField, useReducer } from "onescript/views"

type State = { name: string; changes: number }
type Action = { type: "changedName"; name: string } | { type: "reset" }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "changedName":
      return { name: action.name, changes: state.changes + 1 }
    case "reset":
      return { name: "", changes: 0 }
  }
}

function ProfileEditor() {
  const [state, dispatch] = useReducer(reducer, { name: "", changes: 0 })

  return (
    <VStack spacing={8}>
      <TextField
        value={state.name}
        onChange={name => dispatch({ type: "changedName", name })}
      />
      <Text>Changes: {state.changes}</Text>
    </VStack>
  )
}

State and actions may use any types. Keep reducers pure and return new objects or arrays to represent changes. Dispatch has a stable identity for the component lifetime and is safe to include in useEffect dependencies.

OneScript batches consecutive dispatches and applies each action to the latest reducer state in order. A reducer result equal to the current state according to Object.is skips rendering. After dispatching, state in the current event handler remains the snapshot from its render.

Use the third init argument for expensive lazy initialization. It runs once during the component's initial mount:

const [state, dispatch] = useReducer(reducer, username, createInitialState)

When init is omitted, initialState becomes the state directly, including when it is a function value. New initial states or initial arguments passed by later renders do not reset existing state. Reducer, initializer, and dispatch errors flow through the existing script error channel.

createContext and useContext

createContext creates context that can carry data across component layers. useContext returns the value from the closest matching Provider above the calling component, or the static default supplied when the context was created when no Provider exists above it.

interface ContextProviderProps<T> {
  readonly value: T
  readonly children?: ViewChild
}

interface Context<T> extends ViewComponent<ContextProviderProps<T>> {
  readonly Provider: Context<T>
  readonly Consumer: ViewComponent<{
    readonly children: (value: T) => ViewChild
  }>
}

function createContext<T>(defaultValue: T): Context<T>
function useContext<T>(context: Context<T>): T

Use the Context as a component and pass shared data to its children through value:

import { Text, TextField, createContext, useContext, useState } from "onescript/views"

const ThemeContext = createContext("light")

function Greeting() {
  const theme = useContext(ThemeContext)
  return <Text>Current theme: {theme}</Text>
}

function Screen() {
  const [theme, setTheme] = useState("dark")

  return (
    <ThemeContext value={theme}>
      <TextField
        value={theme}
        onChange={setTheme}
      />
      <Greeting />
    </ThemeContext>
  )
}

<ThemeContext.Provider value={theme}> is the compatible Provider form. ThemeContext.Consumer accepts a children function whose argument is the current value, supporting legacy components.

Providers may be nested, and the closest matching Provider overrides outer values. Passing value={undefined} explicitly provides undefined; omitting value also provides undefined and prints a warning. A component that renders a Provider reads the value above that Provider when it calls useContext.

Providers compare successive values with Object.is. Every consumer subscribed to a Provider receives its latest changed value, including consumers below intermediate components with stable props. Stabilize object and function context values with useMemo and useCallback when useful:

const login = useCallback(response => saveSession(response), [])
const account = useMemo(() => ({ user, login }), [user, login])

return <AccountContext value={account}><Profile /></AccountContext>

useCallback

useCallback caches a function reference between renders. OneScript returns the function passed during the initial render, keeps returning that function while every dependency is equal according to Object.is, and adopts the function from the current render when a dependency changes.

type DependencyList = readonly unknown[]

function useCallback<T extends (...args: any[]) => any>(
  callback: T,
  dependencies: DependencyList
): T
import { TextField, useCallback, useEffect, useState } from "onescript/views"

function Search({ userID }: { userID: string }) {
  const [query, setQuery] = useState("")

  const loadResults = useCallback(() => {
    void search(userID, query)
  }, [userID, query])

  useEffect(() => {
    loadResults()
  }, [loadResults])

  return (
    <TextField
      value={query}
      onChange={setQuery}
      prompt="Search"
    />
  )
}

Use [] when the callback reads no changing props or state. Include every reactive value read by the callback in its dependency list. A stable callback can keep an Effect dependency unchanged and lets OneScript reuse a child component whose other props are also shallowly equal.

The TypeScript API requires a dependency list. JavaScript code that omits it receives the current function on every render. The dependency mode and array length must stay stable for the lifetime of the component.

useMemo

useMemo caches a calculation result between renders. It runs the calculation during the initial render, returns the cached value while every dependency remains equal according to Object.is, and recalculates with the current closure when a dependency changes.

function useMemo<T>(
  calculateValue: () => T,
  dependencies: DependencyList
): T
import { Text, useMemo, useState } from "onescript/views"

function Factorial() {
  const [count, setCount] = useState(5)
  const factorial = useMemo(() => {
    let result = 1
    for (let i = 1; i <= count; i++) result *= i
    return result
  }, [count])

  return <Text>{count}! = {factorial}</Text>
}

Keep the calculation pure and parameterless, and read required values through its closure. Include every reactive value read by the calculation in the dependency list. Passing [] reuses the initial result for the lifetime of the current component instance.

useMemo can stabilize object and array references so children with shallowly equal props can reuse their output, and it can keep object dependencies of useEffect stable. useCallback(callback, dependencies) caches a function reference; useMemo(() => value, dependencies) caches a calculation result.

The TypeScript API requires a dependency list. JavaScript code that omits it recalculates on every render. The dependency mode and array length must stay stable for the lifetime of the component. useMemo is a render performance optimization and may cache any JavaScript value.

useEffect

useEffect synchronizes a mounted component with timers, subscriptions, network requests, and other external systems. OneScript runs setup after the native view commit succeeds. Return a cleanup function to release resources before the effect runs again or the component unmounts.

type EffectCleanup = () => void
type EffectCallback = () => void | EffectCleanup
type DependencyList = readonly unknown[]

function useEffect(
  setup: EffectCallback,
  dependencies?: DependencyList
): void
import { Text, useEffect, useState } from "onescript/views"

function Clock() {
  const [seconds, setSeconds] = useState(0)

  useEffect(() => {
    const timer = setInterval(() => {
      setSeconds(previous => previous + 1)
    }, 1000)

    return () => clearInterval(timer)
  }, [])

  return <Text>Running for {seconds} seconds</Text>
}
DependenciesBehavior
OmittedRuns after every successful component commit.
[]Runs once after the initial native mount.
[value1, value2]Runs after the initial mount and whenever any dependency changes according to Object.is.

When dependencies change, OneScript runs the previous cleanup before the new setup. Session termination also runs every active cleanup before releasing the JavaScript runtime.

Effect setup is synchronous and must return a cleanup function or undefined. Start asynchronous work inside the effect instead of making the setup function async:

useEffect(() => {
  let active = true

  void loadProfile().then(profile => {
    if (active) setProfile(profile)
  })

  return () => {
    active = false
  }
}, [userID])

Widget behavior

Widget rendering is static. Initial state from useState and useReducer contributes to the generated view, useContext reads a default or closest Provider value, useCallback returns the supplied function, and useMemo runs its calculation and uses the result. State setters and dispatch do not produce Widget updates, and Widget rendering skips useEffect setup and cleanup. Use the Widget Timeline to schedule updated content.

How is this guide?

Last updated on

On this page