hookstate icon indicating copy to clipboard operation
hookstate copied to clipboard

Prevent HOOKSTATE-111 exception being raised during Vite and NextJS HMR

Open avkonst opened this issue 3 years ago • 13 comments

Upvote this if you are interested in this feature.

avkonst avatar Dec 18 '22 23:12 avkonst

hi, this gets really annoying when combining global states with local ones, i can't figure out any workarounds at the moment, you got any @avkonst ?

also, please explain when exactly does the issue occur, is it when i use a global state hookstate within a useHookstate and then reinitialize the hookstate, or is it when i reinitialize the useHookstate?

i'm using vite

UPDATE: the issue occurs when using useHookstate and hookstate within the same function, when parent component rerenders it reinitializes hookstate states which causes the issue. so as a workaround i did this:

// global map of key and state
import { hookstate } from '@hookstate/core';
const global_store: Map<any, State<any, any>> = new Map();
export function get_store(key: any) {
    let store = global_store.get(key);
    if (!store) {
        store = hookstate(values);
        global_store.set(key, store);
    }
   return store;
}
export function delete_store(key: any) {
    global_store.delete(key)
}

// in component
import { useHookstate } from '@hookstate/core';
import { get_store, delete_Store } from "./global_store"
import { useEffect } from 'react';

export function Login() {
   const store = useHookstate(get_store("my_store"))

   useEffect(() => {
      return () => delete_store("my_store")
   }, [])

  return (
      <>
      <input onChange={(e: any) => store.set(e.target.value)} />
       <p>{JSON.stringify(store.get())}</p>
      </>
    )
}

now i have to find a way to prevent duplicate keys and uuid is not an option, hope this gets fixed soon <3

qundus avatar Dec 21 '22 23:12 qundus

yep, had this issue with next.js and vite HMR

ed6767 avatar Feb 09 '23 18:02 ed6767

Same with Expo/React-Native HMR

Squix avatar Feb 10 '23 15:02 Squix

This is generally an issue whenever the state you want to use depends on another state. E.g.,


const keyState = hookstate('key')
const stateMap = new Map<string, State>()

const MyComponent = () => {
  const currentKey = useHookstate(keyState)
  const state = useHookstate(stateMap.get(currentKey))) // HOOKSTATE-111 exception thrown when keyState changes
  return null
}

My current workaround is to use a keyed child component:


const keyState = hookstate('key')
const stateMap = new Map<string, State>()

const MyComponent = () => {
  const currentKey = useHookstate(keyState)
  return <MyChildComponent currentKey={currentKey} key={currentKey}/> // note: key is necessary
}

const MyChildComponent = ({currentKey}) => {
  const state = useHookstate(stateMap.get(currentKey))) // no more HOOKSTATE-111 exception
}

A fix for this would be greatly appreciated.

speigg avatar Feb 23 '23 06:02 speigg

The fix for the issue described intially is quite hard. I have not explored how to accomplish it. Interestlngly it did not happen with react-scripts... new with Vite

avkonst avatar Mar 05 '23 01:03 avkonst

Any news on this? I have this situation


const productEditorBottomSheetGlobalState = hookstate<{
  isOpen?: boolean
  data?: Partial<ProductsDto> | undefined
}>(
  { isOpen: false, data: undefined },
  extend(extensions('productEditorBottomSheet'))
)

export function useProductFormBottomSheet() {
  const state = useHookstate(
    productEditorBottomSheetGlobalState,
    extensions('productEditorBottomSheetHook')
  )

  const showBottomSheet = useEvent(() => {
    state.merge({ data: none, isOpen: true })
  })
  const close = useEvent(() => {
    state.merge({ isOpen: false })
  })
  const isOpen = state.isOpen?.value

  const editProduct = useEvent((data: Partial<Partial<ProductsDto>>) => {
    state.merge({ isOpen: true, data })
  })
  return useMemo(() => {
    return {
      data: state.data,
      showBottomSheet,
      close,
      isOpen,
      editProduct,
    }
  }, [close, editProduct, isOpen, showBottomSheet, state.data])
}

and then I use useProductFormBottomSheet where I need.

When I edit some code and the page refreshes, I always get this error

Screenshot 2023-06-12 at 10 40 59

How can I fix this?

apperside avatar Jun 12 '23 08:06 apperside

Here is an easy fix to this issue:

import { hookstate, useHookstate } from "@hookstate/core";
import { useEffect } from "react";

const defaultState = {
  count: 0,
};
const globalState = hookstate(defaultState);
export const useStore = () => {
  const state = useHookstate(defaultState);
  const stateVal = globalState.get({ noproxy: true });
  useEffect(() => {
    state.set(stateVal);
  }, [stateVal]);
  return state;
};

solidsnail avatar Jun 12 '23 19:06 solidsnail

@solidsnail can you explain your solution please? By a quick reading of the code, it sounds like causing a lot of re-renders..

apperside avatar Jun 13 '23 10:06 apperside

The crash occurs when you pass a hookstate to another hookstate as defaultValue, to avoid this you can create a "copycat" state and grab the real state value using noproxy: true which stops the code from listening to changes, and instead we use the good old useEffect to listen to its changes and update the "copycat" state.

const state = useHookstate(
    productEditorBottomSheetGlobalState, // <=== This is a real proxy state which should not be passed as defaultValue
    extensions('productEditorBottomSheetHook')
  )

@apperside Here you are hooking two states together which causes the crash when the HMR tries to reconciliate the changes

solidsnail avatar Jun 13 '23 12:06 solidsnail

@avkonst I keep running into scenarios where it would be incredibly helpful if it were possible to dynamically change which state is being observed in useHookstate(). Wondering if you can explain what the complications are here, and if you would accept some help in implementing this?

speigg avatar Apr 18 '24 18:04 speigg

@speigg Ok. This was long time ago when I looked at it, but here is what I remember:

  • hot reload reloads JS variables only partially. It can happen in any combinations: any variable can come from old state and any variable can come from new reloaded state.
  • In particular Vite hot reload caused the reload for global variable state but kept react native local state (which hookstate uses internally). As a result a new component render happens with new global variables and old local state.
  • Easy reproducer without hot reload is to have a component binding two 2 global states: every first render uses 1st global state, every second uses the second.

Potential fix could possibly be around resetting local state when a new (non matching) global state is detected, but there are probably consequences and complications. As far as I remember, local component state (not the one used by hookstate, but other, like effects, memo, etc.) was also preserved on hot reload and it captured all of the callbacks to manage the global state (old state from the initial render). As a result, even if we make hookstate to workaround the mentioned exception, all callbacks / actions in reactions will still refer to the old global state.

This is where I stopped my investigation last year, understanding that the fix would not be easy, if even possible. And I knew that it was vite specific as react-scripts did hot reload and did not cause this problem.

The things might changed since last time I looked at it. So all of the above needs reinvestigation and confirmation.

I am happy if you look at it, confirm the problem and develop state rebind solution. The local isolated test to make working is described above.

PS: I am still maintaining and looking after the project, but significant development is a problem for me as I moved to very rapid start up 2+ years ago at very senior architect role and I barely have time now for family and sport. I appreciate if you can crack this problem. I can help with guidance. You know my email.

avkonst avatar Apr 23 '24 19:04 avkonst

@avkonst thanks for the details, that was helpful. I believe this PR fixes the issues:

https://github.com/avkonst/hookstate/pull/409

speigg avatar May 16 '24 05:05 speigg

FWIW, I'm getting this with React hot reloading. I can't say if it's an artifact of my setup or not since I barely understand the issue in the first place. But I thought I would report it anyway.

jpodpro avatar May 23 '24 00:05 jpodpro