Skip to content

Conversation

@Ayc0
Copy link

@Ayc0 Ayc0 commented Dec 13, 2023

This RFC related to:

This is about performance & memoization

>>> View rendered text <<<

@EECOLOR
Copy link

EECOLOR commented Dec 15, 2023

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), [])
}

@Ayc0
Copy link
Author

Ayc0 commented Dec 15, 2023

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)

@gaearon
Copy link
Member

gaearon commented Jan 3, 2024

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.

@jaskp
Copy link

jaskp commented Dec 16, 2025

Wouldn't this concept of isolation also be useful for creating suspense boundaries around hooks?

const useSuspenseFoo = (): Foo => React.use(fooPromise)

const ComponentThatDoesNotSuspend = () => {
    const maybeFoo: Foo | undefined = React.useIsolation(() => useSuspenseFoo())
}

@Ayc0
Copy link
Author

Ayc0 commented Dec 20, 2025

Wouldn't this concept of isolation also be useful for creating suspense boundaries around hooks?

const useSuspenseFoo = (): Foo => React.use(fooPromise)

const ComponentThatDoesNotSuspend = () => {
    const maybeFoo: Foo | undefined = React.useIsolation(() => useSuspenseFoo())
}

I don't know, not fully sure 🤔
Suspense boundaries need to have fallbacks. Maybe if an API like what Preact does with useErrorBoundary could work here?

Like another useSuspense() (that internally could use useIsolation() indeed), like:

const [value, isSuspensed] = useSuspense(() => useSuspenseFoo())

But I'd move this to another RFC

@Ayc0
Copy link
Author

Ayc0 commented Dec 20, 2025

That said, based on the React's team direction, this RFC is highly certain to never exist.

I thought a lot about this RFC and I still think it could be awesome as:

But, based on how the compiler auto optimizes things, and based on the fact we can now do use(Context), if someone would use the compiler and do something like:

const Foo = () => {
  const foo = React.useContext(CustomHeavyContext).foo;
};

or

const Bar = () => {
  const bar = useSearchParams().get("bar");
};

then the variables foo & bar would be memoized deeply. And even if Foo & Bar would auto-re-render when any of the context / search params would change, no other variable using foo nor bar would change, and neither would children of Foo & Bar. So maybe we don't need this RFC anymore with the compiler.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants