Skip to content

Commit

Permalink
Implement Hook with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sergiodxa committed Jul 23, 2020
1 parent 3fbae3e commit 6c8277c
Show file tree
Hide file tree
Showing 9 changed files with 6,724 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
patreon: sergiodxa
custom: https://paypal.me/sergiodxa
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: CI

on: [push]

jobs:
test:
name: Test
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v1
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install
run: yarn install
- name: Test
run: yarn test
env:
CI: true
19 changes: 19 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Publish

on:
release:
types: [published]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: 12
registry-url: https://registry.npmjs.org/
- run: yarn install
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
196 changes: 194 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,194 @@
# use-mutation
Run async mutations safely in React
# useMutation ![CI](https://github.com/sergiodxa/use-mutation/workflows/CI/badge.svg) ![Publish](https://github.com/sergiodxa/use-mutation/workflows/Publish/badge.svg)

Run function which cause a side-effect, specially useful to run requests against an API, inside a React component.

## Usage

Install it:

```sh
$ yarn add use-mutation
```

Import it:

```ts
import useMutation from 'use-mutation';
```

Create a function which runs a mutation

```tsx
async function createComment({
authorId,
comment,
}: {
authorId: number;
comment: string;
}) {
const res = await fetch('/api/comments', {
method: 'POST',
body: JSON.stringify({ authorId, comment }),
});
if (!res.ok) throw new Error(res.statusText);
return await res.json();
}
```

Use your function with `useMutation`

```tsx
function CommentForm({ authorId }) {
const [comment, setComment] = React.useState('');
const [mutate, { status }] = useMutation(createComment, {
onMutate(input) {
// do something before the mutation run
return () => {
// rollback changes if the mutation failed
};
},
onSuccess(result) {
// do something once the mutation succeeded
},
onFailure(reason, rollback) {
// do something once the mutation failed
},
onSettled(reason, result, rollback) {
if (reason) {
// do something if the mutation failed
} else {
// do something if the mutation succeeded
}
},
});

const handleSubmit = React.useCallback(
function handleSubmit(event) {
mutate({ authorId, comment });
},
[mutate, comment]
);

// render your UI
}
```

### Usage with SWR

If you are using SWR, you can use `useMutation` to run your mutations to perform Optimistic UI changes.

```tsx
import { cache, mutate } from 'swr';

function createComment(input) {
// ...
}

function useCreateComment() {
return useMutation(createComment, {
onMutate(input) {
const oldData = cache.get('comment-list');
// optimistically update the data before your mutation is run
mutate('comment-list', current => current.concat(input), false);
return () => mutate('comment-list', oldData, false); // revalidate if it failed
},
});
}
```

This way when you run `mutate`, it will first optimistically update your SWR cache and if it fails it will rollback to the old data.

## API Reference

```tsx
const [mutate, { status, data, error, reset }] = useMutation<
Input,
Data,
Error
>(mutationFn, {
onMutate,
onSuccess,
onFailure,
onSettled,
throwOnFailure,
useErrorBoundary,
});

const promise = mutate(input, {
onSuccess,
onSettled,
onError,
throwOnFailure,
});
```

### Hook Generic

> Only if you are using TypeScript
- `Input = any`
- The data your mutation function needs to run
- `Data = any`
- The data the hook will return as result of your mutation
- `Error = any`
- The error the hook will return as a failure in your mutation

### Hook Options

- `mutationFn(input: Input): Promise<Data>`
- **Required**
- A function to be executed before the mutation runs.
- It receives the same input as the mutate function.
- It can be an async or sync function, in both cases if it returns a function it will keep it as a way to rollback the changed applied inside onMutate.
- `onMutate?(input: Input): Promise<rollbackFn | undefined> | rollbackFn | undefined`
- Optional
- A function to be executed before the mutation runs.
- It receives the same input as the mutate function.
- It can be an async or sync function, in both cases if it returns a function.
- it will keep it as a way to rollback the changed applied inside `onMutate`
- `onSuccess?(result: Data): Promise<void> | void`
- Optional
- A function to be executed after the mutation resolves successfully.
- It receives the result of the mutation.
- If a Promise is returned, it will be awaited before proceeding.
- `onFailure?(reason: Error, rollback: rollbackFn): Promise<void> | void`
- Optional
- A function to be executed after the mutation failed to execute.
- If a Promise is returned, it will be awaited before proceeding.
- `onSettled?(reason?: Error, result?: Data, rollback?: rollbackFn): Promise<void> | void`
- Optional
- A function to be executed after the mutation has resolves, either successfully or as failure.
- This function receives the reason of the error or the result of the mutation. It follow the normal Node.js callback style.
- If a Promise is returned, it will be awaited before proceeding.
- `throwOnFailure?: boolean`
- Optional
- If defined as `true`, a failure in the mutation will cause the `mutate` function to throw. Disabled by default.
- `useErrorBoundary?: boolean` (default false)
- Optional
- If defined as `true`, a failure in the mutation will cause the Hook to throw in render time, making error boundaries catch the error.

### Hook Returned Value

- `mutate(input: Input, config: Omit<Options<Input, Data, Error>, 'onMutate' | 'useErrorBoundary'> = {}): Promise<Data | undefined>`
- The function you call to trigger your mutation, passing the input your mutation function needs.
- All the lifecycle callback defined here will run _after_ the callback defined in the Hook.
- `status: 'idle' | 'running' | 'success' | 'failure'`
- The current status of the mutation, it will be:
- `idle` initial status of the hook, and the status after a reset
- `running` if the mutation is currently running
- `success` if the mutation resolved successfully
- `failure` if the mutation failed to resolve
- `data: Data`
- The data returned as the result of the mutation.
- `error: Error`
- The error returned as the result of the mutation.
- `reset(): void
- A function to reset the internal state of the Hook to the orignal idle and clear any data or error.

## Author

- [Sergio Xalambrí](https://sergiodxa.com) - [Able](https://able.co)

## License

The MIT License.
57 changes: 57 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "use-mutation",
"version": "0.1.0",
"license": "MIT",
"author": "Sergio Xalambrí",
"main": "dist/index.js",
"module": "dist/a.esm.js",
"typings": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"repository": {
"type": "git",
"url": "sergiodxa/use-mutation"
},
"engines": {
"node": ">=10"
},
"scripts": {
"start": "tsdx watch",
"build": "tsdx build",
"test": "tsdx test --passWithNoTests",
"lint": "tsdx lint",
"prepare": "tsdx build"
},
"husky": {
"hooks": {
"pre-commit": "tsdx lint"
}
},
"prettier": {
"printWidth": 80,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
},
"devDependencies": {
"@testing-library/react": "^10.4.7",
"@testing-library/user-event": "^12.0.12",
"@types/react": "^16.9.43",
"@types/react-dom": "^16.9.8",
"husky": "^4.2.5",
"mutation-observer": "^1.0.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"tsdx": "^0.13.2",
"tslib": "^2.0.0",
"typescript": "^3.9.7"
},
"peerDependencies": {
"react": ">=16"
},
"dependencies": {
"use-safe-callback": "^1.0.1"
}
}
Loading

0 comments on commit 6c8277c

Please sign in to comment.