diff --git a/README.md b/README.md
index 0f63d49c..044c4c06 100644
--- a/README.md
+++ b/README.md
@@ -8,11 +8,11 @@ Writing one large query per page doesn't organize well. Asynchronous data fetchi
Why can't static data queries be written closer to the component too?
-`next-data-hooks` is a small and simple lib that lets you write React hooks for static data queries in next.js by lifting static props into React Context.
+`next-data-hooks` is a small and simple lib that lets you write React hooks for static data queries in Next.js by lifting static props into React Context.
## Example
-See [the example in this repo](https://github.com/ricokahler/next-data-hooks/tree/main/examples/next-data-hooks-example) for some ideas on how to organize your static data call using this hook.
+See [the example in this repo](https://github.com/ricokahler/next-data-hooks/tree/main/examples/next-data-hooks-example) for some ideas on how to organize your static data calls using this hook.
## Installation
@@ -66,8 +66,8 @@ At the root, add a `.babelrc` file that contains the following:
import { createDataHook } from 'next-data-hooks';
// this context is the GetStaticPropsContext from 'next'
-// π
-export const useBlogPost = createDataHook('BlogPost', async (context) => {
+// π
+const useBlogPost = createDataHook('BlogPost', async (context) => {
const slug = context.params?.slug as string;
// do something async to grab the data your component needs
@@ -75,14 +75,47 @@ export const useBlogPost = createDataHook('BlogPost', async (context) => {
return blogPost;
});
+
+export default useBlogPost;
```
-2. Get the data hooks props and pass it down in `getStaticProps`. Import all data hooks.
+2. Use the data hook in a component. Add it to a static prop in an array with other data hooks to compose them downward.
+
+```tsx
+import ComponentThatUsesDataHooks from '..';
+import useBlogPost from '..';
+import useOtherDataHook from '..';
+
+function BlogPostComponent() {
+ const { title, content } = useBlogPost();
+ const { other, data } = useOtherDataHook();
+
+ return (
+
+ {title}
+ {content}
+
+ {other} {data}
+
+
+ );
+}
+
+// compose together other data hooks
+BlogPostComponent.dataHooks = [
+ ...ComponentThatUsesDataHooks.dataHooks,
+ useOtherDataHooks,
+ useBlogPost,
+];
+
+export default BlogPostComponent;
+```
+
+3. Pass the data hooks down in `getStaticProps`.
```tsx
import { getDataHooksProps } from 'next-data-hooks';
import { GetStaticPaths, GetStaticProps } from 'next';
-import { useBlogPost } from '..';
import BlogPostComponent from '..';
export const getStaticPaths: GetStaticPaths = async (context) => {
@@ -92,44 +125,22 @@ export const getStaticPaths: GetStaticPaths = async (context) => {
export const getStaticProps: GetStaticProps = async (context) => {
const dataHooksProps = await getDataHooksProps({
context,
- // you can add more than one here
- // πππ
- hooks: [useBlogPost],
+ // this is an array of all data hooks from the `dataHooks` static prop.
+ // πππ
+ dataHooks: BlogPostComponent.dataHooks,
});
return {
props: {
// spread the props required by next-data-hooks
...dataHooksProps,
+
+ // add additional props to Next.js here
},
};
};
-export default function BlogPostEntry() {
- return (
- <>
- {/* Note: this component doesn't have to be a direct child of BlogPostEntry */}
-
- >
- );
-}
-```
-
-3. Use the data hook in any component under that page.
-
-```tsx
-import { useBlogPost } from '..';
-
-function BlogPostComponent() {
- const { title, content } = useBlogPost();
-
- return (
-
- {title}
- {content}
-
- );
-}
+export default BlogPostComponent;
```
## Useful Patterns
@@ -180,7 +191,7 @@ my-project
import { createDataHook } from 'next-data-hooks';
// write your data hook in a co-located place
-export const useBlogPostData = createDataHook('BlogPost', async (context) => {
+const useBlogPostData = createDataHook('BlogPost', async (context) => {
const blogPostData = // get blog post dataβ¦
return blogPostData;
});
@@ -197,6 +208,8 @@ function BlogPost() {
);
}
+BlogPost.dataHooks = [useBlogPostData]
+
export default BlogPost;
```
@@ -205,12 +218,15 @@ export default BlogPost;
```ts
import { GetStaticProps, GetStaticPaths } from 'next';
import { getDataHooksProps } from 'next-data-hooks';
-import BlogPost, { useBlogPost } from 'routes/blog/components/blog-post';
+import BlogPost from 'routes/blog/components/blog-post';
export const getStaticPaths: GetStaticPaths = {}; /* ... */
export const getStaticProps: GetStaticProps = async (context) => {
- const dataHooksProps = getDataHooksProps({ context, hooks: [useBlogPost] });
+ const dataHooksProps = getDataHooksProps({
+ context,
+ dataHooks: BlogPost.dataHooks,
+ });
return { props: dataHooksProps };
};
@@ -220,81 +236,6 @@ export default BlogPost;
> **π Note:** the above is just an example of how you can use `next-data-hooks` to organize your project. The main takeaway is that you can re-export page components to change the structure and `next-data-hooks` works well with this pattern.
-### Co-located queries
-
-This pattern can be particularly useful if you're writing a component that requires dynamic data but you don't want to worry about how that data gets to your component.
-
-For example, let's say you have a `Header` component that's nested in a `Layout` component.
-
-With `next-data-hooks`, you write the query closer to the component.
-
-#### `header.tsx`
-
-```tsx
-import { createDataHook } from 'next-data-hooks';
-
-// Write a query closer to the component
-export const useHeaderData = createDataHook('Header', async (context) => {
- // pull header data...
-});
-
-function Header() {
- const headerData = useHeaderData();
-
- return <>{/* use `headerData` */}>;
-}
-
-export default Header;
-```
-
-#### `layout.tsx`
-
-Then you can use the component anywhere else in your component tree. Note how this component is unaware of the header data.
-
-```tsx
-import Header from './header';
-
-interface Props {
- // ...
-}
-
-function Layout({ children }: Props) {
- return (
- <>
-
- {children}
- >
- );
-}
-
-export default Layout;
-```
-
-#### `my-page.tsx`
-
-Finally, wire-up the hooks in one place.
-
-```tsx
-// my-page.tsx
-import { GetStaticProps } from 'next';
-import { useHeaderData } from 'components/header';
-import MyPage from 'routes/my-page';
-
-export const getStaticProps: GetStaticProps = async (context) => {
- const dataHooksProps = await getDataHooksProps({
- context,
- // include it once here and it'll wire up the data hook wherever it's used
- hooks: [useHeaderData],
- });
-
- return {
- props: { ...dataHooksProps },
- };
-};
-
-export default MyPage;
-```
-
## Code elimination
For smaller bundles, Next.js eliminates code that is only intended to run inside `getStaticProps`.
diff --git a/package-lock.json b/package-lock.json
index 136163eb..496b0bcf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "next-data-hooks",
- "version": "0.2.1",
+ "version": "0.3.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index 461bfbc9..574ea958 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "next-data-hooks",
- "version": "0.2.1",
+ "version": "0.3.0",
"description": "Use `getStaticProps` as react hooks",
"private": true,
"scripts": {
diff --git a/src/create-data-hook.test.tsx b/src/create-data-hook.test.tsx
index 3b730453..626d1a2c 100644
--- a/src/create-data-hook.test.tsx
+++ b/src/create-data-hook.test.tsx
@@ -55,7 +55,7 @@ it('returns a hook that pulls from the given key', () => {
`);
});
-it('throws if the data key could not be found', () => {
+it('throws if the context not be found', () => {
const useData = createDataHook('DataKey', async () => null);
const handleError = jest.fn();
@@ -79,3 +79,30 @@ it('throws if the data key could not be found', () => {
`"Could not find \`NextDataHooksContext\`. Ensure \`NextDataHooksProvider\` is configured correctly."`
);
});
+
+it('throws if a data hook could not be found', () => {
+ const useData = createDataHook('DataKey', () => null);
+ const handleError = jest.fn();
+
+ function Foo() {
+ useData();
+
+ return null;
+ }
+
+ act(() => {
+ create(
+ >} onError={handleError}>
+
+
+
+
+ );
+ });
+
+ expect(handleError).toHaveBeenCalled();
+ const error = handleError.mock.calls[0][0];
+ expect(error).toMatchInlineSnapshot(
+ `[Error: Did not find a data hook named "DataKey". Ensure it was provided to getDataHooksProps.]`
+ );
+});
diff --git a/src/create-data-hook.ts b/src/create-data-hook.ts
index 734c83fe..834a2aef 100644
--- a/src/create-data-hook.ts
+++ b/src/create-data-hook.ts
@@ -26,7 +26,14 @@ function createDataHook(
'Could not find `NextDataHooksContext`. Ensure `NextDataHooksProvider` is configured correctly.'
);
}
- return dataHooksContext[key];
+ const dataHooksValue = dataHooksContext[key];
+ if (!Object.keys(dataHooksContext).includes(key)) {
+ throw new Error(
+ `Did not find a data hook named "${key}". Ensure it was provided to getDataHooksProps.`
+ );
+ }
+
+ return dataHooksValue;
}
return Object.assign(useData, {
diff --git a/src/get-data-hooks-props.test.tsx b/src/get-data-hooks-props.test.tsx
index fd62b5cd..fac86a80 100644
--- a/src/get-data-hooks-props.test.tsx
+++ b/src/get-data-hooks-props.test.tsx
@@ -17,7 +17,7 @@ it('returns a getStaticProps function that pulls and populates props', async ()
const result = await getDataHooksProps({
context: mockContext,
- hooks: [useFoo, useBar],
+ dataHooks: [useFoo, useBar],
});
expect(result).toMatchInlineSnapshot(`
@@ -52,7 +52,10 @@ it('throws if it encounters two data hooks with the same key', async () => {
let caught = false;
try {
- await getDataHooksProps({ context: mockContext, hooks: [useFoo, useBar] });
+ await getDataHooksProps({
+ context: mockContext,
+ dataHooks: [useFoo, useBar],
+ });
} catch (e) {
expect(e).toMatchInlineSnapshot(
`[Error: Found duplicate hook key "Hook". Ensure all hook keys per \`createDatHooksProps\` call are unique.]`
@@ -72,7 +75,7 @@ it('throws if it the stub function is run', async () => {
try {
await getDataHooksProps({
context: mockContext,
- hooks: [useFoo],
+ dataHooks: [useFoo],
});
} catch (e) {
caught = true;
diff --git a/src/get-data-hooks-props.ts b/src/get-data-hooks-props.ts
index 7a447485..2064916b 100644
--- a/src/get-data-hooks-props.ts
+++ b/src/get-data-hooks-props.ts
@@ -5,16 +5,21 @@ type DataHook = ReturnType;
interface Params {
context: GetStaticPropsContext;
- hooks: DataHook[];
+ dataHooks: DataHook[];
}
/**
* Pulls the data from the next-data-hooks and returns the props to be received
* by the NextDataHooksProvider
*/
-async function getDataHooksProps({ hooks, context }: Params) {
+async function getDataHooksProps({ dataHooks, context }: Params) {
const hookKeys: { [key: string]: boolean } = {};
- for (const hook of hooks) {
+
+ // we allow the same function reference to be added to the array more than
+ // once so we de-dupe here
+ const deDupedHooks = Array.from(new Set(dataHooks));
+
+ for (const hook of deDupedHooks) {
if (hookKeys[hook.key]) {
throw new Error(
`Found duplicate hook key "${hook.key}". Ensure all hook keys per \`createDatHooksProps\` call are unique.`
@@ -24,7 +29,7 @@ async function getDataHooksProps({ hooks, context }: Params) {
}
const entries = await Promise.all(
- hooks.map(async (hook) => {
+ dataHooks.map(async (hook) => {
const data = await hook.getData(context);
return [hook.key, data] as [string, any];
})
diff --git a/src/next-data-hooks-provider.test.tsx b/src/next-data-hooks-provider.test.tsx
index be09f478..9361ecbd 100644
--- a/src/next-data-hooks-provider.test.tsx
+++ b/src/next-data-hooks-provider.test.tsx
@@ -6,7 +6,7 @@ import getDataHooksProps from './get-data-hooks-props';
import NextDataHooksProvider from './next-data-hooks-provider';
it('Injects the data from data hooks into React Context.', async () => {
- const useData = createDataHook('DataKey', () => ({ hello: 'world' }));
+ const useData = createDataHook('DataKey', async () => ({ hello: 'world' }));
const dataHandler = jest.fn();
function Foo() {
@@ -22,7 +22,7 @@ it('Injects the data from data hooks into React Context.', async () => {
const mockContext: GetStaticPropsContext = { params: { mock: 'context' } };
const props = await getDataHooksProps({
context: mockContext,
- hooks: [useData],
+ dataHooks: [useData],
});
act(() => {