From 3bb97c2437c6782fc2319a465d06e5c4fa9f3bce Mon Sep 17 00:00:00 2001 From: Rico Kahler Date: Thu, 17 Dec 2020 15:48:31 -0500 Subject: [PATCH 1/7] throw if no data hook key could be found --- src/create-data-hook.test.tsx | 29 ++++++++++++++++++++++++++++- src/create-data-hook.ts | 9 ++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) 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, { From d6bf15788ae829dede44d92bf06573b0d920a09b Mon Sep 17 00:00:00 2001 From: Rico Kahler Date: Thu, 17 Dec 2020 15:48:42 -0500 Subject: [PATCH 2/7] de-dupe data hook function references --- src/get-data-hooks-props.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/get-data-hooks-props.ts b/src/get-data-hooks-props.ts index 7a447485..e5b92fea 100644 --- a/src/get-data-hooks-props.ts +++ b/src/get-data-hooks-props.ts @@ -14,7 +14,12 @@ interface Params { */ async function getDataHooksProps({ hooks, 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(hooks)); + + 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.` From d4c422b6831b4db4ffe3ad95302d4cb732ae6972 Mon Sep 17 00:00:00 2001 From: Rico Kahler Date: Thu, 17 Dec 2020 15:48:48 -0500 Subject: [PATCH 3/7] bump version --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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": { From 88f8de1792e8ab17aaa8119e235d3b49248f81a0 Mon Sep 17 00:00:00 2001 From: Rico Kahler Date: Thu, 17 Dec 2020 16:06:27 -0500 Subject: [PATCH 4/7] rename to dataHooks --- src/get-data-hooks-props.test.tsx | 9 ++++++--- src/get-data-hooks-props.ts | 8 ++++---- src/next-data-hooks-provider.test.tsx | 4 ++-- 3 files changed, 12 insertions(+), 9 deletions(-) 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 e5b92fea..2064916b 100644 --- a/src/get-data-hooks-props.ts +++ b/src/get-data-hooks-props.ts @@ -5,19 +5,19 @@ 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 } = {}; // 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(hooks)); + const deDupedHooks = Array.from(new Set(dataHooks)); for (const hook of deDupedHooks) { if (hookKeys[hook.key]) { @@ -29,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(() => { From 015d9ab92e32965394ac7cc9db45c5e30dd99c6e Mon Sep 17 00:00:00 2001 From: Rico Kahler Date: Thu, 17 Dec 2020 16:25:10 -0500 Subject: [PATCH 5/7] update readme --- README.md | 159 +++++++++++++++++------------------------------------- 1 file changed, 50 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index 0f63d49c..21d55bc6 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ 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 (if applicable). + +```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. there can be more than one + // πŸ‘‡πŸ‘‡πŸ‘‡ + dataHooks: BlogPostComponent.dataHooks, }); return { props: { // spread the props required by next-data-hooks ...dataHooksProps, + // add additional props 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`. From 1eff0bccbc63d25106b78ebcc1687a2ad0a4d950 Mon Sep 17 00:00:00 2001 From: Rico Kahler Date: Thu, 17 Dec 2020 16:29:46 -0500 Subject: [PATCH 6/7] fix arrows --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 21d55bc6..8045a100 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ At the root, add a `.babelrc` file that contains the following: import { createDataHook } from 'next-data-hooks'; // this context is the GetStaticPropsContext from 'next' -// πŸ‘‡ +// πŸ‘‡ const useBlogPost = createDataHook('BlogPost', async (context) => { const slug = context.params?.slug as string; @@ -127,7 +127,7 @@ export const getStaticProps: GetStaticProps = async (context) => { context, // this is an array of all data hooks from the `dataHooks` // static prop. there can be more than one - // πŸ‘‡πŸ‘‡πŸ‘‡ + // πŸ‘‡πŸ‘‡πŸ‘‡ dataHooks: BlogPostComponent.dataHooks, }); From 33da62e9dd01ca7a3bb3067fd94acfd862c2478a Mon Sep 17 00:00:00 2001 From: Rico Kahler Date: Thu, 17 Dec 2020 16:38:12 -0500 Subject: [PATCH 7/7] update readme --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8045a100..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 @@ -76,10 +76,10 @@ const useBlogPost = createDataHook('BlogPost', async (context) => { return blogPost; }); -export default useBlogPost +export default useBlogPost; ``` -2. Use the data hook in a component. Add it to a static prop in an array with other data hooks (if applicable). +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 '..'; @@ -125,8 +125,7 @@ export const getStaticPaths: GetStaticPaths = async (context) => { export const getStaticProps: GetStaticProps = async (context) => { const dataHooksProps = await getDataHooksProps({ context, - // this is an array of all data hooks from the `dataHooks` - // static prop. there can be more than one + // this is an array of all data hooks from the `dataHooks` static prop. // πŸ‘‡πŸ‘‡πŸ‘‡ dataHooks: BlogPostComponent.dataHooks, }); @@ -135,7 +134,8 @@ export const getStaticProps: GetStaticProps = async (context) => { props: { // spread the props required by next-data-hooks ...dataHooksProps, - // add additional props here + + // add additional props to Next.js here }, }; };