1

I'm trying to build a generic Store using React's useReducer and useContext but I'm having an issue with the inference of the default state.

The store generator function is the following:

export function generateStore<Actions extends ReducerAction, State = any>(defaultValue: State, reducer: (state: State, action: Actions) => State): {
  provider: (props: { children: ReactNode }) => ReactElement;
  dispatcher: (action: Actions) => void;
  useStore: () => State;
} {
  const store = createContext(defaultValue);
  const { Provider } = store;

  let dispatch: React.Dispatch<Actions>;

  const ProviderElm = (props: { children: ReactNode }): ReactElement => {
    const { children } = props;
    const [state, dispatcher] = useReducer(reducer, defaultValue);
    dispatch = dispatcher;
    return <Provider value={state}>{children}</Provider>;
  };

  return {
    provider: ProviderElm,
    dispatcher: (action: Actions) => dispatch && dispatch(action),
    useStore: () => useContext(store),
  };
}

An initializer example could be:

const defaultState = {
  auth: {
    authenticated: false,
  },
};

type StoreActions =
  | {
      type: 'LOGIN';
      payload: {
        token: string;
      };
    }
  | {
      type: 'LOGOUT';
    };

const { dispatcher, provider, useStore } = generateStore<StoreActions>(
  defaultState,
  (state = defaultState, action) => {
    switch (action.type) {
      case 'LOGIN': {
        const { token } = action.payload;
        return {
          ...state,
          auth: {
            authenticated: true,
            token,
          },
        };
      }
      case 'LOGOUT': {
        return {
          ...state,
          auth: {
            authenticated: false,
            token: null,
          },
        };
      }

      default:
        return defaultState;
    }
  },
);

The issue is that the State generic of generateStore can't infer itself as the typeof the parameter defaultValue.

It always requires me to initialize it like this or else the intellisense won't work out the type: generateStore<StoreActions, typeof defaultState>

Any idea on how I make this work and why it currently can't infer the type?

1 Answer 1

2

If you want TypeScript to infer your generic types. You cannot provide any type arguments to the function. TypeScript does not support partial type inference. It's all or nothing. By calling generateStore<StoreActions> you are triggering the compiler to use the predefined State = any generic argument on your function.

I would recommend having a strongly typed state to make it cleaner.

type State = {
  auth: {
    authenticated: boolean
  }
}

type StoreActions =
  | {
    type: 'LOGIN';
    payload: {
      token: string;
    };
  }
  | {
    type: 'LOGOUT';
  };

const defaultState: State = {
  auth: {
    authenticated: false,
  },
};

const { dispatcher, provider, useStore } = generateStore<StoreActions, State>(
  defaultState,
  (state = defaultState, action) => {
    switch (action.type) {
      case 'LOGIN': {
        const { token } = action.payload;
        return {
          ...state,
          auth: {
            authenticated: true,
            token,
          },
        };
      }
      case 'LOGOUT': {
        return {
          ...state,
          auth: {
            authenticated: false,
            token: null,
          },
        };
      }

      default:
        return defaultState;
    }
  },
);

The only other option is to create a wrapper function that only needs one argument to infer (the state) and supplies the actions type directly. You'll need one for each set of actions, but it might be a good work around depending on how many times it will be used.

type StoreActions =
  | {
    type: 'LOGIN';
    payload: {
      token: string;
    };
  }
  | {
    type: 'LOGOUT';
  };

const defaultState = {
  auth: {
    authenticated: false,
  },
};

export function generateStoreWithStoreActions<State = any>(defaultValue: State, reducer: (state: State, action: StoreActions) => State) {
  return generateStore<StoreActions, State>(defaultValue, reducer);
}

const { dispatcher, provider, useStore } = generateStoreWithStoreActions(
  defaultState,
  (state = defaultState, action) => {
    switch (action.type) {
      case 'LOGIN': {
        const { token } = action.payload;
        return {
          ...state,
          auth: {
            authenticated: true,
            token,
          },
        };
      }
      case 'LOGOUT': {
        return {
          ...state,
          auth: {
            authenticated: false,
            token: null,
          },
        };
      }

      default:
        return defaultState;
    }
  },
);
Sign up to request clarification or add additional context in comments.

1 Comment

I see what you mean. Perfect explanation. Shame tho. Either way I still have a workaround. Thank you

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.