Skip to content

Commit

Permalink
feat: add useUpsert
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich authored Aug 19, 2019
2 parents d273d99 + 440b4bc commit a7c2899
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 0 deletions.
28 changes: 28 additions & 0 deletions docs/useUpsert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# `useUpsert`

Superset of `useList`. Provides an additional method to upsert (update or insert) an element into the list.
## Usage

```jsx
import {useUpsert} from 'react-use';

const Demo = () => {
const comparisonFunction = (a: DemoType, b: DemoType) => {
return a.id === b.id;
};
const [list, { set, upsert, remove }] = useUpsert(comparisonFunction, initialItems);

return (
<div style={{ display: 'inline-flex', flexDirection: 'column' }}>
{list.map((item: DemoType, index: number) => (
<div key={item.id}>
<input value={item.text} onChange={e => upsert({ ...item, text: e.target.value })} />
<button onClick={() => remove(index)}>Remove</button>
</div>
))}
<button onClick={() => upsert({ id: (list.length + 1).toString(), text: '' })}>Add item</button>
<button onClick={() => set([])}>Reset</button>
</div>
);
};
```
35 changes: 35 additions & 0 deletions src/__stories__/useUpsert.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import ShowDocs from './util/ShowDocs';
import { useUpsert } from '../index';

interface DemoType {
id: string;
text: string;
}

const initialItems: DemoType[] = [{ id: '1', text: 'Sample' }, { id: '2', text: '' }];

const Demo = () => {
const comparisonFunction = (a: DemoType, b: DemoType) => {
return a.id === b.id;
};
const [list, { set, upsert, remove }] = useUpsert(comparisonFunction, initialItems);

return (
<div style={{ display: 'inline-flex', flexDirection: 'column' }}>
{list.map((item: DemoType, index: number) => (
<div key={item.id}>
<input value={item.text} onChange={e => upsert({ ...item, text: e.target.value })} />
<button onClick={() => remove(index)}>Remove</button>
</div>
))}
<button onClick={() => upsert({ id: (list.length + 1).toString(), text: '' })}>Add item</button>
<button onClick={() => set([])}>Reset</button>
</div>
);
};

storiesOf('State|useUpsert', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useUpsert.md')} />)
.add('Demo', () => <Demo />);
74 changes: 74 additions & 0 deletions src/__tests__/useUpsert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { act, renderHook } from '@testing-library/react-hooks';
import useUpsert from '../useUpsert';

interface TestItem {
id: string;
text: string;
}

const testItems: TestItem[] = [{ id: '1', text: '1' }, { id: '2', text: '2' }];

const itemsAreEqual = (a: TestItem, b: TestItem) => {
return a.id === b.id;
};

const setUp = (initialList: TestItem[] = []) => renderHook(() => useUpsert<TestItem>(itemsAreEqual, initialList));

describe('useUpsert', () => {
describe('initialization', () => {
const { result } = setUp(testItems);
const [list, utils] = result.current;

it('properly initiates the list content', () => {
expect(list).toEqual(testItems);
});

it('returns an upsert function', () => {
expect(utils.upsert).toBeInstanceOf(Function);
});
});

describe('upserting a new item', () => {
const { result } = setUp(testItems);
const [, utils] = result.current;

const newItem: TestItem = {
id: '3',
text: '3',
};
act(() => {
utils.upsert(newItem);
});

it('inserts a new item', () => {
expect(result.current[0]).toContain(newItem);
});
it('works immutably', () => {
expect(result.current[0]).not.toBe(testItems);
});
});

describe('upserting an existing item', () => {
const { result } = setUp(testItems);
const [, utils] = result.current;

const newItem: TestItem = {
id: '2',
text: '4',
};
act(() => {
utils.upsert(newItem);
});
const updatedList = result.current[0];

it('has the same length', () => {
expect(updatedList).toHaveLength(testItems.length);
});
it('updates the item', () => {
expect(updatedList).toContain(newItem);
});
it('works immutably', () => {
expect(updatedList).not.toBe(testItems);
});
});
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export { default as useTween } from './useTween';
export { default as useUnmount } from './useUnmount';
export { default as useUpdate } from './useUpdate';
export { default as useUpdateEffect } from './useUpdateEffect';
export { default as useUpsert } from './useUpsert';
export { default as useVideo } from './useVideo';
export { useWait, Waiter } from './useWait';
export { default as useWindowScroll } from './useWindowScroll';
Expand Down
39 changes: 39 additions & 0 deletions src/useUpsert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import useList, { Actions as ListActions } from './useList';

export interface Actions<T> extends ListActions<T> {
upsert: (item: T) => void;
}

const useUpsert = <T>(
comparisonFunction: (upsertedItem: T, existingItem: T) => boolean,
initialList: T[] = []
): [T[], Actions<T>] => {
const [items, actions] = useList(initialList);

const upsert = (upsertedItem: T) => {
let itemWasFound = false;
for (let i = 0; i < items.length; i++) {
const existingItem = items[i];

const shouldUpdate = comparisonFunction(existingItem, upsertedItem);
if (shouldUpdate) {
actions.updateAt(i, upsertedItem);
itemWasFound = true;
break;
}
}
if (!itemWasFound) {
actions.push(upsertedItem);
}
};

return [
items,
{
...actions,
upsert,
},
];
};

export default useUpsert;

0 comments on commit a7c2899

Please sign in to comment.