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

v1.2.0 #7

Merged
merged 1 commit into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ yarn-error.log
node_modules
temp
dist
lib
coverage
esm
cjs
buildcache
types
*.log
example/.cache
Expand Down
4 changes: 3 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ node_modules
*.map
tests
.circleci
tsconfig.json
tsconfig.json
tsconfig.tsbuildinfo
buildcache
108 changes: 108 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ yarn add @rennalabs/hooks
- [`useMousePosition()`](#useMousePosition)
- [`useFullscreen()`](#useFullscreen)
- [`useIdle()`](#useIdle)
- [`useCookie()`](#useCookie)
- [`useDocumentTitle()`](#useDocumentTitle)
- [`useDocumentVisibility()`](#useDocumentVisibility)
- [`useGeolocation()`](#useGeolocation)
- [`useIsomorphicEffect()`](#useIsomorphicEffect)

## Hooks

Expand Down Expand Up @@ -520,6 +525,109 @@ function Demo() {
}
```

### `useCookie()`

Returns the current value of a cookie, a callback to update the cookie and a callback to delete the cookie.

#### Example

```js
import { useCookie } from '@rennalabs/hooks';

function Demo() {
const [value, updateCookie, deleteCookie] = useCookie('my-cookie');

const updateCookieHandler = () => {
updateCookie('new-cookie-value');
};

return (
<div>
<p>Value: {value}</p>
<button onClick={updateCookieHandler}>Update Cookie</button>

<button onClick={deleteCookie}>Delete Cookie</button>
</div>
);
}
```

### `useDocumentTitle()`

Sets document.title property with React's useLayoutEffect hook. Hook is not called during server side rendering. Use this hook with client only applications. Call hook with string that should be set as document title inside any component. Hook is triggered every time value changes and value is not empty string (trailing whitespace is trimmed) or null.

#### Example

```js
import { useState } from 'react';
import { useDocumentTitle } from '@rennalabs/hooks';

function Demo() {
const [title, setTitle] = useState('');
useDocumentTitle(title);

return <button onClick={() => setTitle('new title')}>Set document title</button>;
}
```

### `useDocumentVisibility()`

Returns current document.visibilityState – it allows to detect if current tab is active.

#### Example

```js
import { useDocumentTitle, useDocumentVisibility } from '@rennalabs/hooks';

function Demo() {
const documentState = useDocumentVisibility();
useDocumentTitle(`Document is ${documentState}`);

return <div>Switch to another tab to see document title change</div>;
}
```

### `useGeolocation()`

Returns user's geographic location. This hook accepts [position options](https://developer.mozilla.org/docs/Web/API/PositionOptions).

#### Example

```js
import { useGeolocation } from '@rennalabs/hooks';

function Demo() {
const { loading, error, latitude, longitude } = useGeolocation();

if (loading) return 'loading...';
if (error) return 'error';

return (
<div>
Your location is {latitude} x {longitude}
</div>
);
}
```

### `useIsomorphicEffect()`

Allows you to switch between useEffect during server side rendering and useLayoutEffect after hydration. Use it wherever you would use useLayoutEffect to avoid warnings during ssr.

#### Example

```js
import { useIsomorphicEffect } from '@rennalabs/hooks';

function Demo() {
useIsomorphicEffect(() => {
document.title = 'title';
});

return null;
}
```

---

[![CircleCI](https://circleci.com/gh/Renna-Labs/hooks.svg?style=svg)](https://circleci.com/gh/Renna-Labs/hooks)
Expand Down
21 changes: 14 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
"name": "@rennalabs/hooks",
"version": "1.1.5",
"description": "A library of useful hooks.",
"main": "dist/index.js",
"typings": "types/index.d.ts",
"main": "cjs/index.js",
"module": "esm/index.js",
"types": "lib/index.d.ts",
"files": [
"dist",
"types"
"cjs",
"esm",
"lib"
],
"scripts": {
"start": "NODE_ENV=development babel -w ./src --out-dir ./dist --ignore \"src/**/*.test.ts\",\"src/**/*.test.tsx\" --extensions \".ts,.tsx\"",
"build": "NODE_ENV=production npm run clean && babel ./src --out-dir ./dist --ignore \"src/**/*.test.ts\",\"src/**/*.test.tsx\" --extensions \".ts,.tsx\" && tsc",
"clean": "rimraf types dist",
"build:cjs": "tsc --outDir ./cjs --module commonjs --project tsconfig.build.json",
"build:esm": "tsc --outDir ./esm --module esnext --target esnext --project tsconfig.build.json",
"build": "yarn clean && yarn build:cjs && yarn build:esm && tsc --project tsconfig.build.json --declaration --emitDeclarationOnly",
"clean": "rimraf lib esm cjs",
"format": "prettier --write \"src/*.{js,ts,tsx,json,md}\"",
"lint": "eslint \"src/**/*.{js,ts,tsx}\" --quiet",
"test": "jest",
Expand Down Expand Up @@ -89,6 +92,7 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.4.0",
"@types/js-cookie": "^3.0.2",
"@types/react": "^18.0.27",
"@typescript-eslint/eslint-plugin": "^5.30.0",
"@typescript-eslint/parser": "^5.30.0",
Expand All @@ -110,5 +114,8 @@
"rimraf": "^4.1.2",
"ts-jest": "^29.0.5",
"typescript": "^4.x.x"
},
"dependencies": {
"js-cookie": "^3.0.1"
}
}
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@ export * from './utils';
export { useOs } from './useOs/useOs';
export { useIdle } from './useIdle/useIdle';
export { useHover } from './useHover/useHover';
export { useCookie } from './useCookie/useCookie';
export { useCounter } from './useCounter/useCounter';
export { useTimeout } from './useTimeout/useTimeout';
export { useInterval } from './useInterval/useInterval';
export { useFullscreen } from './useFullscreen/useFullscreen';
export { useMediaQuery } from './useMediaQuery/useMediaQuery';
export { useWindowSize } from './useWindowSize/useWindowSize';
export { useGeolocation } from './useGeolocation/useGeolocation';
export { useElementSize } from './useElementSize/useElementSize';
export { useLocalStorage } from './useLocalStorage/useLocalStorage';
export { useResizeObserver } from './useElementSize/useElementSize';
export { useDocumentTitle } from './useDocumentTitle/useDocumentTitle';
export { useMousePosition } from './useMousePosition/useMousePosition';
export { useNetworkStatus } from './useNetworkStatus/useNetworkStatus';
export { useOnClickOutside } from './useOnClickOutside/useOnClickOutside';
export { useRandomInterval } from './useRandomInterval/useRandomInterval';
export { useIsomorphicEffect } from './useIsomorphicEffect/useIsomorphicEffect';
export { useDocumentVisibility } from './useDocumentVisibility/useDocumentVisibility';
export { useWindowScrollPosition } from './useWindowScrollPosition/useWindowScrollPosition';
export { usePrefersReducedMotion } from './usePrefersReducedMotion/usePrefersReducedMotion';
23 changes: 23 additions & 0 deletions src/useCookie/useCookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useCallback, useState } from 'react';
import Cookies from 'js-cookie';

export const useCookie = (
cookieName: string,
): [string | null, (newValue: string, options?: Cookies.CookieAttributes) => void, () => void] => {
const [value, setValue] = useState<string | null>(() => Cookies.get(cookieName) || null);

const updateCookie = useCallback(
(newValue: string, options?: Cookies.CookieAttributes) => {
Cookies.set(cookieName, newValue, options);
setValue(newValue);
},
[cookieName],
);

const deleteCookie = useCallback(() => {
Cookies.remove(cookieName);
setValue(null);
}, [cookieName]);

return [value, updateCookie, deleteCookie];
};
22 changes: 22 additions & 0 deletions src/useDocumentTitle/useDocumentTitle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { renderHook } from '@testing-library/react';
import { useDocumentTitle } from './useDocumentTitle';

describe('useDocumentTitle', () => {
it('sets given value as document.title', () => {
renderHook(() => useDocumentTitle('test-title'));
expect(document.title).toBe('test-title');
});

it('does not change document.title if called with empty string', () => {
document.title = 'test-title';
renderHook(() => useDocumentTitle(''));
expect(document.title).toBe('test-title');
renderHook(() => useDocumentTitle(' \t\n'));
expect(document.title).toBe('test-title');
});

it('trims value before setting to document.title', () => {
renderHook(() => useDocumentTitle(' test-title\t\n '));
expect(document.title).toBe('test-title');
});
});
9 changes: 9 additions & 0 deletions src/useDocumentTitle/useDocumentTitle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useIsomorphicEffect } from '../useIsomorphicEffect/useIsomorphicEffect';

export function useDocumentTitle(title: string) {
useIsomorphicEffect(() => {
if (typeof title === 'string' && title.trim().length > 0) {
document.title = title.trim();
}
}, [title]);
}
36 changes: 36 additions & 0 deletions src/useDocumentVisibility/useDocumentVisibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { renderHook, fireEvent } from '@testing-library/react';
import { useDocumentVisibility } from './useDocumentVisibility';

describe('useDocumentVisibility', () => {
beforeAll(() => {
Object.defineProperty(document, 'visibilityState', {
value: 'visible',
writable: true,
});
});

it('return default visibility state', () => {
const { result } = renderHook(() => useDocumentVisibility());

expect(result.current).toBe('visible');
});

it('should update return value when visibilityState changes', () => {
const { result } = renderHook(() => useDocumentVisibility());

expect(result.current).toBe('visible');

// @ts-ignore
document.visibilityState = 'hidden';

fireEvent(document, new Event('visibilitychange'));

expect(result.current).toBe('hidden');
// @ts-ignore
document.visibilityState = 'visible';

fireEvent(document, new Event('visibilitychange'));

expect(result.current).toBe('visible');
});
});
13 changes: 13 additions & 0 deletions src/useDocumentVisibility/useDocumentVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useEffect, useState } from 'react';

export function useDocumentVisibility(): DocumentVisibilityState {
const [documentVisibility, setDocumentVisibility] = useState<DocumentVisibilityState>('visible');

useEffect(() => {
const listener = () => setDocumentVisibility(document.visibilityState);
document.addEventListener('visibilitychange', listener);
return () => document.removeEventListener('visibilitychange', listener);
}, []);

return documentVisibility;
}
73 changes: 73 additions & 0 deletions src/useGeolocation/useGeolocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useEffect, useState } from 'react';

/**
* @desc Made compatible with {GeolocationPositionError} and {PositionError} cause
* PositionError been renamed to GeolocationPositionError in typescript 4.1.x and making
* own compatible interface is most easiest way to avoid errors.
*/
export interface IGeolocationPositionError {
readonly code: number;
readonly message: string;
readonly PERMISSION_DENIED: number;
readonly POSITION_UNAVAILABLE: number;
readonly TIMEOUT: number;
}

export interface GeoLocationSensorState {
loading: boolean;
accuracy: number | null;
altitude: number | null;
altitudeAccuracy: number | null;
heading: number | null;
latitude: number | null;
longitude: number | null;
speed: number | null;
timestamp: number | null;
error?: Error | IGeolocationPositionError;
}

export const useGeolocation = (options?: PositionOptions): GeoLocationSensorState => {
const [state, setState] = useState<GeoLocationSensorState>({
loading: true,
accuracy: null,
altitude: null,
altitudeAccuracy: null,
heading: null,
latitude: null,
longitude: null,
speed: null,
timestamp: Date.now(),
});
let mounted = true;
let watchId: any;

const onEvent = (event: any) => {
if (mounted) {
setState({
loading: false,
accuracy: event.coords.accuracy,
altitude: event.coords.altitude,
altitudeAccuracy: event.coords.altitudeAccuracy,
heading: event.coords.heading,
latitude: event.coords.latitude,
longitude: event.coords.longitude,
speed: event.coords.speed,
timestamp: event.timestamp,
});
}
};
const onEventError = (error: IGeolocationPositionError) =>
mounted && setState(oldState => ({ ...oldState, loading: false, error }));

useEffect(() => {
navigator.geolocation.getCurrentPosition(onEvent, onEventError, options);
watchId = navigator.geolocation.watchPosition(onEvent, onEventError, options);

return () => {
mounted = false;
navigator.geolocation.clearWatch(watchId);
};
}, []);

return state;
};
5 changes: 5 additions & 0 deletions src/useIsomorphicEffect/useIsomorphicEffect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useEffect, useLayoutEffect } from 'react';

// useLayoutEffect will show warning if used during ssr, e.g. with Next.js
// useIsomorphicEffect removes it by replacing useLayoutEffect with useEffect during ssr
export const useIsomorphicEffect = typeof document !== 'undefined' ? useLayoutEffect : useEffect;
Loading