Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature request: createActions utility #119

Open
twinstae opened this issue Aug 24, 2022 · 2 comments
Open

feature request: createActions utility #119

twinstae opened this issue Aug 24, 2022 · 2 comments

Comments

@twinstae
Copy link

twinstae commented Aug 24, 2022

TL:DR

I made createActions utility like redux-toolkit's createSlice. TypeScript ready with Type Inference.

Problem

It takes a long time to create multiple actions.

There are many boilerplates now. Like next TodoList example.

export const todoListStore = atom<TodoT[]>([])

export const actions = {
  addTodo: action(todoListStore, 'addTodo', (store, content: string) => {
    const newTodo = {
      id: generateId(),
      content,
      isCompleted: false,
    }
    store.set([...store.get(), newTodo])
  }),
  completeTodo: action(todoListStore, 'completeTodo', (store, id: number, isCompleted: boolean) => {
    const todoList = store.get();
    store.set(todoList.map((todo) =>
      todo.id === id ? { ...todo, isCompleted } : todo
    ));
  }),
  changeTodo: action(todoListStore, 'changeTodo', (store,id: number, content: string) => {
    const todoList = store.get();
    store.set(todoList.map((todo) => (todo.id === id ? { ...todo, content } : todo)));
  }),
  deleteTodo: action(todoListStore, 'deleteTodo', (store, id: number) => {
    store.set(store.get().filter((todo) => todo.id !== id)) 
  })
}

Did you find the duplicated pattern here?

{
  [name]: action(store, name, (store, ...args) => {
    const oldState = store.get();
    ...
    store.set(newState)
  })
}

Solution

So I made createActions utility like redux-toolkit's createSlice but simple.

Here it is:

export const todoListStore = atom<TodoT[]>([]);

// use createActions
export const actions = createActions(todoListStore,  {
  // just pure old javascript function
  addTodo(old, content: string) {
    if (content.length === 0) return old;
    
    const newTodo = {
      id: generateId(),
      content,
      isCompleted: false,
    };
    return [...old, newTodo]; // return next state
  },
  completeTodo(old, id: TodoT["id"], isCompleted: boolean) {
    return old.map((todo) =>
      todo.id === id ? { ...todo, isCompleted } : todo
    );
  },
  changeTodo(old, id: TodoT["id"], content: string) {
    return old.map((todo) => (todo.id === id ? { ...todo, content } : todo));
  },
  deleteTodo(old, id: TodoT["id"]) {
    return old.filter((todo) => todo.id !== id);
  },
});

It is Typescript ready, and can inference types well. Thanks to @XiNiha

image

Implementation

It is simple, but just a proof of concept.

// XiNiHa's work
type AsAction<T, I extends Record<string, (old: T, ...args: any[]) => T>> = {
  [K in keyof I]: I[K] extends (old: T, ...args: infer A) => T ? (...args: A) => void : never
}

function createActions<
  T,
  I extends Record<string, (old: T, ...args: any) => T>
>(store: WritableStore<T>, rawActions: I): AsAction<T, I> {
  return Object.fromEntries(
    Object.entries(rawActions).map(([name, rawAction]) => {
      return [
        name,
        action(store, name, (store, ...args) => {
          const old = store.get();
          store.set(rawAction(old, ...args));
        }),
      ];
    })
  ) as AsAction<T, I>;
}

It can be imperative, but encapsulate effect in the function...

function createActions<
  T,
  I extends Record<string, (old: T, ...args: any) => T>
>(store: WritableStore<T>, rawActions: I): AsAction<T, I> {
  const result: any = {};
  for (const name in rawActions){
    result[name] = action(store, name, (store, ...args) => {
      const old = store.get();
      store.set(rawActions[name](old, ...args));
    });
  }
  return result as AsAction<T, I>;
}

Limitation

  • We can't use store's methods like setKey and notify.
  • I don't know it can support Map and MapTempltate
  • It can't handle async actions. (now)
  • It is just tested on todoMVC. More tests are needed.
@ai
Copy link
Member

ai commented Aug 24, 2022

For your case it is a good solution.

But I want to wait a little before adding it to the core to have more use cases.

@twinstae
Copy link
Author

twinstae commented Aug 24, 2022

Thank you for your comment. :) I understand. nanostore has tiny (266 B) core...

People can use the utility, if they want. Just copy and paste the code.

I also created alternative API and want to share. It just wrap store.

Example

export const todoListStore = atom<TodoT[]>([]);

export const actions = createActions(todoListStore, {
  addTodo(store, content: string) {
    if (content.length === 0) return;
    
    const newTodo = {
      id: generateId(),
      content,
      isCompleted: false,
    };
    store.set([...store.get(), newTodo]);
  },
  completeTodo(store, id: TodoT["id"], isCompleted: boolean) {
    store.set(store.get().map((todo) =>
      todo.id === id ? { ...todo, isCompleted } : todo
    ));
  },
  changeTodo(store, id: TodoT["id"], content: string) {
    store.set(store.get().map((todo) => (todo.id === id ? { ...todo, content } : todo)));
  },
  deleteTodo(store, id: TodoT["id"]) {
    store.set(store.get().filter((todo) => todo.id !== id));
  },
});

implementation

export type WrapAction<SomeStore extends WritableStore, I extends Record<string, (store: SomeStore, ...args: any[]) => void>> = {
  [K in keyof I]: I[K] extends (store: SomeStore, ...args: infer A) => void ? (...args: A) => void : never
}

function createActions<
  SomeStore extends WritableStore,
  I extends Record<string, (store: SomeStore, ...args: any[]) => void>
>(store: SomeStore, rawActions: I): WrapAction<SomeStore, I> {
  const result: any = {};
  for (const name in rawActions){
    result[name] = action(store, name, rawActions[name]);
  }
  return result as WrapAction<SomeStore, I>;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants