rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

RFC: `useIsolation`

Open Ayc0 opened this issue 6 months ago • 3 comments

This RFC related to:

  • useIsolation in https://github.com/reactjs/rfcs/issues/168,
  • useContextSelector in https://github.com/reactjs/rfcs/pull/119.

This is about performance & memoization

>>> View rendered text <<<

Ayc0 avatar Dec 13 '23 15:12 Ayc0

If I understand correctly you want the equivalent of the following:

Usage:

import React from 'react'

export function App() {
  console.log('render App')
  return (
    <IsolationProvider>
        <Test />
    </IsolationProvider>
  );
}

function Test() {
  console.log('render test')

  const x = useIsolation(() => {
    const [x, setX] = React.useState(0)
    React.useEffect(
      () => {
        let interval = setInterval(() => setX(x => x + 1), 1000)
        return () => { clearInterval(interval) }
      },
      []
    )
    return React.useMemo(() => x - (x % 2), [x])
  })

  return <p>{x}</p>
}

Non-optimized user space implementation:

const isolationContext = React.createContext(null)

function useIsolation(unsafeHook) {
  const hook = useEvent(unsafeHook)

  const [result, setResult] = React.useState(null)

  const registerHook = React.useContext(isolationContext)

  React.useEffect(
    () => registerHook({ hook, callback: (...args) => setTimeout(() => setResult(...args), 0) }),
    []
  )

  return result
}

function IsolationProvider({ children }) {
  console.log('render isolation provider')

  const [hookInfo, setHookInfo] = React.useState([])

  const registerHook = React.useCallback(
    (hookInfoToBeIsolated) => {
      setHookInfo(existing => existing.concat(hookInfoToBeIsolated))
      return function cleanup() {
        setHookInfo(existing => existing.filter(info => info !== hookInfoToBeIsolated))
      }
    },
    []
  )

  return (
    <isolationContext.Provider value={registerHook}>
      {children}
      {hookInfo.map((info, i) => 
        // key should be handled differently
        <Isolation key={i} {...{ info }} />
      )}
    </isolationContext.Provider>
  )
}

function Isolation({ info }) {
  const { callback, hook } = info
  const result = hook()

  console.log('hook executed', result)

  useCallOnChange({ ifChanged: result, callback })

  return null
}

function useCallOnChange({ ifChanged, callback }) {
  const changeRef = React.useRef(null)
  if (changeRef.current !== ifChanged) callback(ifChanged)
  changeRef.current = ifChanged
}

function useEvent(f) {
  const fRef = React.useRef(null)
  fRef.current = f

  return React.useCallback((...args) => fRef.current(...args), [])
}

EECOLOR avatar Dec 15 '23 07:12 EECOLOR

If I understand correctly you want the equivalent of the following:

Yes, the idea is here with 2 differences:

  • useIsolation could run synchronously during the 1st render (like a useMemo)
  • you could also provide dependencies to useIsolation(hook, [deps]) to conditionally re-call the hook when its parent scope changes (but not if hooks within it would trigger a re-render)

Ayc0 avatar Dec 15 '23 09:12 Ayc0

Thank you for the RFC. I wanted to note that we’ve had a very similar proposal planned except that we wanted to roll this behavior into the existing useMemo Hook.

gaearon avatar Jan 03 '24 10:01 gaearon