Skip to content

Commit

Permalink
Merge pull request #10 from ricokahler/feat/new-api
Browse files Browse the repository at this point in the history
feat: updated api
  • Loading branch information
ricokahler authored Dec 17, 2020
2 parents 1a2a524 + 33da62e commit 06f6a60
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 125 deletions.
165 changes: 53 additions & 112 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -66,23 +66,56 @@ 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
const blogPost = /* ... */;

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 (
<article>
<h1>{title}</h1>
<p>{content}</p>
<p>
{other} {data}
</p>
</article>
);
}

// 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) => {
Expand All @@ -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 */}
<BlogPostComponent />
</>
);
}
```

3. Use the data hook in any component under that page.

```tsx
import { useBlogPost } from '..';

function BlogPostComponent() {
const { title, content } = useBlogPost();

return (
<article>
<h1>{title}</h1>
<p>{content}</p>
</article>
);
}
export default BlogPostComponent;
```

## Useful Patterns
Expand Down Expand Up @@ -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;
});
Expand All @@ -197,6 +208,8 @@ function BlogPost() {
);
}

BlogPost.dataHooks = [useBlogPostData]

export default BlogPost;
```

Expand All @@ -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 };
};

Expand All @@ -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 (
<>
<Header />
<main>{children}</main>
</>
);
}

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`.
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
29 changes: 28 additions & 1 deletion src/create-data-hook.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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(
<ErrorBoundary fallback={<></>} onError={handleError}>
<NextDataHooksContext.Provider value={{}}>
<Foo />
</NextDataHooksContext.Provider>
</ErrorBoundary>
);
});

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.]`
);
});
9 changes: 8 additions & 1 deletion src/create-data-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ function createDataHook<R>(
'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, {
Expand Down
9 changes: 6 additions & 3 deletions src/get-data-hooks-props.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
Expand Down Expand Up @@ -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.]`
Expand All @@ -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;
Expand Down
13 changes: 9 additions & 4 deletions src/get-data-hooks-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@ type DataHook = ReturnType<typeof createDataHook>;

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.`
Expand All @@ -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];
})
Expand Down
4 changes: 2 additions & 2 deletions src/next-data-hooks-provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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(() => {
Expand Down

0 comments on commit 06f6a60

Please sign in to comment.