dai-shi / react-hooks-global-state

[NOT MAINTAINED] Simple global state for React with Hooks API without Context API

Home Page:https://www.npmjs.com/package/react-hooks-global-state

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Great work! And some comments/tips

jarlah opened this issue · comments

I really like how almost identical it is to redux, and how it enables me early access to some of the things that might trickle down into react-redux. I don't like however that initial state is required.

I use typesafe-actions for mostly everything. And I use initial state in my reducers and have never depended on providing initial state on the top level where I combine the reducers. I mean, how silly is that? Therefore I have to do the following to be compliant with this library, and its really not a big deal:

const initialState = reducers(undefined, { type: undefined });

export const { GlobalStateProvider, dispatch, useGlobalState } = createStore(
    reducers,
    initialState,
    enhancers
);

Isn't there a possibility that this library could do this automatically when it doesn't receive initial state?

otherwise, I am going to do some tests now where I confirm the fact that needs to just work: sibling components that uses useGlobalState should not be affected by unrelated updates to the global state. I am sure that it works, but I haven't got the time to make this test yet. By opening react devtools and highlighting updates, this is very easy to check, but I need to just hack up some components.

@dai-shi about the optional initialState, i saw you reverted the initialState?: S fix which in effect makes it impossible to pass in undefined as initialState. So I cant use 0.8.0, with support for optional initialState, before that is fixed.. (if not using ts-ignore)

What is the type definition of your reducer?
Is it like const reducer = (state: State | undefined, action: Action) => { ?

my reducer is like
https://github.com/freeacs/freeacs-web/blob/master/src/modules/search.ts#L21
and my store is like this now:
https://github.com/freeacs/freeacs-web/blob/master/src/state/store.ts#L14

the project is just a playground atm, trying this framework for a new project

I see...

e554df6 How does this work for you?
I can't make it an optional parameter like initialState?:, but | undefined seems to work.

For some reason it didnt work. I see why you think it should work, but typescript is a dumbass sometimes. I manipulated the file locally, and got it working with this:

export type CreateStore = <S, A>(
    reducer: Reducer<S, A>,
    initialState: S | undefined,
    enhancer?: Enhancer<S, A> | AnyEnhancer,
) => Store<S, A>;

export const createGlobalState: CreateGlobalState;
export const createStore: CreateStore;

export type CreateStore = <S, A>(
    reducer: Reducer<S, A>,
    initialState: S | undefined,
    enhancer?: Enhancer<S, A> | AnyEnhancer,
) => Store<S, A>;

However, this is not a correct type. reducer must accept undefined for the first argument.

ah yes, I see now. i will take a look at it myself too now

If it's a typescript issue, can you try this?

export const { GlobalStateProvider, dispatch, useGlobalState } =
    (createStore as CreateReduxLikeStore<RootState, RootActions>)(reducers, undefined as any, enhancers);

(I think undefined as any can be just undefined.)

got it working with this:

export const { GlobalStateProvider, dispatch, useGlobalState } = (createStore as CreateReduxLikeStore)<
  RootState,
  RootActions
>(reducers, undefined, enhancers);

e.g. no types to CreateReduxLikeStore.. and it works as expected.

i am willing to use this hack, its only one place in every codebase. and if you want initialstate required by default, then we can have this special type as an expert alternative? what do you think? should we try to remove cast?

btw i cant set initialState in reducer to XXX | undefined, it pains me to do that in every reducer. So i didnt do that. but it works without that now with the cast above.

Ooh ... and i have confirmed btw that the library does what its supposed to :) made two sibling components with different substate subscriptions. and they do logically not cause the other component to update when dispatching an action that should only affect the one. just needed to test it, since its a basic assumption/requirement.

@dai-shi ready for a bit more complicated challenge? if i type the store correctly my dispatch have Dispatch<RootActions> .. if i add a RootThunkAction to RootActions typescript fails to compile naturally, because dispatch is Dispatch<A> where A extends AnyAction, which has a type. Thunks have no type ;) i wonder, maybe i should make a custom middleware that allows me send in { type: 'yada', thunk: () => void }
meh, using dispatch(search(term) as any);. It works. Its not too hacky.

I have added a PR with a suggestion on how to fix the typescript problems

#10

btw i cant set initialState in reducer to XXX | undefined, it pains me to do that in every reducer. So i didnt do that. but it works without that now with the cast above.

How do you like this pattern? You don't have to change your reducer.

export const { GlobalStateProvider, dispatch, useGlobalState } = createStore(
  reducers as ReduxLikeReducer<RootState, RootActions>,
  undefined,
  enhancers,
);

didnt work, but ill use function cast :) 👍 If you can release the current state I am totally happy.

But should we/you document this? The option to cast the createStore function?

Oh, and btw. Redux Thunk is totally useless with hooks. If you have a use case for a thunk, you have a use case for a custom hook. So I made myself a reusable custom hook that replaces the need for a thunk:

export function useSearch(): UseSearchProps {
  const [{ hits, term, error, loading }] = useGlobalState('search');

  useEffect(() => {
    if (typeof term === 'undefined') {
      return;
    }
    dispatch(SearchActions.search.request());
    apiCall('POST', '/search', { term })
      .then(
        json =>
          UnitArray.decode(json).bimap(
            e => dispatch(SearchActions.search.failure(e)),
            r => dispatch(SearchActions.search.success(r))
          ),
        e => dispatch(SearchActions.search.failure(e))
      )
      .catch(e => dispatch(SearchActions.search.failure(e)));
  }, [term]);

  const setTerm = useCallback(
    (term: string) => dispatch(SearchActions.setTerm(term)),
    []
  );

  return { hits, error, term, setTerm, loading };
}

so problems with dispatch(searchThunk(term) as any) is not relevant for me any more. Its impossible add a thunk type to RootActions because a thunk doesn't have type. So, out of the window with thunks :P and welcome to hooks ;)

@jarlah Would you like to write a wiki about the optional initState? I'd appreciate for your help. https://github.com/dai-shi/react-hooks-global-state/wiki/Optional-initialState

As for redux-thunk, I'd like to support it with this library. Have you looked into the example? https://github.com/dai-shi/react-hooks-global-state/tree/master/examples/08_thunk

@dai-shi it is definitely supported. But i didnt check this example. I just casted the thunk as any as a temp hack.

However. In the recent discussions you have been leaning strongly on type correctness. So my inevitable suggestion to provide another function from the store wont go home. So what i would have done instead of doing this cast deep down in components is to override the dispatch before i return from the store. Like

const {..., dispatch: Dispatch<RootActions | (d: Dispatch<RootActions>, getState: () => RootState) => void)} =.... 

However. We need also to support getState fn as second argument to be fully compliant. So i added that above

@dai-shi i will update the wiki if i am allowed to :)
(i am. Great ;))

Can you do something like this for thunk?

createStore<State, Action | (d: Dispatch<Action>, getState: () => State) => void)>(...);

Can you do something like this for thunk?

createStore<State, Action | (d: Dispatch<Action>, getState: () => State) => void)>(...);

given:

type ThunkAction = (d: Dispatch<RootActions>, getState: () => RootState) => void
export const { GlobalStateProvider, dispatch, useGlobalState } = createStore<
  RootState,
  RootActions | ThunkAction
>(reducers, undefined as any /* temp hack before new release */, enhancers);

I get compile error, because ThunkAction is not having a type, naturally:

Error:(18, 3) TS2345: Argument of type 'Reducer<{ search: SearchState; about: AboutState; }, AnyAction>' is not assignable to parameter of type 'Reducer<{ search: SearchState; about: AboutState; }, PayloadAction<"SEARCH_REQUEST", void> | PayloadAction<"SEARCH_SUCCESS", { profile: { id: number; name: string; }; unitId: string; unitType: { id: number; name: string; }; }[]> | PayloadAction<...> | PayloadAction<...> | PayloadAction<...> | ThunkAction>'.
  Types of parameters 'action' and 'action' are incompatible.
    Type 'PayloadAction<"SEARCH_REQUEST", void> | PayloadAction<"SEARCH_SUCCESS", { profile: { id: number; name: string; }; unitId: string; unitType: { id: number; name: string; }; }[]> | PayloadAction<"SEARCH_FAILURE", Errors | Error> | PayloadAction<...> | PayloadAction<...> | ThunkAction' is not assignable to type 'AnyAction'.
      Property 'type' is missing in type 'ThunkAction' but required in type 'AnyAction'.

it doesn't help to say:

type Thunk = RootActions | ((d: Dispatch<RootActions>) => void);

or similar (and the first RootActions | is superfluous.).

Almost same error stack. Type does not exist on ThunkAction, as required by AnyAction base type or something. This COULD however be a dependency issue, that I am using another type implicitly.

I looked at a redux issue, but didn't find any help there that could solve this issue.

reduxjs/redux-thunk#103

@dai-shi wiki done as best I could. I guess it can be improved :)

Property 'type' is missing in type 'ThunkAction' but required in type 'AnyAction'.

Ahhh, I see. Yeah, for another reason, I've wanted to redux to have AnyAction=any...
I don't have other solutions at the moment. There should be a better way, I hope.

Thanks for the wiki. I guess it's good as a start.

It works btw. V0.9.0. I am shifting focus to just using it now. Only dependency on redux now is combineReducers. Feel free to close this issue.

Awesome!

type CreateStoreOverloaded = {
<S, A>(
reducer: Reducer<S, A>,
initialState: S,
enhancer?: Enhancer<S, A> | AnyEnhancer,
): Store<S, A>;
<S, A>(
reducer: ReduxLikeReducer<S, A>,
initialState: S | undefined,
enhancer?: Enhancer<S, A> | AnyEnhancer,
): Store<S, A>;
};
export const createGlobalState: CreateGlobalState;
export const createStore: CreateStoreOverloaded;

This is probably better typing with function overload.