OneScript
UI

Hooks

组件状态、Context、缓存和副作用。

Hooks 从 onescript/views 导入,为函数组件提供状态、Context、缓存和副作用能力。

Hooks 为函数组件提供本地状态、Context 和生命周期副作用。请在组件顶层调用 Hooks,并在每次渲染时保持调用顺序和数量稳定。

useState

useState 为每个已挂载的组件实例保存本地状态。

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

传入函数可以执行一次惰性初始化。setter 支持直接传入下一个值,或传入接收旧值的更新函数。OneScript 会批处理连续更新,并在新值通过 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="姓名"
      />
      <Text>你好,{name}</Text>
    </VStack>
  )
}

useReducer

useReducer 使用 reducer 集中管理组件状态。reducer 接收当前 state 和 action,并返回下一份 state;dispatch 接收一个 action,并安排组件使用新状态重新渲染。

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>修改次数:{state.changes}</Text>
    </VStack>
  )
}

state 和 action 可以使用任意类型。reducer 应保持纯函数,并返回新的对象或数组来表达变化。dispatch 的引用在组件生命周期内保持稳定,可以安全地作为 useEffect 依赖。

OneScript 会批处理连续 dispatch,并依次将每个 action 应用于最新 reducer state。reducer 返回与当前 state 通过 Object.is 比较相等的值时跳过渲染。调用 dispatch 后,当前事件处理函数中的 state 仍是本次渲染的快照。

第三个 init 参数用于昂贵的惰性初始化,并且只在组件首次挂载时执行一次:

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

省略 init 时,initialState 会直接成为初始状态;函数类型的 initialState 也会作为普通值保存。组件后续渲染传入的新 initial state 或 initial arg 不会重置已有状态。reducer、initializer 或 dispatch 使用错误会通过现有脚本错误通道报告。

createContext 与 useContext

createContext 创建可以跨组件层级传递数据的 Context。useContext 返回调用组件上方最近 Provider 的值;组件上方没有对应 Provider 时返回创建 Context 时传入的静态默认值。

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

将 Context 作为组件,并通过 value 为子组件提供共享数据:

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

const ThemeContext = createContext("light")

function Greeting() {
  const theme = useContext(ThemeContext)
  return <Text>当前主题:{theme}</Text>
}

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

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

<ThemeContext.Provider value={theme}> 提供相同的兼容写法。ThemeContext.Consumer 接收以当前值为参数的 children 函数,适合迁移 legacy 组件。

Provider 可以嵌套,最近的同类型 Provider 会覆盖外层值。显式传入 value={undefined} 会向消费者提供 undefined;省略 value 时同样提供 undefined 并打印警告。Provider 所在组件调用 useContext 时读取该 Provider 上方的值。

Provider 使用 Object.is 比较前后值。值发生变化时,所有订阅该 Provider 的消费者都会获得最新值,稳定 props 的中间组件不会阻断传播。对象或函数 Context 值可以通过 useMemo 和 useCallback 保持引用稳定:

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

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

useCallback

useCallback 用于在多次渲染之间缓存函数引用。首次渲染时,OneScript 返回传入的函数;所有依赖通过 Object.is 比较保持相等时继续返回同一函数;任一依赖变化时采用当前渲染传入的新函数。

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="搜索"
    />
  )
}

回调没有读取会变化的 props 或 state 时可以传入 []。依赖数组需要包含回调读取的所有响应式值。稳定的 callback 可以保持 Effect 依赖不变,也能让 OneScript 复用其他 props 同样浅比较相等的子组件。

TypeScript API 要求传入依赖数组。JavaScript 代码省略依赖时,每次渲染都会获得当前传入的函数。组件生命周期内需要保持依赖模式和数组长度稳定。

useMemo

useMemo 用于在多次渲染之间缓存计算结果。首次渲染时执行计算函数;所有依赖通过 Object.is 比较保持相等时返回缓存值;任一依赖变化时使用当前闭包重新计算。

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>
}

计算函数应保持纯函数、接收零个参数,并通过闭包读取所需值。依赖数组需要包含计算函数读取的所有响应式值。传入 [] 会在当前组件实例的生命周期内持续复用首次结果。

useMemo 可以稳定对象、数组等引用,让浅比较相等的子组件复用输出,也可以保持 useEffect 的对象依赖稳定。useCallback(callback, dependencies) 对应缓存函数引用,useMemo(() => value, dependencies) 对应缓存计算结果。

TypeScript API 要求传入依赖数组。JavaScript 代码省略依赖时,每次渲染都会重新计算。组件生命周期内需要保持依赖模式和数组长度稳定。useMemo 是渲染性能优化,计算结果可以是任意 JavaScript 值。

useEffect

useEffect 用于将已挂载的组件与定时器、订阅、网络请求等外部系统同步。原生视图提交成功后,OneScript 执行 setup。setup 可以返回 cleanup 函数,在 effect 重新执行或组件卸载前释放资源。

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>已运行 {seconds} 秒</Text>
}
依赖参数行为
省略每次组件成功提交后执行。
[]原生视图首次挂载后执行一次。
[value1, value2]首次挂载后执行,并在任一依赖通过 Object.is 比较发生变化时重新执行。

依赖变化时,OneScript 会先执行上一次 cleanup,再执行新的 setup。会话终止时也会在释放 JavaScript 运行时之前执行所有生效中的 cleanup。

Effect setup 采用同步函数,并返回 cleanup 函数或 undefined。异步工作应在 effect 内部启动:

useEffect(() => {
  let active = true

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

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

Widget 中的行为

Widget 采用静态渲染。useState 和 useReducer 的初始状态会参与生成视图,useContext 会读取默认值或最近 Provider 的值,useCallback 会返回传入的函数,useMemo 会执行计算函数并使用结果。setter 和 dispatch 不会产生 Widget 更新,Widget 渲染会跳过 useEffect setup 和 cleanup。请使用 Widget Timeline 安排内容更新。

How is this guide?

最后更新于

本页目录