diff --git a/.gitignore b/.gitignore
index f3a8765..6441ce9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
node_modules/
dist/
yarn-error.log
-.vscode/
\ No newline at end of file
+.vscode/
diff --git a/README.md b/README.md
index dfe165d..d0b9ac2 100644
--- a/README.md
+++ b/README.md
@@ -24,21 +24,27 @@ yarn add react-use-fetch-with-redux
## API
-`useFetchWithRedux` is a function that takes two parameters:
+`useFetchWithRedux` is a function that takes three parameters:
- `getDataStart`: This is a function that returns a Redux action (i.e. an action creator) that must kickstart your data fetching process (i.e. the handler for this action could make an API call and store the result of that in your Redux store).
- `selector`: This is a function that takes your Redux state and returns the slice of state you are returning from your hook. If the selector returns `null`, then your `getDataStart` action will be dispatched.
+- `options`: This is a configuration object that allows cache settings to be configured.
## How it works
- You provide an action creator (`getDataStart`)
- You provide a selector (`selector`)
+- You provide an optional cache config (`options`)
-If your selector returns data, then your action creator is not called and the hook will simply return that data from state.
+If you **have not** provided a cache config:
-If your selector returns `null`, then the action your action creator returns will be dispatched. It is up to you to provide the logic in your selectors to know when to return null.
+- If your selector returns `null`, then the action your action creator returns will be dispatched. It is up to you to provide the logic in your selectors to know when to return null.
+- If your selector returns data, then your action creator is not called and the hook will simply return that data from state.
-This is an explicit design decision that was made when designing this hook to avoid forcing people to shape their state around this hook (i.e. we could have forced people to have a flag on each slice of state to indicate if something had loaded from an API or not, but that was deemed too intrusive).
+If you **have** provided a cache config:
+
+- If your selector returns data and cache timeout **hasn't** been reached, then your action creator is not called and the hook will simply return data from state.
+- If your selector returns data and cache timeout **has** been reached, then the action your action creator returns will be dispatched.
## Usage
@@ -86,6 +92,122 @@ const SomeComponent = () => {
};
```
+## Additional features
+
+### Caching
+
+There is the option to invalidate the cache, meaning next time the hook is called it will fetch the data again.
+
+By setting a `timeTillCacheInvalidate` time (in ms), as follows:
+
+**In `SomeHighLevelComponent.tsx`**
+
+```tsx
+import React from 'react';
+import { Provider } from 'react-redux';
+import { store } from './redux/store';
+
+const SomeComponent = () => {
+
+
+
+
+ ;
+};
+```
+
+Will result in the cache invalidating after 30 minutes.
+There is also the option to set the cache invalidation time per hook, with an optional third parameter like:
+
+**In `useThing.ts`**
+
+```typescript
+import { useFetchWithRedux } from 'react-use-fetch-with-redux';
+import { getThingStart } from './actions/ThingActions'; // getThingStart is an action creator.
+import { getThingSelector } from './selectors/ThingSelector'; // getThingSelector is a selector.
+
+const useThing = () =>
+ useFetchWithRedux(getThingStart, getThingSelector, {
+ timeTillCacheInvalidate: 1800000,
+ });
+
+export { useThing };
+```
+
+This will override any value set by the provider.
+You can also not set any value at the provider level, and handle all invalidation times in the hooks, but you will still need the provider, just with no value:
+
+**In `SomeHighLevelComponent.tsx`**
+
+```tsx
+import React from 'react';
+import { Provider } from 'react-redux';
+import { store } from './redux/store';
+
+const SomeComponent = () => {
+
+
+
+
+ ;
+};
+```
+
+**In `useThing.ts`**
+
+```typescript
+import { useFetchWithRedux } from 'react-use-fetch-with-redux';
+import { getThingStart } from './actions/ThingActions'; // getThingStart is an action creator.
+import { getThingSelector } from './selectors/ThingSelector'; // getThingSelector is a selector.
+
+const useThing = () =>
+ useFetchWithRedux(getThingStart, getThingSelector, {
+ timeTillCacheInvalidate: 1800000,
+ });
+
+export { useThing };
+```
+
+## Gotchas
+
+A lower cache timeout value will always take precedence. For example if we have:
+
+- `timeTillCacheInvalidate` at the Provider level is 10000 (10 seconds)
+- We call the hook with a `timeTillCacheInvalidate` of 5000 (5 seconds)
+
+Then the cache timeout will be 5 seconds for the individual instance of that hook. This is what that looks like in code:
+
+**In `SomeHighLevelComponent.tsx`**
+
+```tsx
+import React from 'react';
+import { Provider } from 'react-redux';
+import { store } from './redux/store';
+
+const SomeComponent = () => {
+
+
+
+
+ ;
+};
+```
+
+**In `useThing.ts`**
+
+```typescript
+import { useFetchWithRedux } from 'react-use-fetch-with-redux';
+import { getThingStart } from './actions/ThingActions'; // getThingStart is an action creator.
+import { getThingSelector } from './selectors/ThingSelector'; // getThingSelector is a selector.
+
+const useThing = () =>
+ useFetchWithRedux(getThingStart, getThingSelector, {
+ timeTillCacheInvalidate: 5000,
+ });
+
+export { useThing };
+```
+
## Testing
The project uses Jest for testing, along with [react-hooks-testing-library](https://github.com/testing-library/react-hooks-testing-library) for rendering hooks without explicitly creating harness components.
@@ -100,5 +222,4 @@ There are many things that could improve this hook, so keep your eyes peeled or
Possible features include:
-- More sophisticated cachine strategies
-- Ability to specify caching strategies
+- More sophisticated caching strategies
diff --git a/package.json b/package.json
index 5c5a35b..e704161 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-use-fetch-with-redux",
- "version": "2.0.1",
+ "version": "3.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
diff --git a/src/Provider.tsx b/src/Provider.tsx
new file mode 100644
index 0000000..9c71991
--- /dev/null
+++ b/src/Provider.tsx
@@ -0,0 +1,42 @@
+import React, {
+ useState,
+ createContext,
+ Dispatch,
+ SetStateAction,
+} from 'react';
+import { CacheTimeouts } from './types';
+
+type Props = {
+ children: JSX.Element;
+ timeTillCacheInvalidate?: number;
+};
+
+type ContextProps = {
+ cacheTimeouts: CacheTimeouts;
+ setCacheTimeouts: Dispatch>;
+ timeTillCacheInvalidate?: number;
+};
+
+const ReactUseFetchWithReduxContext = createContext({
+ cacheTimeouts: {},
+ setCacheTimeouts: () => {},
+});
+
+const ReactUseFetchWithReduxProvider = ({
+ children,
+ timeTillCacheInvalidate,
+}: Props) => {
+ const [cacheTimeouts, setCacheTimeouts] = useState({});
+
+ const contextValue = timeTillCacheInvalidate
+ ? { cacheTimeouts, setCacheTimeouts, timeTillCacheInvalidate }
+ : { cacheTimeouts, setCacheTimeouts };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export { ReactUseFetchWithReduxProvider, ReactUseFetchWithReduxContext };
diff --git a/src/index.spec.ts b/src/index.spec.ts
index 969b7e7..f5e9a8c 100644
--- a/src/index.spec.ts
+++ b/src/index.spec.ts
@@ -2,50 +2,416 @@ import { renderHook } from '@testing-library/react-hooks';
import { useSelector, useDispatch } from 'react-redux';
import { Action } from 'redux';
import { useFetchWithRedux } from './';
+import { useContext } from 'react';
+import { getRemainingCacheTime } from './utils';
jest.mock('react-redux', () => ({
useSelector: jest.fn(),
useDispatch: jest.fn(),
}));
-const mockUseSelector = (useSelector as unknown) as jest.Mock;
-const mockUseDispatch = (useDispatch as unknown) as jest.Mock;
+jest.mock('react', () => ({
+ ...require.requireActual('react'),
+ useContext: jest.fn(),
+}));
+
+jest.mock('./utils', () => ({
+ getRemainingCacheTime: jest.fn(),
+}));
+
+const mockUseSelector = useSelector as jest.Mock;
+const mockUseDispatch = useDispatch as jest.Mock;
+const mockUseContext = useContext as jest.Mock;
+const mockGetRemainingCacheTime = getRemainingCacheTime as jest.Mock;
+
const mockDispatch = jest.fn();
+const setCacheTimeouts = jest.fn();
const testAction = (): Action => ({
type: 'TEST',
});
+
const testSelector = (value: T) => value;
describe('useFetchWithRedux hook', () => {
beforeEach(() => {
+ const now = Date.now();
+ Date.now = () => now;
jest.resetAllMocks();
mockUseDispatch.mockImplementation(() => mockDispatch);
});
- it('Returns null and calls dispatch when selector returns null', () => {
- mockUseSelector.mockImplementation(callback =>
- callback(testSelector(null)),
- );
+ describe('When no cache has been set', () => {
+ describe('With no value provided for timeTillCacheInvalidate', () => {
+ beforeEach(() => {
+ mockUseContext.mockImplementation(() => ({
+ cacheTimeouts: {},
+ setCacheTimeouts,
+ }));
+ });
+
+ describe('With no value provided for timeTillCacheInvalidate locally', () => {
+ it('Returns null when useSelector returns null, and dispatches the action', () => {
+ mockUseSelector.mockImplementation(callback =>
+ callback(testSelector(null)),
+ );
+
+ const { result } = renderHook(() =>
+ useFetchWithRedux(testAction, testSelector),
+ );
+
+ expect(result.current).toEqual(null);
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
+ expect(mockDispatch).toHaveBeenCalledWith(testAction());
+ });
+
+ it('Returns a value when useSelector returns a value, and should not dispatch an action', () => {
+ mockUseSelector.mockImplementation(callback =>
+ callback(testSelector(testSelector(7))),
+ );
+
+ const { result } = renderHook(() =>
+ useFetchWithRedux(testAction, testSelector),
+ );
+
+ expect(result.current).toEqual(7);
+ expect(mockDispatch).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('With a value provided for timeTillCacheInvalidate locally', () => {
+ it('Returns null when useSelector returns null, and dispatches the action ', () => {
+ mockUseSelector.mockImplementation(callback =>
+ callback(testSelector(null)),
+ );
+
+ const { result } = renderHook(() =>
+ useFetchWithRedux(testAction, testSelector, {
+ timeTillCacheInvalidate: 1234,
+ }),
+ );
+
+ expect(result.current).toEqual(null);
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
+ expect(mockDispatch).toHaveBeenCalledWith(testAction());
+ });
+
+ it('Returns a value when useSelector returns a value, and should not dispatch an action', () => {
+ mockUseSelector.mockImplementation(callback =>
+ callback(testSelector(testSelector(7))),
+ );
+
+ const { result } = renderHook(() =>
+ useFetchWithRedux(testAction, testSelector),
+ );
+
+ expect(result.current).toEqual(7);
+ expect(mockDispatch).not.toHaveBeenCalled();
+ });
+
+ it('Sets cache with the action type, time and timeTillCacheInvalidate', () => {
+ mockUseSelector.mockImplementation(callback =>
+ callback(testSelector(testSelector(7))),
+ );
+
+ renderHook(() =>
+ useFetchWithRedux(testAction, testSelector, {
+ timeTillCacheInvalidate: 1234,
+ }),
+ );
+
+ expect(setCacheTimeouts).toHaveBeenCalledTimes(1);
+ expect(setCacheTimeouts).toHaveBeenCalledWith({
+ TEST: {
+ timeTillCacheInvalidate: 1234,
+ cacheSet: Date.now(),
+ },
+ });
+ });
+ });
+ });
+
+ describe('With a value provided for timeTillCacheInvalidate globally from context provider', () => {
+ beforeEach(() => {
+ mockUseContext.mockImplementation(() => ({
+ cacheTimeouts: {},
+ setCacheTimeouts,
+ timeTillCacheInvalidate: 1111,
+ }));
+ });
+
+ describe('With no value provided for timeTillCacheInvalidate locally', () => {
+ it('Returns null when useSelector returns null, and dispatches the action', () => {
+ mockUseSelector.mockImplementation(callback =>
+ callback(testSelector(null)),
+ );
+
+ const { result } = renderHook(() =>
+ useFetchWithRedux(testAction, testSelector),
+ );
+
+ expect(result.current).toEqual(null);
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
+ expect(mockDispatch).toHaveBeenCalledWith(testAction());
+ });
+
+ it('Returns a value when useSelector returns a value, and should not dispatch an action', () => {
+ mockUseSelector.mockImplementation(callback =>
+ callback(testSelector(testSelector(7))),
+ );
+
+ const { result } = renderHook(() =>
+ useFetchWithRedux(testAction, testSelector),
+ );
+
+ expect(result.current).toEqual(7);
+ expect(mockDispatch).not.toHaveBeenCalled();
+ });
+
+ it('Sets cache with the action type, time and timeTillCacheInvalidate from context', () => {
+ mockUseSelector.mockImplementation(callback =>
+ callback(testSelector(testSelector(7))),
+ );
+
+ renderHook(() => useFetchWithRedux(testAction, testSelector));
+
+ expect(setCacheTimeouts).toHaveBeenCalledTimes(1);
+ expect(setCacheTimeouts).toHaveBeenCalledWith({
+ TEST: {
+ timeTillCacheInvalidate: 1111,
+ cacheSet: Date.now(),
+ },
+ });
+ });
+ });
- const { result } = renderHook(() =>
- useFetchWithRedux(testAction, testSelector),
- );
+ describe('With a value provided for timeTillCacheInvalidate locally', () => {
+ it('Returns null when useSelector returns null, and dispatches the action', () => {
+ mockUseSelector.mockImplementation(callback =>
+ callback(testSelector(null)),
+ );
- expect(result.current).toEqual(null);
- expect(mockDispatch).toHaveBeenCalled();
+ const { result } = renderHook(() =>
+ useFetchWithRedux(testAction, testSelector, {
+ timeTillCacheInvalidate: 1234,
+ }),
+ );
+
+ expect(result.current).toEqual(null);
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
+ expect(mockDispatch).toHaveBeenCalledWith(testAction());
+ });
+
+ it('Returns a value when useSelector returns a value, and should not dispatch an action', () => {
+ mockUseSelector.mockImplementation(callback =>
+ callback(testSelector(testSelector(7))),
+ );
+
+ const { result } = renderHook(() =>
+ useFetchWithRedux(testAction, testSelector),
+ );
+
+ expect(result.current).toEqual(7);
+ expect(mockDispatch).not.toHaveBeenCalled();
+ });
+
+ it('Sets cache with the action type, time and timeTillCacheInvalidate from the hook input', () => {
+ mockUseSelector.mockImplementation(callback =>
+ callback(testSelector(testSelector(7))),
+ );
+
+ renderHook(() =>
+ useFetchWithRedux(testAction, testSelector, {
+ timeTillCacheInvalidate: 1234,
+ }),
+ );
+
+ expect(setCacheTimeouts).toHaveBeenCalledTimes(1);
+ expect(setCacheTimeouts).toHaveBeenCalledWith({
+ TEST: {
+ timeTillCacheInvalidate: 1234,
+ cacheSet: Date.now(),
+ },
+ });
+ });
+ });
+ });
});
- it('Returns value from state and does not call dispatch when selector returns data', () => {
- mockUseSelector.mockImplementation(callback =>
- callback(testSelector(7)),
- );
+ describe('When cache has already been set', () => {
+ beforeEach(() => {
+ mockUseContext.mockImplementation(() => ({
+ cacheTimeouts: {
+ TEST: {
+ timeTillCacheInvalidate: 1234,
+ cacheSet: Date.now(),
+ },
+ },
+ setCacheTimeouts,
+ timeTillCacheInvalidate: 2222,
+ }));
+
+ mockUseSelector.mockImplementation(callback =>
+ callback(testSelector(4444)),
+ );
+ });
+
+ describe('Before it has expired', () => {
+ beforeEach(() => {
+ mockGetRemainingCacheTime.mockImplementation(() => 999999);
+ });
+
+ describe('With no timeTillCacheInvalidate included in the hooks options', () => {
+ it('Returns the value from useSelector', () => {
+ const { result } = renderHook(() =>
+ useFetchWithRedux(testAction, testSelector),
+ );
+
+ expect(result.current).toEqual(4444);
+ });
+
+ it('Does not dispatch an action', () => {
+ renderHook(() => useFetchWithRedux(testAction, testSelector));
+ expect(mockDispatch).not.toHaveBeenCalled();
+ });
+
+ it('Should not make a call to setCacheTimeouts to update the cache', () => {
+ renderHook(() => useFetchWithRedux(testAction, testSelector));
+ expect(setCacheTimeouts).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('When timeTillCacheInvalidate is included in the hooks options', () => {
+ describe('and is less than than the remaining time till the cache is invalid', () => {
+ it('Should make a call to setCacheTimeouts to update the cache to the value provided by the options value', () => {
+ renderHook(() =>
+ useFetchWithRedux(testAction, testSelector, {
+ timeTillCacheInvalidate: 6464,
+ }),
+ );
+
+ expect(setCacheTimeouts).toHaveBeenCalledTimes(1);
+ expect(setCacheTimeouts).toHaveBeenCalledWith({
+ TEST: {
+ timeTillCacheInvalidate: 6464,
+ cacheSet: Date.now(),
+ },
+ });
+ });
+
+ it('Should not dispatch an action', () => {
+ renderHook(() =>
+ useFetchWithRedux(testAction, testSelector, {
+ timeTillCacheInvalidate: 6464,
+ }),
+ );
+
+ expect(mockDispatch).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('and is greater than than the remaining time till the cache is invalid', () => {
+ beforeEach(() => {
+ mockGetRemainingCacheTime.mockImplementation(() => 123);
+ });
+
+ it('Should not make a call to setCacheTimeouts', () => {
+ renderHook(() =>
+ useFetchWithRedux(testAction, testSelector, {
+ timeTillCacheInvalidate: 6464,
+ }),
+ );
+
+ expect(setCacheTimeouts).not.toHaveBeenCalled();
+ });
+
+ it('Should not dispatch an action', () => {
+ renderHook(() =>
+ useFetchWithRedux(testAction, testSelector, {
+ timeTillCacheInvalidate: 6464,
+ }),
+ );
+
+ expect(mockDispatch).not.toHaveBeenCalled();
+ });
+ });
+ });
+ });
+
+ describe('After it has expired', () => {
+ beforeEach(() => {
+ mockGetRemainingCacheTime.mockImplementation(() => 0);
+ });
+
+ describe('With no timeTillCacheInvalidate included in the hooks options', () => {
+ it('Returns the value from useSelector', () => {
+ const { result } = renderHook(() =>
+ useFetchWithRedux(testAction, testSelector),
+ );
+
+ expect(result.current).toEqual(4444);
+ });
+
+ it('Dispatches an action', () => {
+ renderHook(() => useFetchWithRedux(testAction, testSelector));
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
+ expect(mockDispatch).toHaveBeenCalledWith(testAction());
+ });
+
+ it('Should make a call to setCacheTimeouts to update the cache', () => {
+ renderHook(() => useFetchWithRedux(testAction, testSelector));
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
+ expect(setCacheTimeouts).toHaveBeenCalledWith({
+ TEST: {
+ timeTillCacheInvalidate: 2222,
+ cacheSet: Date.now(),
+ },
+ });
+ });
+ });
+
+ describe('When timeTillCacheInvalidate is also included in the hooks options', () => {
+ it('Returns the value from useSelector', () => {
+ const { result } = renderHook(() =>
+ useFetchWithRedux(testAction, testSelector),
+ );
+
+ expect(result.current).toEqual(4444);
+ });
+
+ it('Should make a call to setCacheTimeouts to update the cache to the value provided by the options value', () => {
+ renderHook(() =>
+ useFetchWithRedux(testAction, testSelector, {
+ timeTillCacheInvalidate: 6464,
+ }),
+ );
+
+ expect(setCacheTimeouts).toHaveBeenCalledTimes(1);
+ expect(setCacheTimeouts).toHaveBeenCalledWith({
+ TEST: {
+ timeTillCacheInvalidate: 6464,
+ cacheSet: Date.now(),
+ },
+ });
+ });
- const { result } = renderHook(() =>
- useFetchWithRedux(testAction, testSelector),
- );
+ it('Dispatches the action', () => {
+ renderHook(() => useFetchWithRedux(testAction, testSelector));
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
+ expect(mockDispatch).toHaveBeenCalledWith(testAction());
+ });
- expect(result.current).toEqual(7);
- expect(mockDispatch).not.toHaveBeenCalled();
+ it('Should make a call to setCacheTimeouts to update the cache', () => {
+ renderHook(() => useFetchWithRedux(testAction, testSelector));
+ expect(setCacheTimeouts).toHaveBeenCalledTimes(1);
+ expect(setCacheTimeouts).toHaveBeenCalledWith({
+ TEST: {
+ timeTillCacheInvalidate: 2222,
+ cacheSet: Date.now(),
+ },
+ });
+ });
+ });
+ });
});
});
diff --git a/src/index.ts b/src/index.ts
deleted file mode 100644
index 7253de0..0000000
--- a/src/index.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { useSelector, useDispatch } from 'react-redux';
-import { useEffect } from 'react';
-import { Action } from 'redux';
-
-function useFetchWithRedux(
- getDataStart: () => Action,
- selector: (state: State) => Selected,
-) {
- const dispatch = useDispatch();
- const selected = useSelector(selector);
-
- useEffect(() => {
- if (selected === null) {
- dispatch(getDataStart());
- }
- }, [dispatch]);
-
- return selected;
-}
-
-export { useFetchWithRedux };
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100644
index 0000000..1184105
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,73 @@
+import { useContext } from 'react';
+import { useSelector, useDispatch, shallowEqual } from 'react-redux';
+import { useEffect } from 'react';
+import { Action } from 'redux';
+import {
+ ReactUseFetchWithReduxContext,
+ ReactUseFetchWithReduxProvider,
+} from './Provider';
+import { getRemainingCacheTime } from './utils';
+import { Options } from './types';
+
+function useFetchWithRedux(
+ getDataStart: () => Action,
+ selector: (state: State) => Selected,
+ options?: Options,
+) {
+ const dispatch = useDispatch();
+ const selected = useSelector(selector, shallowEqual);
+ const {
+ cacheTimeouts,
+ setCacheTimeouts,
+ timeTillCacheInvalidate: timeTillCacheInvalidateGlobal,
+ } = useContext(ReactUseFetchWithReduxContext);
+
+ useEffect(() => {
+ const cacheIndex = getDataStart().type;
+ const isCacheSet = Object.keys(cacheTimeouts).includes(cacheIndex);
+ const remainingCacheTime = getRemainingCacheTime(cacheTimeouts, cacheIndex);
+ const timeTillCacheInvalidate =
+ options?.timeTillCacheInvalidate ?? timeTillCacheInvalidateGlobal ?? null;
+
+ if (!isCacheSet && !selected) {
+ dispatch(getDataStart());
+ }
+
+ const cacheShouldBeOverwritten =
+ isCacheSet &&
+ options?.timeTillCacheInvalidate &&
+ options?.timeTillCacheInvalidate < remainingCacheTime;
+
+ if ((timeTillCacheInvalidate && !isCacheSet) || cacheShouldBeOverwritten) {
+ setCacheTimeouts({
+ ...cacheTimeouts,
+ [cacheIndex]: {
+ timeTillCacheInvalidate,
+ cacheSet: Date.now(),
+ },
+ });
+ }
+
+ if (isCacheSet && remainingCacheTime === 0) {
+ dispatch(getDataStart());
+ setCacheTimeouts({
+ ...cacheTimeouts,
+ [cacheIndex]: {
+ timeTillCacheInvalidate,
+ cacheSet: Date.now(),
+ },
+ });
+ }
+ }, [
+ cacheTimeouts,
+ dispatch,
+ getDataStart,
+ options,
+ selected,
+ setCacheTimeouts,
+ ]);
+
+ return selected;
+}
+
+export { useFetchWithRedux, ReactUseFetchWithReduxProvider };
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..c458920
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,10 @@
+export type Options = {
+ timeTillCacheInvalidate: number;
+};
+
+export type CacheTimeouts = {
+ [key: string]: {
+ timeTillCacheInvalidate: number;
+ cacheSet: number;
+ };
+};
diff --git a/src/utils/getRemainingCacheTime.spec.ts b/src/utils/getRemainingCacheTime.spec.ts
new file mode 100644
index 0000000..24744f9
--- /dev/null
+++ b/src/utils/getRemainingCacheTime.spec.ts
@@ -0,0 +1,43 @@
+import getRemainingCacheTime from './getRemainingCacheTime';
+import { CacheTimeouts } from '../types';
+
+describe('getRemainingCacheTime', () => {
+ beforeEach(() => {
+ Date.now = () => 20;
+ });
+
+ const cacheTimeouts: CacheTimeouts = {
+ TEST_ACTION_1: {
+ cacheSet: 5,
+ timeTillCacheInvalidate: 10,
+ },
+ TEST_ACTION_2: {
+ cacheSet: 10,
+ timeTillCacheInvalidate: 10,
+ },
+ TEST_ACTION_3: {
+ cacheSet: 15,
+ timeTillCacheInvalidate: 10,
+ },
+ };
+
+ it('Returns 0 if the remaining time is negative', () => {
+ const result = getRemainingCacheTime(cacheTimeouts, 'TEST_ACTION_1');
+ expect(result).toEqual(0);
+ });
+
+ it('Returns 0 if the remaining time is 0', () => {
+ const result = getRemainingCacheTime(cacheTimeouts, 'TEST_ACTION_2');
+ expect(result).toEqual(0);
+ });
+
+ it('Returns correct value if larger than 0', () => {
+ const result = getRemainingCacheTime(cacheTimeouts, 'TEST_ACTION_3');
+ expect(result).toEqual(5);
+ });
+
+ it('Returns 0 if the cache is not set', () => {
+ const result = getRemainingCacheTime(cacheTimeouts, 'TEST_ACTION_4');
+ expect(result).toEqual(0);
+ });
+});
diff --git a/src/utils/getRemainingCacheTime.ts b/src/utils/getRemainingCacheTime.ts
new file mode 100644
index 0000000..e0d5307
--- /dev/null
+++ b/src/utils/getRemainingCacheTime.ts
@@ -0,0 +1,18 @@
+import { CacheTimeouts } from '../types';
+
+const getRemainingCacheTime = (
+ cacheTimeouts: CacheTimeouts,
+ cacheIndex: string,
+) => {
+ if (!cacheTimeouts[cacheIndex]) {
+ return 0;
+ }
+
+ const { cacheSet, timeTillCacheInvalidate } = cacheTimeouts[cacheIndex];
+
+ const timeRemaining = timeTillCacheInvalidate + cacheSet - Date.now();
+
+ return timeRemaining < 0 ? 0 : timeRemaining;
+};
+
+export default getRemainingCacheTime;
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 0000000..954642f
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1 @@
+export { default as getRemainingCacheTime } from './getRemainingCacheTime';
diff --git a/tsconfig.json b/tsconfig.json
index 8bd8b93..e85d34d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,6 +3,7 @@
"target": "ES6",
"module": "commonjs",
"declaration": true,
+ "jsx": "react",
"outDir": "dist",
"strict": true,
"moduleResolution": "node",