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

misterjohannesson commented Sep 8, 2019

Here is my default React Reducer for when using function components, an array state and the useReducer hook in typescript.

The way to use it is quite simply, imagine having this type

type Item = {
  id: string;
  completed: boolean;
}

Then inside the functional components having an array of these Items is easy

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

...

const addItem = useCallback((item: Item) => {
  dispatchItems({
    type: ListReducerActionType.Add,
    data: item //The only possible type for this is Item | Item[]
  }
}, [])

My only gripe with this (and something I will try to study and figure out how to resolve) is that when using the Remove action the type
of the data isn't just the type of the key from the parameter, rather it is the types of any of the keys.
For example

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

Here data can also be the type of the other key completed
I am sure there is a trick to it - just havent gotten that deep in the advance Typescript... yet.

@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