reactjs/rfcs

[Feature Request]: useRenderEffect - hook that is called just after rendering function returned

pie6k opened this issue · 2 comments

pie6k commented

In some cases, it is very useful to know when rendering of some component has finished. By rendering in this context I mean exact moment when render function has returned something.

Such case could be libraries like mobx.

In mobx, if you use some store, mobx is watching what exact 'parts' of the store were used during the render so it knows when it could be required to re-render.

However, (as far as I understand) due to lack of possibility to know when rendering function call finished, it is not possible to use mobx hooks as you would with any other hook.

Instead, what you have to do is (https://mobx-react.js.org/observer-hook):

function Person() {
  const person = useLocalStore(() => ({ name: 'John' }))
  return useObserver(() => (
    <div>
      {person.name}
      <button onClick={() => (person.name = 'Mike')}>No! I am Mike</button>
    </div>
  ))
}

Which requires you to pass function returning jsx node so mobx knows exactly when it can stop watching for parts of store you're using for this specific node in the react tree.

With hook like useRenderEffect something like this would be possible:

function useStore() {
  const stopWatching = startWatching();

  const observableStore = 'foo';

  useRenderEffect(() => {
    stopWatching();
  });

  return observableStore;
}

This cannot be solved with useLayoutEffect as many other render functions might be called before such effect is called.

It would be safe in concurrent mode, as render effect would be called every time render function returned, even if result of given render will never be used.

Possible problems:

If rendered element is returning other nodes like <SomeComponent /> rendering of SomeComponent might actually happen not just after main component render function.

Possible solution:

useRenderEffect(() => {
  /**
   * This code would be called instantly (sync) when:
   * This hook is used
   * Before any render function of child node of current node is called
   */
  return () => {
    /**
     * This code is called after render function of current component has finished
     * As well as any render function of child component caused by this component has returned
     */
  }
})

I'm not sure, however - if it's needed. If it's needed, then the code of such effect could be called very frequently, which might be not so good.

In such case, maybe something like:

/**
 * would return some sort of key-path to current render.
 * 
 * As it's path - anywhere below (in children rendering) paths of all parents would be present
 * This would allow store to know which parent caused such rendering
 */
const currentKey = useCurrentKey(); 

useRenderEffect(() => {
  /**
   * This code would be called after full render of given node and all children renders has returned
   */
}, [currentKey])

would be helpful

Hi, thanks for your suggestion. RFCs should be submitted as pull requests, not issues. I will close this issue but feel free to resubmit in the PR format.

function useRenderEffect(effect: EffectCallback, deps: DependencyList) {
    const state = useRef<{ cleanup: ReturnType<EffectCallback>; deps: DependencyList }>();
    const prev = state.current;
    if (prev && deps.length === prev.deps.length && deps.every((value, index) => value === prev.deps[index])) {
        return;
    }
    state.current = { cleanup: effect(), deps };
}