diff --git a/docs/useHash.md b/docs/useHash.md
new file mode 100644
index 0000000000..7cb4092b6f
--- /dev/null
+++ b/docs/useHash.md
@@ -0,0 +1,39 @@
+# `useHash`
+
+React sensor hook that tracks browser's location hash.
+
+## Usage
+
+```jsx
+import {useHash} from 'react-use';
+
+const Demo = () => {
+ const [hash, setHash] = useHash();
+
+ useMount(() => {
+ setHash('#/path/to/page?userId=123');
+ });
+
+ return (
+
+
window.location.href:
+
+
{window.location.href}
+
+
Edit hash:
+
+ setHash(e.target.value)} />
+
+
+ );
+};
+```
+
+## API
+
+`const [hash, setHash] = useHash()`
+
+Get latest url hash with `hash` and set url hash with `setHash`.
+
+- `hash: string`: get current url hash. listen to `hashchange` event.
+- `setHash: (newHash: string) => void`: change url hash. Invoke this method will trigger `hashchange` event.
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index 70e290a965..6d0181648b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -110,3 +110,4 @@ export { useRendersCount } from './useRendersCount';
export { useFirstMountState } from './useFirstMountState';
export { default as useSet } from './useSet';
export { createGlobalState } from './createGlobalState';
+export { useHash } from './useHash'
\ No newline at end of file
diff --git a/src/useHash.ts b/src/useHash.ts
new file mode 100644
index 0000000000..34d5dd7597
--- /dev/null
+++ b/src/useHash.ts
@@ -0,0 +1,27 @@
+import { useState, useCallback } from "react"
+import useLifecycles from "./useLifecycles"
+
+/**
+ * read and write url hash, response to url hash change
+ */
+export const useHash = () => {
+ const [hash, setHash] = useState(() => window.location.hash)
+
+ const onHashChange = useCallback(() => {
+ setHash(window.location.hash)
+ }, [])
+
+ useLifecycles(() => {
+ window.addEventListener('hashchange', onHashChange)
+ }, () => {
+ window.removeEventListener('hashchange', onHashChange)
+ })
+
+ const _setHash = useCallback((newHash: string) => {
+ if (newHash !== hash) {
+ window.location.hash = newHash
+ }
+ }, [hash])
+
+ return [hash, _setHash] as const
+}
\ No newline at end of file
diff --git a/stories/useHash.story.tsx b/stories/useHash.story.tsx
new file mode 100644
index 0000000000..ad21c82329
--- /dev/null
+++ b/stories/useHash.story.tsx
@@ -0,0 +1,29 @@
+import { storiesOf } from '@storybook/react';
+import * as React from 'react';
+import { useHash, useMount } from '../src';
+import ShowDocs from './util/ShowDocs';
+
+const Demo = () => {
+ const [hash, setHash] = useHash();
+
+ useMount(() => {
+ setHash('#/path/to/page?userId=123');
+ });
+
+ return (
+
+
window.location.href:
+
+
{window.location.href}
+
+
Edit hash:
+
+ setHash(e.target.value)} />
+
+
+ );
+};
+
+storiesOf('Sensors|useHash', module)
+ .add('Docs', () => )
+ .add('Demo', () => );
diff --git a/tests/useHash.test.ts b/tests/useHash.test.ts
new file mode 100644
index 0000000000..d097538a49
--- /dev/null
+++ b/tests/useHash.test.ts
@@ -0,0 +1,54 @@
+import { renderHook, act } from '@testing-library/react-hooks';
+import { useHash } from '../src/useHash';
+
+(global as any).window = Object.create(window);
+let mockHash = '#';
+const mockLocation = {};
+Object.defineProperty(mockLocation, 'hash', {
+ get() {
+ return mockHash;
+ },
+ set(newHash) {
+ mockHash = newHash;
+ window.dispatchEvent(new HashChangeEvent('hashchange'));
+ },
+});
+Object.defineProperty(window, 'location', {
+ value: mockLocation,
+});
+
+beforeEach(() => {
+ window.location.hash = '#';
+});
+
+test('returns current url hash', () => {
+ window.location.hash = '#abc';
+
+ const { result } = renderHook(() => useHash());
+
+ const hash = result.current[0];
+ expect(hash).toBe('#abc');
+});
+
+test('returns latest url hash when change the hash with setHash', () => {
+ const { result } = renderHook(() => useHash());
+ const hash = result.current[0];
+ const setHash = result.current[1];
+ expect(hash).toBe('#');
+ act(() => {
+ setHash('#abc');
+ });
+ const hash2 = result.current[0];
+ expect(hash2).toBe('#abc');
+});
+
+it('returns latest url hash when change the hash with "hashchange" event', () => {
+ const {result} = renderHook(() => useHash());
+ const hash = result.current[0]
+ expect(hash).toBe('#');
+ act(() => {
+ window.location.hash = '#abc'
+ })
+ const hash2 = result.current[0]
+ expect(hash2).toBe('#abc');
+});
\ No newline at end of file