Skip to content

Commit

Permalink
eat(react): useNetwork 신규 훅 추가 (#194)
Browse files Browse the repository at this point in the history
  • Loading branch information
ssi02014 authored Jun 2, 2024
1 parent af930a0 commit e12da88
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/fair-windows-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modern-kit/react': minor
---

feat(react): useNetwork 신규 훅 추가 - @ssi02014
75 changes: 75 additions & 0 deletions docs/docs/react/hooks/useNetwork.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useNetwork } from '@modern-kit/react';

# useNetwork

`useSyncExternalStore`을 사용해 브라우저 네트워크 연결 상태를 구독 후 online 상태 여부를 반환하는 커스텀 훅입니다.

`onlineCallback`, `offlineCallback`을 통해 네트워크 상태가 변경 될 때 원하는 함수를 호출 할 수 있습니다.

<br />

## Code
[🔗 실제 구현 코드 확인](https://github.com/modern-agile-team/modern-kit/blob/main/packages/react/src/hooks/useNetwork/index.ts)

## Interface

```ts title="typescript"
interface UseNetworkProps {
onlineCallback?: (event: Event) => void;
offlineCallback?: (event: Event) => void;
}

const useNetwork: ({ onlineCallback, offlineCallback }?: UseNetworkProps) => {
isOnline: boolean;
};
```

## Usage

```tsx title="typescript"
import { useNetwork } from '@modern-kit/react';

const Example = () => {
const { isOnline } = useNetwork({
onlineCallback: () => {
alert('online');
},
offlineCallback: () => {
alert('offline');
},
});

return (
<div>
<p>개발자 도구의 네트워크 탭에서 디바이스 연결을 끊어보세요.</p>
<p>{isOnline ? '🟢 online' : '❌ offline'}</p>
</div>
);
};
```

## Example

export const Example = () => {
const { isOnline } = useNetwork({
onlineCallback: () => {
alert('online');
},
offlineCallback: () => {
alert('offline');
},
});

return (
<div>
<p>개발자 도구의 네트워크 탭에서 디바이스 연결을 끊어보세요.</p>
<p>{isOnline ? '🟢 Online' : '❌ Offline'}</p>
</div>
);
};

<Example />

## Note
- [useSyncExternalStore(en) - react.dev](https://react.dev/reference/react/useSyncExternalStore)
- [useSyncExternalStore(ko) - react.dev](https://ko.react.dev/reference/react/useSyncExternalStore)
1 change: 1 addition & 0 deletions packages/react/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export * from './useKeyDown';
export * from './useMediaQuery';
export * from './useMergeRefs';
export * from './useMouse';
export * from './useNetwork';
export * from './useOnClickOutside';
export * from './usePreservedState';
export * from './usePreservedCallback';
Expand Down
57 changes: 57 additions & 0 deletions packages/react/src/hooks/useNetwork/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { usePreservedCallback } from '../usePreservedCallback';
import { noop } from '@modern-kit/utils';
import { useSyncExternalStore } from 'react';

interface UseNetworkProps {
onlineCallback?: (event: Event) => void;
offlineCallback?: (event: Event) => void;
}

const getSnapshot = () => {
return navigator.onLine;
};

const getServerSnapshot = () => {
return true;
};

const subscribe = (
onStoreChange: () => void,
onlineCallback: (event: Event) => void,
offlineCallback: (event: Event) => void
) => {
const handleOnlineCallback = (event: Event) => {
onlineCallback(event);
return onStoreChange();
};

const handleOfflineCallback = (event: Event) => {
offlineCallback(event);
return onStoreChange();
};

window.addEventListener('online', handleOnlineCallback);
window.addEventListener('offline', handleOfflineCallback);

return () => {
window.removeEventListener('online', handleOnlineCallback);
window.removeEventListener('offline', handleOfflineCallback);
};
};

export const useNetwork = ({
onlineCallback = noop,
offlineCallback = noop,
}: UseNetworkProps = {}) => {
const preservedSubscribe = usePreservedCallback((onStoreChange: () => void) =>
subscribe(onStoreChange, onlineCallback, offlineCallback)
);

const isOnline = useSyncExternalStore(
preservedSubscribe,
getSnapshot,
getServerSnapshot
);

return { isOnline };
};
67 changes: 67 additions & 0 deletions packages/react/src/hooks/useNetwork/useNetwork.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { renderHook } from '@testing-library/react';
import { MockInstance } from 'vitest';
import { useNetwork } from '.';
import { renderToString } from 'react-dom/server';

let navigatorOnLineMockSpy: MockInstance;

beforeEach(() => {
navigatorOnLineMockSpy = vi.spyOn(navigator, 'onLine', 'get');
});

describe('useNetwork', () => {
const onlineCallbackMock = vi.fn();
const offlineCallbackMock = vi.fn();

it('should return true for isOnline when online', () => {
navigatorOnLineMockSpy.mockReturnValue(true);

const { result } = renderHook(() => useNetwork());

expect(result.current.isOnline).toBeTruthy();
});

it('should return false for isOnline when offline', () => {
navigatorOnLineMockSpy.mockReturnValue(false);

const { result } = renderHook(() => useNetwork());

expect(result.current.isOnline).toBeFalsy();
});

it('should execute the registered callback when the network status changes', () => {
navigatorOnLineMockSpy.mockReturnValue(true);

renderHook(() =>
useNetwork({
onlineCallback: onlineCallbackMock,
offlineCallback: offlineCallbackMock,
})
);

expect(offlineCallbackMock).not.toBeCalled();
expect(onlineCallbackMock).not.toBeCalled();

window.dispatchEvent(new Event('offline'));

expect(offlineCallbackMock).toBeCalled();

window.dispatchEvent(new Event('online'));

expect(onlineCallbackMock).toBeCalled();
});

it('should return true for isOnline during server-side rendering', () => {
const TestComponent = () => {
const { isOnline } = useNetwork({
onlineCallback: onlineCallbackMock,
offlineCallback: offlineCallbackMock,
});
return <p>{isOnline ? 'online' : 'offline'}</p>;
};

const html = renderToString(<TestComponent />); // server side rendering

expect(html).toContain('online');
});
});

0 comments on commit e12da88

Please sign in to comment.