Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
avkonst opened this issue Dec 18, 2022 · 13 comments · May be fixed by #409
Open

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

avkonst opened this issue Dec 18, 2022 · 13 comments · May be fixed by #409
Labels
enhancement New feature or request hookstate-5 Features postponed for v5

Comments

@avkonst
Copy link
Owner

avkonst commented Dec 18, 2022

Upvote this if you are interested in this feature.

@avkonst avkonst changed the title Prevent HOOKSTATE-111 during Vite and NextJS HMR Prevent HOOKSTATE-111 exception being raised during Vite and NextJS HMR Dec 18, 2022
@avkonst avkonst added hookstate-5 Features postponed for v5 enhancement New feature or request labels Dec 18, 2022
@qundus
Copy link

qundus commented Dec 21, 2022

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

@ed6767
Copy link

ed6767 commented Feb 9, 2023

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

@Squix
Copy link

Squix commented Feb 10, 2023

Same with Expo/React-Native HMR

@speigg
Copy link
Contributor

speigg commented Feb 23, 2023

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.

@avkonst
Copy link
Owner Author

avkonst commented Mar 5, 2023

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

@apperside
Copy link

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?

@solidsnail
Copy link

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;
};

@apperside
Copy link

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

@solidsnail
Copy link

solidsnail commented Jun 13, 2023

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

@speigg
Copy link
Contributor

speigg commented Apr 18, 2024

@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?

@avkonst
Copy link
Owner Author

avkonst commented Apr 23, 2024

@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.

@speigg
Copy link
Contributor

speigg commented May 16, 2024

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

#409

@speigg speigg linked a pull request May 16, 2024 that will close this issue
@jpodpro
Copy link

jpodpro commented May 23, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request hookstate-5 Features postponed for v5
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants