Home > OS >  useState polyfill, is this correct?
useState polyfill, is this correct?

Time:04-10

Trying to write a custom implementation of useState. Let's say only for a single value.

function useMyState(initVal){
  const obj = {
    value: initVal,
    get stateValGet() {
      return this.value
    },
    set stateValSet(val) {
      this.value = val
    }
  };
  const setVal = (val) => {
    obj.stateValSet = val
  }
  return [obj.stateValGet, setVal]
}

Doesn't seem to work though, can anyone tell why? Unable to crack this.

It returns this [, <function_setter>] So if you try to run this setVal method, it does trigger the setter. But getter never gets called upon the updation.

CodePudding user response:

useState's functionality can't really be polyfilled or substituted with your own custom implementation, because it not only stores state, but it also triggers a component re-render when the state setter is called. Triggering such a re-render is only possible with access to React internals, which the surface API available to us doesn't have access to.

useState can't be replaced with your own implementation unless that implementation also uses useState itself in order to get the component it's used in to re-render when the state setter is called.

You could create your own custom implementation outside of React, though, one which simulates a re-render by calling a function again when the state setter is called.

const render = () => {
  console.log('rendering');
  const [value, setValue] = useMyState(0);
  document.querySelector('.root').textContent = value;
  const button = document.querySelector('.root')
    .appendChild(document.createElement('button'));
  button.addEventListener('click', () => setValue(value   1));
  button.textContent = 'increment';
};
const useMyState = (() => {
  let mounted = false;
  let currentState;
  return (initialValue) => {
    if (!mounted) {
      mounted = true;
      currentState = initialValue;
    }
    return [
      currentState,
      (newState) => {
        currentState = newState;
        render();
      }
    ];
  };
})();
render();
<div ></div>

CodePudding user response:

Every state manager that wants to interact with React has to find a way to connect to React lifecycle, in order to be able to trigger re-renders on state change. useState hook internally uses useReducer : https://github.com/facebook/react/blob/16.8.6/packages/react-dom/src/server/ReactPartialRendererHooks.js#L254

That's why I made this naive implementation of useState based on JS Proxies and a useReducer dummy dispatch just to force a re-render when state changes.

It's naive, but that's what valtio is based on.

Consider that the power of proxies would make it possible to trigger re-renders by mutating state directly, that's what happens in valtio !

import { useReducer, useCallback, useMemo } from 'react';

export const useMyState = (_state) => {
  // FORCE RERENDER
  const [, rerender] = useReducer(() => ({}));
  const forceUpdate = useCallback(() => rerender({}), []);

  // INITIALIZE STATE AS A MEMOIZED PROXY
  const { proxy, set } = useMemo(() => {
    const target = {
      state: _state,
    };
    // Place a trap on setter, to trigger a component rerender
    const handler = {
      set(target, prop, value) {
        console.log('SETTING', target, prop, value);
        target[prop] = value;
        forceUpdate();
        return true;
      },
    };

    const proxy = new Proxy(target, handler);

    const set = (d) => {
      const value = typeof d === 'function' ? d(proxy.state) : d;
      if (value !== proxy.state) proxy.state = value;
    };

    return { proxy, set };
  }, []);

  return [proxy.state, set];
};

Demo https://stackblitz.com/edit/react-33fpbk?file=src/App.js

  • Related