Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: migrates to monorepo #5

Merged
merged 4 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ jobs:
- name: Build package
run: npm run build

- name: Release package
- name: Release @hyperjumptech/react-next-pathname
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
working-directory: apps/react

- name: Release @hyperjumptech/react-next-pathname-nextjs
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
working-directory: apps/next
50 changes: 32 additions & 18 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# Dependencies
node_modules
.pnp
.pnp.js

# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Testing
coverage

# Turbo
.turbo

# Vercel
.vercel

# Build Outputs
.next/
out/
build
dist
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea

# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Misc
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.pem
Empty file added .npmrc
Empty file.
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

![Minified size](https://img.shields.io/bundlephobia/min/@hyperjumptech/react-next-pathname) ![Test coverage](https://img.shields.io/codecov/c/github/hyperjumptech/react-next-pathname) ![Monthly download](https://img.shields.io/npm/dm/@hyperjumptech/react-next-pathname)

This is a helper tool you can use in your React application to make it feel faster and smoother when navigating between pages. Say you have a sidebar that contains links to different pages. When you click a link, the component that displays the link will show that it's currently active—usually like this:
`@hyperjumptech/react-next-pathname` is a helper tool designed to enhance the user experience in React applications by improving the responsiveness and feedback when navigating between pages. It ensures that the active state of links in components like sidebars is updated immediately upon clicking, even if the new page is slow to load.

## Problem

When a user clicks a link, the typical implementation updates the link's active state based on the current pathname, which only changes after the new page loads. This delay can confuse users, making them think that their click did not register. For example:

```tsx
const isPathActive = (pathname: string) => {
Expand All @@ -15,15 +19,12 @@ const Sidebar = () => {
{[
{ pathname: "/", title: "Home" },
{ pathname: "/about", title: "About" },
{
pathname: "/contact",
title: "Contact",
},
{ pathname: "/contact", title: "Contact" },
].map(({ pathname, title }) => (
<a
key={pathname}
href={pathname}
className={`${ispathActive(pathname) ? "active" : ""}`}
className={`${isPathActive(pathname) ? "active" : ""}`}
>
{title}
</a>
Expand All @@ -33,9 +34,9 @@ const Sidebar = () => {
};
```

The problem is that when the clicked page is slow to load due to bad network conditions or slow server response, the clicked link will not immediately show that it's active. This is because the `window.location.pathname` is not updated until the page has fully loaded. Users might think that nothing is happening when they click the link, and they will be confused.
## Solution

This library helps you solve this problem by allowing you to get the next pathname immediately when a link is clicked, without waiting for the new page to load.
`@hyperjumptech/react-next-pathname` solves this problem by providing the next pathname immediately when a link is clicked, without waiting for the new page to load. This ensures that the active state is updated right away, providing immediate feedback to the user.

## Installation

Expand Down Expand Up @@ -96,15 +97,12 @@ const Sidebar = () => {
{[
{ pathname: "/", title: "Home" },
{ pathname: "/about", title: "About" },
{
pathname: "/contact",
title: "Contact",
},
{ pathname: "/contact", title: "Contact" },
].map(({ pathname, title }) => (
<a
key={pathname}
href={pathname}
className={`${ispathActive(pathname, nextPathname) ? "active" : ""}`}
className={`${isPathActive(pathname, nextPathname) ? "active" : ""}`}
>
{title}
</a>
Expand All @@ -114,6 +112,10 @@ const Sidebar = () => {
};
```

## Next.js Support

For Next.js applications, use the package `@hyperjumptech/react-next-pathname-nextjs`.

## License

[MIT License](/LICENSE)
46 changes: 46 additions & 0 deletions apps/next/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@hyperjumptech/react-next-pathname-nextjs",
"description": "Instantly tracks the next pathname on link click.",
"version": "1.0.2",
"main": "dist/index.mjs",
"typings": "dist/index.d.mts",
"scripts": {
"build": "tsup",
"test": "vitest run",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^16.0.0",
"@vitest/coverage-v8": "^1.6.0",
"jsdom": "^24.1.0",
"tsup": "^8.1.0",
"vitest": "^1.6.0"
},
"peerDependencies": {
"next": "*",
"react": "*",
"react-dom": "*"
},
"files": [
"dist"
],
"license": "MIT",
"author": "Kevin Hermawan",
"repository": {
"type": "git",
"url": "git+https://github.com/hyperjumptech/react-next-pathname.git"
},
"bugs": {
"url": "https://github.com/hyperjumptech/react-next-pathname/issues"
},
"homepage": "https://github.com/hyperjumptech/react-next-pathname#readme",
"keywords": [
"react",
"pathname",
"navigation",
"link",
"routing",
"next pathname"
]
}
38 changes: 38 additions & 0 deletions apps/next/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";

import { useRouter } from "next/router.js";
import type { ReactNode } from "react";
import React, { createContext, useContext, useEffect, useState } from "react";

type NextPathnameContextType = {
nextPathname: string;
};

const defaultNextPathname = "/";

export const NextPathnameContext = createContext<NextPathnameContextType>({
nextPathname: defaultNextPathname,
});

export function NextPathnameProvider({ children }: { children: ReactNode }) {
const router = useRouter();
const [nextPathname, setNextPathname] = useState(router.pathname);

useEffect(() => {
router.events.on("routeChangeStart", setNextPathname);

return () => {
router.events.off("routeChangeStart", setNextPathname);
};
}, [router.events]);

return (
<NextPathnameContext.Provider value={{ nextPathname }}>
{children}
</NextPathnameContext.Provider>
);
}

export function useNextPathname() {
return useContext(NextPathnameContext);
}
88 changes: 88 additions & 0 deletions apps/next/test/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { RouterEvent, useRouter } from "next/router";
import React from "react";
import { afterEach, describe, expect, it, Mock, vi } from "vitest";

import { act, cleanup, render, waitFor } from "@testing-library/react";

import { NextPathnameProvider, useNextPathname } from "../src";

vi.mock("next/router", () => {
const originalModule = vi.importActual(
"next/router"
) as unknown as typeof import("next/router");

return {
__esModule: true,
...originalModule,
useRouter: vi.fn(),
};
});

type MockRouter = {
pathname: string;
events: {
on: (event: RouterEvent, handler: (...args: any[]) => void) => void;
off: (event: RouterEvent, handler: (...args: any[]) => void) => void;
emit: (event: RouterEvent, ...args: any[]) => void;
};
mockHandler?: (...args: any[]) => void;
};

const mockRouter: MockRouter = {
pathname: "/",
events: {
on: vi.fn((event, handler) => {
if (event === "routeChangeStart") {
mockRouter.mockHandler = handler;
}
}),
off: vi.fn(),
emit: vi.fn(),
},
};

(useRouter as Mock).mockImplementation(() => mockRouter);

describe("NextPathnameProvider", () => {
afterEach(() => {
cleanup();

mockRouter.pathname = "/";
});

const TestComponent: React.FC = () => {
const { nextPathname } = useNextPathname();

return <div data-testid="pathname">{nextPathname}</div>;
};

it("provides the initial pathname", () => {
const { getByTestId } = render(
<NextPathnameProvider>
<TestComponent />
</NextPathnameProvider>
);

expect(getByTestId("pathname").textContent).toBe("/");
});

it("updates the pathname on route change", async () => {
render(
<NextPathnameProvider>
<TestComponent />
</NextPathnameProvider>
);

act(() => {
mockRouter.mockHandler && mockRouter.mockHandler("/new-path");
});

await waitFor(() => {
const pathnameElement = document.querySelector(
'[data-testid="pathname"]'
);

expect(pathnameElement?.textContent).toBe("/new-path");
});
});
});
3 changes: 2 additions & 1 deletion tsconfig.json → apps/next/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
"jsx": "react",
"target": "es5",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"skipLibCheck": true,
"isolatedModules": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["vitest.config.js", ".eslintrc.js", "src"]
"include": [".eslintrc.js", "src"]
}
10 changes: 10 additions & 0 deletions apps/next/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from "tsup";

export default defineConfig({
dts: true,
clean: true,
outDir: "dist",
format: ["esm"],
entry: ["src/index.tsx"],
external: ["react", "react-dom", "next"],
});
43 changes: 43 additions & 0 deletions apps/react/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@hyperjumptech/react-next-pathname",
"description": "Instantly tracks the next pathname on link click.",
"version": "1.0.3",
"main": "dist/index.mjs",
"typings": "dist/index.d.mts",
"scripts": {
"build": "tsup",
"test": "vitest run",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^16.0.0",
"@vitest/coverage-v8": "^1.6.0",
"jsdom": "^24.1.0",
"tsup": "^8.1.0",
"vitest": "^1.6.0"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
},
"files": ["dist"],
"license": "MIT",
"author": "Kevin Hermawan",
"repository": {
"type": "git",
"url": "git+https://github.com/hyperjumptech/react-next-pathname.git"
},
"bugs": {
"url": "https://github.com/hyperjumptech/react-next-pathname/issues"
},
"homepage": "https://github.com/hyperjumptech/react-next-pathname#readme",
"keywords": [
"react",
"pathname",
"navigation",
"link",
"routing",
"next pathname"
]
}
File renamed without changes.
Loading
Loading