EskiMojo14 / history-adapter

A "history adapter" for managing undoable (and redoable) state changes.
https://eskimojo14.github.io/history-adapter/
MIT License
5 stars 1 forks source link

Parameterless reducer wrapped in undoableReducer always requires a parameter #13

Open Trikas77 opened 2 months ago

Trikas77 commented 2 months ago

Using your example I tried to add a reducer with no parameters, as example setToSeven When I now try to call the function via dispatch I always get:

TS2554: Expected 1 arguments, but got 0

It does work correctly if I pass any object, null or undefined. But it does not feel clean.

counterSlice.ts

import { createSlice } from "@reduxjs/toolkit";
import { createHistoryAdapter } from "history-adapter/redux";

interface CounterState {
  value: number;
}

const counterAdapter = createHistoryAdapter<CounterState>();

const { selectPresent, ...selectors } = counterAdapter.getSelectors();

const initialState = counterAdapter.getInitialState({ value: 0 });

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    setToSeven: counterAdapter.undoableReducer(
      (state) => {
        state.value = 7;
      },
    ),
  },
  selectors: {
    ...selectors,
    selectCount: (state) => selectPresent(state).value,
  },
});

export const {
  setToSeven
} = counterSlice.actions;

export const { selectCount, selectCanRedo, selectCanUndo, selectPaused } =
  counterSlice.selectors;

HistoryButtons.tsx

import { useAppDispatch } from "../../hooks";
import {
   setToSeven
} from "./counterSlice";

export function HistoryButtons() {
  const dispatch = useAppDispatch();

  return (
    <div className="card">
      <button onClick={() => dispatch(setToSeven())}>set to 7</button>
    </div>
  );
}
EskiMojo14 commented 2 months ago

Thanks for the report! I'll investigate if I can improve the inference here - in the meantime a workaround would be to annotate the action type anyway:

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    setToSeven: counterAdapter.undoableReducer(
      (state, action: PayloadAction) => {
        state.value = 7;
      },
    ),
    // or
    setToSeven: counterAdapter.undoableReducer<PayloadAction>(
      (state) => {
        state.value = 7;
      },
    ),
  },
  selectors: {
    ...selectors,
    selectCount: (state) => selectPresent(state).value,
  },
});

It also works properly with the creator callback syntax:

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: (create) => ({
    setToSeven: create.reducer(
      counterAdapter.undoableReducer((state) => {
        state.value = 7;
      }),
    ),
  }),
  selectors: {
    ...selectors,
    selectCount: (state) => selectPresent(state).value,
  },
});

Or creating it outside:

const setToSevenReducer = counterAdapter.undoableReducer(
  (state) => {
    state.value = 7;
  },
);

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    setToSeven: setToSevenReducer,
  },
  selectors: {
    ...selectors,
    selectCount: (state) => selectPresent(state).value,
  },
});
EskiMojo14 commented 2 months ago

after some investigation, I'm not sure I'll be able to resolve this in a satisfactory manner - inference is a finnicky thing, and getting something that pleases this scenario appears to break others. It appears the reducers's constraint of Record<string, CaseReducer<State, PayloadAction<any>> is causing undoableReducer to infer the action type as PayloadAction<any>.

If I have any brainwaves I'll definitely come back to this, but hopefully in the meantime one of the workarounds above will be enough for your use case.