Git Product home page Git Product logo

next-data-hooks's Introduction

next-data-hooks · codecov github status checks bundlephobia

Use getStaticProps and getServerSideProps as react hooks

next-data-hooks is a small and simple lib that lets you write React hooks for data queries in Next.js by lifting static props into React Context.

import { createDataHook } from 'next-data-hooks';

const useBlogPost = createDataHook('BlogPost', async (context) => {
  const { slug } = context.params;

  return; // ... get the blog post
});

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

  return (
    <>
      <h1>{title}</h1>
      <p>{content}</p>
    </>
  );
}

BlogPost.dataHooks = [useBlogPost];

export default BlogPost;

Why?

  1. Writing one large query per page doesn't organize well. Asynchronous data fetching frameworks like apollo, relay, and react-query already allow you to write the queries closer to the component. Why can't static data queries be written closer to the component too?
  2. Works better with TypeScript — when you import a data hook, you're also importing its return type. When you call the hook inside your component, the types are already there.

Why not?

The primary thing this library offers is a pattern organizing getStaticProps/getServerSideProps.

⚠️ Note: It does not offer any more capabilities than vanilla Next.js.

See this question: Why aren't the data hooks parameterized?

Example

See the example in this repo for some ideas on how to organize your static data calls using this hook.

Installation

  1. Install
npm i next-data-hooks

or

yarn add next-data-hooks
  1. Add the babel plugin

At the root, add a .babelrc file that contains the following:

{
  "presets": ["next/babel"],
  "plugins": ["next-data-hooks/babel"]
}

⚠️ Don't forget this step. This enables code elimination to eliminate server-side code in client code.

  1. Add the provider to _app.tsx or _app.js
import { AppProps } from 'next/app';
import { NextDataHooksProvider } from 'next-data-hooks';

function App({ Component, pageProps }: AppProps) {
  const { children, ...rest } = pageProps;

  return (
    <NextDataHooksProvider {...rest}>
      <Component {...rest}>{children}</Component>
    </NextDataHooksProvider>
  );
}

Usage

  1. Create a data hook. This can be in the same file as the component you're using it in or anywhere else.
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;

  // do something async to grab the data your component needs
  const blogPost = /* ... */;

  return blogPost;
});

export default useBlogPost;
TypeScript User?

Note: For TypeScript users, if you're planning on only using the data hook in the context of getServerSideProps, you can import the provided type guard, isServerSidePropsContext, to narrow the type of the incoming context.

import { createDataHook, isServerSidePropsContext } from 'next-data-hooks';

const useServerSideData = createDataHook('Data', async (context) => {
  if (!isServerSidePropsContext(context)) {
    throw new Error('This data hook only works in getServerSideProps.');
  }

  // here, the type of `context` has been narrowed to the server side conext
  const query = context.req.query;
});

export default useServerSideData;
  1. Use the data hook in a component. Add it to a static prop in an array with other data hooks to compose them downward.
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;
  1. Pass the data hooks down in getStaticProps or getServerSideProps.
import { getDataHooksProps } from 'next-data-hooks';
import { GetStaticPaths, GetStaticProps } from 'next';
import BlogPostComponent from '..';

export const getStaticPaths: GetStaticPaths = async (context) => {
  // return static paths...
};

// NOTE: this will also work with `getServerSideProps`
export const getStaticProps: GetStaticProps = async (context) => {
  const dataHooksProps = await getDataHooksProps({
    context,
    // 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 BlogPostComponent;

Useful Patterns

A separate routes directory

Next.js has a very opinionated file-based routing mechanism that doesn't allow you to put a file in the /pages folder without it being considered a page.

Simply put, this doesn't allow for much organization.

With next-data-hooks, you can treat the /pages folder as a folder of entry points and organize files elsewhere.

my-project
# think of the pages folder as entry points to your routes
├── pages
│   ├── blog
│   │   ├── [slug].ts
│   │   └── index.ts
│   └── shop
│       ├── category
│       │   └── [slug].ts
│       ├── index.ts
│       └── product
│           └── [slug].ts
|
# think of each route folder as its own app with it's own components and helpers
└── routes
    ├── blog
    │   ├── components
    │   │   ├── blog-index.tsx
    │   │   ├── blog-post-card.tsx
    │   │   └── blog-post.tsx
    │   └── helpers
    │       └── example-blog-helper.ts
    └── shop
        ├── components
        │   ├── category.tsx
        │   ├── product-description.tsx
        │   └── product.tsx
        └── helpers
            └── example-shop-helper.ts

/routes/blog/components/blog-post.tsx

import { createDataHook } from 'next-data-hooks';

// write your data hook in a co-located place
const useBlogPostData = createDataHook('BlogPost', async (context) => {
  const blogPostData = // get blog post data…
  return blogPostData;
});

function BlogPost() {
  // use it in the component
  const { title, content } = useBlogPostData();

  return (
    <article>
      <h1>{title}</h1>
      <p>{content}</p>
    </article>
  );
}

BlogPost.dataHooks = [useBlogPostData];

export default BlogPost;

/pages/blog/[slug].ts

import { GetStaticProps, GetStaticPaths } from 'next';
import { getDataHooksProps } from 'next-data-hooks';
import BlogPost from 'routes/blog/components/blog-post';

export const getStaticPaths: GetStaticPaths = {}; /* ... */

export const getStaticProps: GetStaticProps = async (context) => {
  const dataHooksProps = getDataHooksProps({
    context,
    dataHooks: BlogPost.dataHooks,
  });
  return { props: dataHooksProps };
};

// re-export your component. this file is just an entry point
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.

Composing data hooks

Each data hook exposes a getData method which is simply the function you pass into createDataHook.

This can be used within other data hooks to pull the same data:

import { createDataHook } from 'next-data-hooks';

const useHook = createDataHook('DataHook', async (context) => {
  return; // ...
});

export default useHook;
import useHook from './';

const useOtherHook = createDataHook('Other', async (context) => {
  const data = await useHook.getData(context);

  // use data to do something…
});

👋 Note: Be aware that this method re-runs the function.

Code elimination

For smaller bundles, Next.js eliminates code that is only intended to run inside getStaticProps.

next-data-hooks does the same by a babel plugin that prefixes your data hook definition with typeof window !== 'undefined' ? <stub> : <real data hook>.

This works because Next.js pre-evaluates the expression typeof window to 'object' in browsers. This will make the above ternary always evaluate to the <stub> in the browser. Terser then shakes away the <real data hook> expression eliminating it from the browser bundle.

If you saw the error Create data hook was run in the browser. then something may have went wrong with the code elimination. Please open an issue.

👋 Note. There may be differences in Next.js's default code elimination and next-data-hooks code elimination. Double check your bundle.

next-data-hooks's People

Contributors

renovate-bot avatar renovate[bot] avatar ricokahler avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

next-data-hooks's Issues

Action Required: Fix Renovate Configuration

There is an error with this repository's Renovate configuration that needs to be fixed. As a precaution, Renovate will stop PRs until it is resolved.

Error type: undefined. Note: this is a nested preset so please contact the preset author if you are unable to fix it yourself.

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Awaiting Schedule

These updates are awaiting their schedule. Click on a checkbox to get an update now.

  • chore(deps): update dependency @types/react to v18.2.46
  • chore(deps): update typescript-eslint monorepo to v6.16.0 (@typescript-eslint/eslint-plugin, @typescript-eslint/parser)

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

github-actions
.github/workflows/test.yml
  • actions/checkout v3
  • actions/setup-node v3
  • codecov/codecov-action v3
npm
package.json
  • @babel/core 7.23.6
  • @babel/node 7.22.19
  • @babel/plugin-transform-runtime 7.23.6
  • @babel/preset-env 7.23.6
  • @babel/preset-react 7.23.3
  • @babel/preset-typescript 7.23.3
  • @ricokahler/exec 1.1.0
  • @rollup/plugin-babel 5.3.1
  • @rollup/plugin-node-resolve 13.3.0
  • @types/common-tags 1.8.4
  • @types/jest 29.5.11
  • @types/react 18.2.45
  • @types/react-dom 18.2.18
  • @types/react-test-renderer 18.0.7
  • @types/webpack 5.28.5
  • @typescript-eslint/eslint-plugin 6.15.0
  • @typescript-eslint/parser 6.15.0
  • @babel/eslint-parser 7.23.3
  • common-tags 1.8.2
  • create-next-app 13.4.19
  • eslint 8.56.0
  • eslint-config-react-app 7.0.1
  • eslint-plugin-flowtype 8.0.3
  • eslint-plugin-import 2.29.1
  • eslint-plugin-jsx-a11y 6.8.0
  • eslint-plugin-react 7.33.2
  • eslint-plugin-react-hooks 4.6.0
  • folder-hash 4.0.4
  • jest 28.1.3
  • next 13.4.19
  • prettier 2.8.8
  • react 18.2.0
  • react-dom 18.2.0
  • react-error-boundary 3.1.4
  • react-test-renderer 18.2.0
  • rollup 2.79.1
  • typescript 4.9.5
  • react ^16.8 || ^17 || ^18.0.0
  • next ^9.3 || ^10 || ^11.0.0 || ^12.0.0 || ^13.0.0

  • Check this box to trigger a request for Renovate to run again on this repository

Not working with latest Next

Great project! Tried to use it with my project, however, it was failing with an error Error: Could not find NextDataHooksContext. Ensure NextDataHooksProvider is configured correctly.

Tried to debug and ended up cloning the repo and running the example. First run, works (next 10.0.1), then proceeded to update Next to 10.1.3 (latest). Hit the exception

Error when attaching a second DataHook

It should be possible to attach more than just one datahook I believe. But I get an error. Can just one DataHook be delivered to getStaticProps()?

export const getStaticProps: GetStaticProps = async (context) => {
  const dataHooksProps = await getDataHooksProps({
    context,
    dataHooks: ParagraphExample.dataHooks,
  });

  const dataHooksProps2 = await getDataHooksProps({
    context,
    dataHooks: BlogPost.dataHooks,
  });

  

  return {
    props: { ...dataHooksProps, dataHooksProps2 },
  };

I also tried:
dataHooks: [ParagraphExample.dataHooks, BlogPost.dataHooks].

Parameter in Call

Thanks for the lib.
I wanted to implement this feature but had been confronted with 2 mature issues, that let me stick to simple function in the comp file, that will be called in a static function within a page.

  1. The useHooks do not carry payloads… this is important to handle specific calls for specific instances of the same component in the same page.

  2. The hook body is more like a static independent function without context… or index. This makes it hard to bind the data two the components if there are more than just one.
    Are there any plans to modify or add payload features in the future ?

generate .dataHooks and getStaticProps with the babel plugin

Component.dataHooks always is an array of the data hooks from other components and the createDataHook function.

similarly getStaticProps, unless extra static props are desired, is also the same every time you write it: calling getDataHooksProps.

couldn't both of these be generated by the babel plugin? from my little experience from using babel, this shouldn't be too tricky to make, and it would reduce a lot of extra code that is copied from page to page, and eliminates the chance of missing a step.

the only issue here would be how to know weather to use getStaticProps or getServerSideProps, since you can't tell that from the hook usage themselves.

i'd like to try to implement this, maybe as an optional thing you need to enable. what are your thoughts?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.