Skip to content

Commit 6875e13

Browse files
committed
feat: 🎸 add useUpsert
1 parent 797cdea commit 6875e13

File tree

2 files changed

+111
-0
lines changed

2 files changed

+111
-0
lines changed

src/__tests__/useUpsert.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { act, renderHook } from '@testing-library/react-hooks';
2+
import useUpsert from '../useUpsert';
3+
4+
interface TestItem {
5+
id: string;
6+
text: string;
7+
}
8+
9+
const testItems: TestItem[] = [{ id: '1', text: '1' }, { id: '2', text: '2' }];
10+
11+
const itemsAreEqual = (a: TestItem, b: TestItem) => {
12+
return a.id === b.id;
13+
};
14+
15+
const setUp = (initialList: TestItem[] = []) => renderHook(() => useUpsert<TestItem>(itemsAreEqual, initialList));
16+
17+
describe('useUpsert', () => {
18+
describe('initialization', () => {
19+
const { result } = setUp(testItems);
20+
const [list, utils] = result.current;
21+
22+
it('properly initiates the list content', () => {
23+
expect(list).toEqual(testItems);
24+
});
25+
26+
it('returns an upsert function', () => {
27+
expect(utils.upsert).toBeInstanceOf(Function);
28+
});
29+
});
30+
31+
describe('upserting a new item', () => {
32+
const { result } = setUp(testItems);
33+
const [, utils] = result.current;
34+
35+
const newItem: TestItem = {
36+
id: '3',
37+
text: '3',
38+
};
39+
act(() => {
40+
utils.upsert(newItem);
41+
});
42+
43+
it('inserts a new item', () => {
44+
expect(result.current[0]).toContain(newItem);
45+
});
46+
it('works immutably', () => {
47+
expect(result.current[0]).not.toBe(testItems);
48+
});
49+
});
50+
51+
describe('upserting an existing item', () => {
52+
const { result } = setUp(testItems);
53+
const [, utils] = result.current;
54+
55+
const newItem: TestItem = {
56+
id: '2',
57+
text: '4',
58+
};
59+
act(() => {
60+
utils.upsert(newItem);
61+
});
62+
const updatedList = result.current[0];
63+
64+
it('has the same length', () => {
65+
expect(updatedList).toHaveLength(testItems.length);
66+
});
67+
it('updates the item', () => {
68+
expect(updatedList).toContain(newItem);
69+
});
70+
it('works immutably', () => {
71+
expect(updatedList).not.toBe(testItems);
72+
});
73+
});
74+
});

src/useUpsert.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import useList, { Actions as ListActions } from './useList';
2+
3+
export interface Actions<T> extends ListActions<T> {
4+
upsert: (item: T) => void;
5+
}
6+
7+
const useUpsert = <T>(
8+
itemsAreTheSame: (upsertedItem: T, existingItem: T) => boolean,
9+
initialList: T[] = []
10+
): [T[], Actions<T>] => {
11+
const [items, actions] = useList(initialList);
12+
13+
const upsert = (upsertedItem: T) => {
14+
const itemAlreadyExists = items.find(item => itemsAreTheSame(upsertedItem, item));
15+
if (itemAlreadyExists) {
16+
return actions.set(
17+
items.map(existingItem => {
18+
if (itemsAreTheSame(upsertedItem, existingItem)) {
19+
return upsertedItem;
20+
}
21+
return existingItem;
22+
})
23+
);
24+
}
25+
return actions.push(upsertedItem);
26+
};
27+
28+
return [
29+
items,
30+
{
31+
...actions,
32+
upsert,
33+
},
34+
];
35+
};
36+
37+
export default useUpsert;

0 commit comments

Comments
 (0)