Skip to content

Instantly share code, notes, and snippets.

@misterjohannesson
Last active February 11, 2020 09:22
Show Gist options
  • Save misterjohannesson/034128924b72a1bfbffc2d365bd1343a to your computer and use it in GitHub Desktop.
Save misterjohannesson/034128924b72a1bfbffc2d365bd1343a to your computer and use it in GitHub Desktop.
Generic List Reducer, React, Typescript
import { Reducer } from "react";
/**
* Different mutating types to be performed in the reducers dispatch.
*/
export enum ListReducerActionType {
/** Remove one or many item(s) from state */
Remove,
/** Replace one or many item(s) from state if present */
Update,
/** Append one or many item(s) to state*/
Add,
/** Replace or append one or many item(s) of state*/
AddOrUpdate,
/** Reset the whole state */
Reset
}
//The conditional type, specifying the type of data depending on the action type
type ActionDataType<T, K> = K extends ListReducerActionType.Reset
? T[]
: K extends ListReducerActionType.Remove
? T[keyof T][] | T[keyof T]
: T[] | T;
type ListReducerActionInput<T, K extends ListReducerActionType> = {
type: K;
data: ActionDataType<T, K>;
};
type AllListActions<T> =
| ListReducerActionInput<T, ListReducerActionType.Remove>
| ListReducerActionInput<T, ListReducerActionType.Update>
| ListReducerActionInput<T, ListReducerActionType.Add>
| ListReducerActionInput<T, ListReducerActionType.Reset>
| ListReducerActionInput<T, ListReducerActionType.AddOrUpdate>;
/**
*
* @typeparam `T` type of the reducer state
* @param {keyof T} key value of `U`
* @return {Reducer} React reducer for a stateful list of `T`
*
* Can be initiated like this
* `listReducer<Entity>("id")`
* Where `Entity` is the type of the list
* and `"id"` is a property key on the type
* that is to be used to find index in the list
*/
export default <T>(key: keyof T): Reducer<T[], AllListActions<T>> => (
state: T[],
action: AllListActions<T>
) => {
const replace = (t: T) => {
const index = state.findIndex(i => i[key] === t[key]);
state[index] = t;
};
switch (action.type) {
case ListReducerActionType.AddOrUpdate:
if ((action.data as T[]).push) {
console.log("what?");
} else {
const index = state.findIndex(i => i[key] === (action.data as T)[key]);
if (index !== -1) {
replace(action.data as T);
return [...state];
} else {
return [...state, action.data as T];
}
}
case ListReducerActionType.Add:
if ((action.data as T[]).push) {
return [...state, ...(action.data as T[])];
} else {
return [...state, action.data as T];
}
case ListReducerActionType.Update: {
if ((action.data as T[]).push) {
(action.data as T[]).forEach(replace);
} else {
replace(action.data as T);
}
return [...state];
}
case ListReducerActionType.Remove:
if ((action.data as T[keyof T][]).push) {
return state.filter(
t => (action.data as T[keyof T][]).indexOf(t[key]) === -1
);
} else {
return state.filter(t => t[key] !== action.data);
}
case ListReducerActionType.Reset:
return action.data as T[];
default:
return state;
}
};
@misterjohannesson
Copy link
Author

My new solution to my previous problem is still not fully optimal.
But at least there is only one possible type for when the type is Remove.

The new revision changes initialization to

const [items, dispatchItems] = useReducer(  listReducer<Item, "id">("id"),  [] );

And then the remove action looks the same but accepted types are different

const removeItems= useCallback((item: Item) => {
  dispatchItems({
    type: ListReducerActionType.Remove,
    data: item.id //The accepted types are:  string | string[]
  }
}, [])

If I can now find a way to have type "id" to also be a value then I am golden.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment