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

hooks useMutation and useQuery #642

Closed
wants to merge 11 commits into from
133 changes: 133 additions & 0 deletions components/react/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,139 @@ export default function Demo() {

Keep in mind this functionality if you want to prioritize one design over other. For example, to be sure you render on the server the mobile layout and keep for the client.

### useMutation

Useful when creating, updating or removing data from a source.

Perform a given mutation and update the status of it.

```jsx
import useQuery from '@s-ui/react-hooks/lib/useMutation/index.js'

const QueryStory = () => {
const {domain} = useContext(Context)
const [createAd, {isLoading}] = useMutation(
() => {
return domain.get('create_ad_use_case').execute()
},
{
onSuccess: ad => {
console.log(ad)
}
}
)

const handleClick = () => {
createAd()
}

return (
<button onClick={handleClick} isLoading={isLoading}>
Create
</button>
)
}
```

### useQuery

Hook to fetch data from a source and receive a status update.


##### Basic usage

Perform a given query and update the status of it

```jsx
import useQuery from '@s-ui/react-hooks/lib/useQuery/index.js'

const QueryStory = () => {
const {domain} = useContext(Context)
const {
isLoading,
isError,
data: ads,
error
} = useQuery(() => {
return domain.get('get_ads_use_case').execute()
})

if (isLoading) {
return <span>Loading...</span>
}

if (isError) {
return <span>Error: {error.message}</span>
}

// also status === 'success', but "else" logic works, too
return (
<ul>
{ads.map(ad => (
<li key={ad.id}>{ad.title}</li>
))}
</ul>
)
}
```

##### Initial state

Use `initialData` prop to avoid executing the query when the component is mounted. Useful when using Server-Side rendering.

```jsx
import useQuery from '@s-ui/react-hooks/lib/useQuery/index.js'

const QueryStory = ({ads}) => {
const {domain} = useContext(Context)
const {data} = useQuery(
() => {
return domain.get('get_ads_use_case').execute()
},
{initialData: ads}
)
}
```

##### Interval

Execute the query every `refetchInterval` milliseconds

```jsx
import useQuery from '@s-ui/react-hooks/lib/useQuery/index.js'

const QueryStory = ({ads}) => {
const {domain} = useContext(Context)

const {data} = useQuery(
() => {
return domain.get('get_ads_use_case').execute()
},
{refetchInterval: 3000}
)
}
```

##### Disabled mount execution

Disable mount fetch assigning `isExecuteOnMountDisabled` prop to `true`

```jsx
import useQuery from '@s-ui/react-hooks/lib/useQuery/index.js'

const QueryStory = ({ads}) => {
const {domain} = useContext(Context)

// Wont be executed on mount instead refetch can be used
const {data, refetch} = useQuery(
() => {
return domain.get('get_ads_use_case').execute()
},
{isExecuteOnMountDisabled: true}
)
}
```

### useScroll

Hook to get the scroll position and the direction of scroll, limited to the Y axis.
Expand Down
31 changes: 31 additions & 0 deletions components/react/hooks/jest-tests/useMutation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import sinon from 'sinon'

import {waitFor} from '@testing-library/react'
import {act, renderHook} from '@testing-library/react-hooks'

import {useMutation} from '../src/index.js'

describe('useMutation hook', () => {
it('should call mutate when invoked', async () => {
const mutate = sinon.spy()

const {result} = renderHook(() => useMutation(mutate))

act(() => {
const [mutation] = result.current
mutation()
})

await waitFor(() => expect(mutate.called).toBe(true))
})

it('should have idle status initially', async () => {
const mutate = sinon.spy()

const {result} = renderHook(() => useMutation(mutate))

const [, {status}] = result.current

expect(status).toBe('idle')
})
})
59 changes: 59 additions & 0 deletions components/react/hooks/jest-tests/useQuery.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {renderHook} from '@testing-library/react-hooks'

import {useQuery} from '../src/index.js'

describe('useQuery hook', () => {
it('should fetch data when is mounted', async () => {
const data = {id: '1'}

const {result, waitForNextUpdate} = renderHook(() =>
useQuery(() => {
return Promise.resolve([null, data])
})
)

await waitForNextUpdate()

expect(result.current.isSuccess).toBe(true)
expect(result.current.data).toBe(data)
})

it('should not fetch data when initial data is set', async () => {
const initialData = {id: '1'}

const {result} = renderHook(() =>
useQuery(
() => {
return Promise.resolve([null, null])
},
{initialData}
)
)

expect(result.current.isSuccess).toBe(true)
expect(result.current.data).toBe(initialData)
})

it('should refetch data', async () => {
let counter = 1

const {result, waitForNextUpdate} = renderHook(() =>
useQuery(
() => {
return Promise.resolve([null, counter++])
},
{refetchInterval: 100}
)
)

await waitForNextUpdate()

expect(result.current.isSuccess).toBe(true)
expect(result.current.data).toBe(1)

await waitForNextUpdate()

expect(result.current.isSuccess).toBe(true)
expect(result.current.data).toBe(2)
})
})
3 changes: 3 additions & 0 deletions components/react/hooks/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ export {default as useMediaQuery} from './useMediaQuery/index.js'
export {default as useMergeRefs} from './useMergeRefs/index.js'
export {default as useMount} from './useMount/index.js'
export {default as useMountedState} from './useMountedState/index.js'
export {default as useMutation} from './useMutation/index.js'
export {default as useOnClickOutside} from './useOnClickOutside/index.js'
export {default as useOnScreen, useNearScreen} from './useOnScreen/index.js'
export {default as useOrientation, orientations} from './useOrientation/index.js'
export {default as usePrevious} from './usePrevious/index.js'
export {default as useQuery} from './useQuery/index.js'
export {default as useRequest} from './useRequest/index.js'
export {default as useScroll} from './useScroll/index.js'
export {default as useSteps} from './useSteps/index.js'
export {default as useSwipe} from './useSwipe/index.js'
Expand Down
13 changes: 13 additions & 0 deletions components/react/hooks/src/useMutation/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import useRequest, {STATUSES} from '../useRequest/index.js'
export * from '../useRequest/index.js'

const useMutation = (mutation, options = {}) => {
const {refetch: mutate, ...others} = useRequest(mutation, {
...options,
initialStatus: STATUSES.IDLE
})

return [mutate, others]
}

export default useMutation
42 changes: 42 additions & 0 deletions components/react/hooks/src/useQuery/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {useEffect, useRef} from 'react'

import useRequest from '../useRequest/index.js'
export * from '../useRequest/index.js'

const useQuery = (query, {refetchInterval, isExecuteOnMountDisabled, ...options} = {}, deps = []) => {
const {data, refetch, ...others} = useRequest(query, options)
const isInitializedRef = useRef(data !== undefined || isExecuteOnMountDisabled)

useEffect(() => {
const handleRefresh = () => {
if (!document.hidden) {
refetch()
}
}

if (!isInitializedRef.current) {
refetch()
}

let intervalId

if (refetchInterval) {
intervalId = setInterval(handleRefresh, refetchInterval)
}

return () => {
if (refetchInterval) {
clearInterval(intervalId)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refetch, refetchInterval, isExecuteOnMountDisabled, ...deps])

return {
refetch,
data,
...others
}
}

export default useQuery
Loading
Loading