Notes on 3rd round of Next.js official tutorial
Last updated on 1 Oct. 2024
Note: Previous rounds are covered in: Notes on learning Next.js - 1st round and 2nd round.
8 Sept. 2024: Starting 3rd round of Next.js official tutorial. First I am reading through its React tutorial (not trying out). [Update: Finished the 3rd round on 24 Sept. 2024].
How To Create A Next.Js App With Serverless?, https://www.saffrontech.net/blog/how-to-create-a-nextjs-app-with-serverless, Jan. 2024 . Article explains serverless architecture in the context of Next.js. Around half-way down the article, how to create a serverless Next.js app is covered. Special steps are involved (e.g. installing 'serverless' framework and then running 'serverless create ...' command). I guess that means that the projects I have done so far in Next.js are NOT serverless including the official Next.js tutorial.
Chapter 4, Getting Started with React, https://nextjs.org/learn/react-foundations/getting-started-with-react has a nice simple example of using React with a CDN and also using Babel (JSX to JS compiler) with a CDN, and thus have only an HTML file that has React code which will run in a browser. npm etc. are not in the picture at all. This simple file index.html code:
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- Babel Script -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/jsx">
const domNode = document.getElementById('app');
const root = ReactDOM.createRoot(domNode);
root.render(<h1>Develop. Preview. Ship.</h1>);
</script>
</body>
</html>
==================
The above code runs in the browser as expected though it takes some time to load.
Above React code is declarative as compared to equivalent JavaScript code which is imperative.
I checked my earlier Notes-post, https://raviswdev.blogspot.com/2024/04/notes-on-learning-nextjs.html and also some folders in my PC and confirmed that I had tried out some of the code in Chapters 9 and 10, though I seem to have only a modified version of Chatper 9 ("... confirms that the "use client" error that crops up for the same code when compiled in next.js, does not crop up in standard react without next").
Server Components, https://nextjs.org/docs/app/building-your-application/rendering/server-components gives lot of info. about rendering, streaming and hydration . From https://react.dev/reference/react-dom/client/hydrateRoot : "To hydrate your app, React will “attach” your components’ logic to the initial generated HTML from the server. Hydration turns the initial HTML snapshot from the server into a fully interactive app that runs in the browser."
Client Components, https://nextjs.org/docs/app/building-your-application/rendering/client-components is informative. ... Why do Client Components get SSR'd to HTML?, https://github.com/reactwg/server-components/discussions/4 [I think SSR in this context is Server Side Rendering.] ... "How are Client Components Rendered?" section is very useful.
Server and Client Composition Patterns, https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns - very useful.
Finished React Foundations part of tutorial.
Next step is to start with 'Learn Next.js' part of tutorial: https://nextjs.org/learn/dashboard-app
----------------
13 Sep. 2024:
CSS -webkit-appearance, https://www.geeksforgeeks.org/css-webkit-appearance/
::-webkit-inner-spin-button, https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-inner-spin-button
The Complete Guide to Lazy Loading Images, https://css-tricks.com/the-complete-guide-to-lazy-loading-images/
Finished Chapter 5: https://nextjs.org/learn/dashboard-app/navigating-between-pages ... Now plan to study app code of an early version (20240503-nextjs-dashboard) of the tutorial app. that I had created in earlier rounds.
"Use flex-shrink-0 to prevent a flex item from shrinking:" Ref: https://v2.tailwindcss.com/docs/flex-shrink has a nice example demonstrating it.
"Use flex-grow to allow a flex item to grow to fill any available space:" Ref: https://v2.tailwindcss.com/docs/flex-grow
"Use flex-none to prevent a flex item from growing or shrinking:", https://tailwindcss.com/docs/flex
"Use the overflow-auto utility to add scrollbars to an element in the event that its content overflows the bounds of that element. Unlike overflow-scroll, which always shows scrollbars, this utility will only show them if scrolling is necessary.", https://tailwindcss.com/docs/overflow ... "Use the overflow-y-auto utility to allow vertical scrolling if needed."
SideNav component, in md breakpoint, keeps Sign Out link at the bottom of the left panel by using an empty content div which is hidden in < md and becomes block in md with grow. Nav links are above this div and Sign Out is below in md. Also, in md, Nav links and Sign out are made flex-none. So the only flex item in this group that can grow is the empty content div which pushes Sign out to the bottom. h-screen and h-full are used in divs that enclose the main SideNav logo and links. That results in left panel in md taking up full height of window.
TypeScript Generics: What’s with the Angle Brackets <>?, https://javascript.plainenglish.io/typescript-generics-whats-with-the-angle-brackets-4e242c567269 ... Key points that I understood from this article:
* In function definition, what's passed between angle brackets is a 'generic' type
* In function invocation, a specific type is passed within angle brackets and function uses that type.
https://vercel.com/docs/storage/vercel-postgres/sdk - sql ... Construct SQL queries with the sql template literal tag. ... Does not show angle brackets example but gives other sql template literal tag examples.
https://vercel.com/docs/storage/vercel-postgres/quickstart ... Intro to using Vercel Postgres SDK
In 'import { sql } from '@vercel/postgres';' sql is (as per VSCode intellisense)
(alias) const sql: VercelPool & (<O extends QueryResultRow>(strings: TemplateStringsArray, ...values: Primitive[]) => Promise<QueryResult<O>>)
In this usage, ' const data = await sql<Revenue>`SELECT * FROM revenue`;'
Revenue is:
(alias) type Revenue = {
month: string;
revenue: number;
}
----
and data is:
const data: QueryResult<Revenue>
----
So the returned data is of type QueryResult<Revenue>
Digging deeper into above sql type definition:
(alias) seems to be just an addition from VSCode Intellisense. In case of Revenue, the actual code (in lib/definitions.ts) is:
export type Revenue = {
month: string;
revenue: number;
};
----
The above usage is referred to as using a type alias (Revenue is the type alias here). See "Object Types", https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#object-types and "Type Aliases", https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-aliases to see an example of using object type followed by same code but using a type alias.
The sql definition is in node_modules\@vercel\postgres\dist\index.d.ts and is as follows:
declare const sql: VercelPool & (<O extends QueryResultRow>(strings: TemplateStringsArray, ...values: Primitive[]) => Promise<QueryResult<O>>);
...
declare class VercelPool extends Pool {
Client: typeof VercelClient;
private connectionString;
constructor(config: VercelPostgresPoolConfig);
/**
* A template literal tag providing safe, easy to use SQL parameterization.
* Parameters are substituted using the underlying Postgres database, and so must follow
* the rules of Postgres parameterization.
* @example
* ```ts
* const pool = createPool();
* const userId = 123;
* const result = await pool.sql`SELECT * FROM users WHERE id = ${userId}`;
* // Equivalent to: await pool.query('SELECT * FROM users WHERE id = $1', [id]);
* ```
* @returns A promise that resolves to the query result.
*/
sql<O extends QueryResultRow>(strings: TemplateStringsArray, ...values: Primitive[]): Promise<QueryResult<O>>;
connect(): Promise<VercelPoolClient>;
connect(callback: (err: Error, client: VercelPoolClient, done: (release?: any) => void) => void): void;
}
...
From node_modules\@types\pg\index.d.ts :
export interface FieldDef {
name: string;
tableID: number;
columnID: number;
dataTypeID: number;
dataTypeSize: number;
dataTypeModifier: number;
format: string;
}
export interface QueryResultBase {
command: string;
rowCount: number;
oid: number;
fields: FieldDef[];
}
export interface QueryResultRow {
[column: string]: any;
}
export interface QueryResult<R extends QueryResultRow = any> extends QueryResultBase {
rows: R[];
}
---------------------------------
====================
https://www.typescriptlang.org/docs/handbook/declaration-files/by-example.html - covers declare class
Unions and Intersection Types, https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html ... Intersection Types, https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#intersection-types
& is used for intersection type which is a combination of multiple types with the resultant type having members of the all the types combined using intersection.
Figuring out details of sql above is turning out to be too complex. Not worther spending so much time on this, as of now. It is sufficient to know that the angle brackets after sql are associated with parameter type of some sort, and thus the result data from sql is probably typed to the type specified in the angle brackets. I have not been able to locate clear documentation that specifies this but this is what I have gathered from above definitions and reference pages so far.
In app\lib\data.ts fetchRevenue() method, I made the following test change:
const data = await sql<Invoice>`SELECT * FROM revenue`;
// const data = await sql<Revenue>`SELECT * FROM revenue`;
----
This, as expected, resulted in a TypeScript error indication (wavy red line) in app\dashboard\page.tsx in following code:
<RevenueChart revenue={revenue} />
----
revenue outside braces has the wavy red line which expands to:
Type 'Invoice[]' is not assignable to type 'Revenue[]'.
Type 'Invoice' is missing the following properties from type 'Revenue': month, revenue ts(2322)
revenue-chart.tsx(15, 3): The expected type comes from property 'revenue' which is declared here on type 'IntrinsicAttributes & { revenue: Revenue[]; }'
(property) revenue: Revenue[]"
----
I later undid the test changes (and the TS wavy line error indicator went away).
==============
How to understand the definition of a const 'sql' in vercel/postgres package, https://stackoverflow.com/questions/78342254/how-to-understand-the-definition-of-a-const-sql-in-vercel-postgres-package - does not give a proper answer but the issue itself is covered and has some related Vercel types & interfaces info. which may be the same as given above in this notes-post.
==============
app\ui\dashboard\revenue-chart.tsx is an interesting bar chart rendering component. Was able to figure out most of its working (mainly the styling is complex) without too much effort. But I don't think I should go in for such charts myself - it is better to simply explore using free chart components.
===========================
Finished study of main/uncommented part of app code of an early version (20240503-nextjs-dashboard) of the tutorial app. that I had created in earlier rounds.
=================
The current tutorial page for chapter 6, Setting Up Your Database, refers to a seed folder and route.ts file and going to 'localhost:3000/seed' to seed the database. The code I have from the tutorial of some months back has a seed.js file which seems to be a console based program to seed the database. I could not find a way to access the older tutorial page.
My earlier round(s) notes, https://raviswdev.blogspot.com/2024/04/notes-on-learning-nextjs.html , mentions not seeing date probably in Vercel postgres Storage part of Nextjs project page on Vercel - "Seeding data seemed to have only one issue of date column not being created in invoices. " But I later noted that the app itself was showing dates or working when dates were involved.
Now in this round when I see the data on Vercel postgres Storage page in Nextjs project page, I can see the date column entries for invoices! Maybe I missed something in my earlier round(s).
==============
Going through https://nextjs.org/learn/dashboard-app/fetching-data (chapter 7)
====================================
14 Sep. 2024
Have started going through the final nextjs-dashboard project that I had done in earlier round(s). I am doing this in the context of Next.js tutorial chapters 7 to 9 (Fetching Data, 'Static and Dynamic Rendering' and Streaming).
Some parts of app\ui\skeletons.tsx which seems to have all the skeletons used in the app (both at loading.tsx level and at individual components wrapped within Suspense elements and having fallback as component skeletons) are quite complex Tailwind CSS. I felt it is not justified to spend time to understand the complex CSS part and the file in general as I am not sure if I will have to use such a skeleton done manually.
app\ui\dashboard\cards.tsx has interesting use of object properties being accessed with the bracket notation and the value within the brackets being a variable which has an object property name. Related code:
import {
BanknotesIcon,
ClockIcon,
UserGroupIcon,
InboxIcon,
} from '@heroicons/react/24/outline';
...
const iconMap = {
collected: BanknotesIcon,
customers: UserGroupIcon,
pending: ClockIcon,
invoices: InboxIcon,
};
...
<Card title="Collected" value={totalPaidInvoices} type="collected" />
<Card title="Pending" value={totalPendingInvoices} type="pending" />
...
export function Card({
title,
value,
type,
}: {
title: string;
value: number | string;
type: 'invoices' | 'customers' | 'pending' | 'collected';
}) {
const Icon = iconMap[type];
------------
Another noteworthy point in above code is the TS spec. for type being a union of valid string values rather than just string.
-------------
The dashboard skeleton uses same skeletons (e.g. RevenueChartSkeleton) that are used as fallback for Suspense elements in dashboard page. Don't know if that is (very slightly) inefficient and whether these component skeletons get re-rendered when dashboard page is first loaded. Visually, I could not make out any rerendering of these skeletons. The streaming works effectively and the Revenue chart which has a delay of few seconds, gets rendered (final component rendering overwriting skeleton) after some other components are rendered, demonstrating the utility of Suspense elements at component within Page level as against using only loading.tsx for streaming.
----------
app\lib\data.ts code uses nullish coalescing operator ??:
const numberOfInvoices = Number(data[0].rows[0].count ?? '0');
Am postponing study of code in data.ts that comes into play after Chapter 9.
------------------
The grid size control of Revenue Chart + Latest Invoices, for various media breakpoints is a little complex (at least, to me). Relevant code:
From app\dashboard\(overview)\page.tsx :
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
<Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart />
</Suspense>
{/* <RevenueChart revenue={revenue} /> */}
<Suspense fallback={<LatestInvoicesSkeleton />}>
<LatestInvoices />
</Suspense>
{/* <LatestInvoices latestInvoices={latestInvoices} /> */}
</div>
------------
From app\ui\dashboard\revenue-chart.tsx :
<div className="w-full md:col-span-4">
<h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
Recent Revenue
...
--------
From app\ui\dashboard\latest-invoices.tsx :
<div className="flex w-full flex-col md:col-span-4">
<h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
Latest Invoices
---------
The grid is defined at dashboard page level. Till md breakpoint it has only one column, and correspondingly till md breakpoint, RevenueChart and LatestInvoices do not specify a col-span. Straight forward code, so far.
At md breakpoint, grid at dashboard page level has 4 columns per row. But at md breakpoint both RevenueChart and LatestInvoices have a col-span-4 (each column with span 4 column widths), so effectively, at md breakpoint, RevenueChart and LatestInvoices each take up the whole grid row, and so are shown one below the other, as earlier. Note that at md breakpoint, the links part of the SideNav bar move from the top to the side (flex-col). So the width for the dashboard page reduces at md breakpoint.
At lg breakpoint, grid at dashboard page level has 8 columns per row with no change in col-span-4 for RevenueChart and LatestInvoices. So from lg breakpoint, half of the width is taken up Revenue Chart and other half by LatestInvoices.
============
Promise.all is the key to parallel data fetching in the tutorial.
The tutorial says that CardWrapper component prevents popping but I am not clear whether that would be the case if CardWrapper component were not used, as the underlying data fetch for the cards is in a Promise.all.
Read chapter 10 but did not go in depth as it is experimental stuff.
https://tailwindcss.com/docs/flex - Use flex-1 to allow a flex item to grow and shrink as needed, ignoring its initial size
'flex-1 flex-shrink-0' is used in app\ui\search.tsx . flex-shrink-0 may be cancelling 'shrink as needed' ... But is there not a simple alternative? How about flex-grow?
https://tailwindcss.com/docs/screen-readers : "Use sr-only to hide an element visually without hiding it from screen readers:"
The Search input field uses a combination of pl-10 (large left padding) for the input field with a Magnifying Glass icon being positioned 'absolute left-3 top-1/2' with enclosing div having position relative. That seems to result in the icon being rendered on the left and the search input field to its right.
https://www.w3schools.com/css/tryit.asp?filename=trycss_form_icon provides a simpler solution though Next.js tutorial app may have some additional polish.
peer and peer-focus are used in app. From 'Styling based on sibling state (peer-{modifier})', https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-sibling-state : "When you need to style an element based on the state of a sibling element, mark the sibling with the peer class, and use peer-* modifiers like peer-invalid to style the target element".
But it seems that the peer and peer-focus are not having any impact in the app. Tried out some variations like in Tailwind doc above, but failed to have any impact.
Currently going through https://nextjs.org/learn/dashboard-app/adding-search-and-pagination (Chapter 11)
============================
15 Sep. 2024
https://www.postgresql.org/docs/current/functions-matching.html : "The key word ILIKE can be used instead of LIKE to make the match case-insensitive according to the active locale. This is not in the SQL standard but is a PostgreSQL extension."
:: is used for Type Casts in Postgresql, https://www.postgresql.org/docs/current/sql-expressions.html#SQL-SYNTAX-TYPE-CASTS
Related code from the tut app (file: app\lib\data.ts):
SELECT
invoices.id,
invoices.amount,
invoices.date,
invoices.status,
customers.name,
customers.email,
customers.image_url
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
WHERE
customers.name ILIKE ${`%${query}%`} OR
customers.email ILIKE ${`%${query}%`} OR
invoices.amount::text ILIKE ${`%${query}%`} OR
invoices.date::text ILIKE ${`%${query}%`} OR
invoices.status ILIKE ${`%${query}%`}
ORDER BY invoices.date DESC
LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset}
`;
-------------
Related code from scripts\seed.js
const createTable = await client.sql`
CREATE TABLE IF NOT EXISTS invoices (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
customer_id UUID NOT NULL,
amount INT NOT NULL,
status VARCHAR(255) NOT NULL,
date DATE NOT NULL
);
`;
-------
Data type of amount is INT and date is DATE. So they have to be cast to text before ILIKE operator.
==========================
export const formatCurrency = (amount: number) => {
return (amount / 100).toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
});
};
-----
Number.prototype.toLocaleString(), https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString
Intl, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl - "Unlike most global objects, Intl is not a constructor. You cannot use it with the new operator or invoke the Intl object as a function. All properties and methods of Intl are static (just like the Math object)."
Intl.NumberFormat() constructor, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
----
export const formatDateToLocal = (
dateStr: string,
locale: string = 'en-US',
) => {
const date = new Date(dateStr);
const options: Intl.DateTimeFormatOptions = {
day: 'numeric',
month: 'short',
year: 'numeric',
};
const formatter = new Intl.DateTimeFormat(locale, options);
return formatter.format(date);
};
----
Intl.DateTimeFormat() constructor, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
========================
app\ui\invoices\table.tsx has:
<div className="mt-6 flow-root">
<div className="inline-block min-w-full align-middle">
---
On the net, articles about flow-root talk about using it to avoid some problems that float creates. But I could not find any matches for 'float' in the above page. So perhaps the page code uses it for something else.
https://developer.mozilla.org/en-US/docs/Web/CSS/display#flow-root states, "flow-root" .. "The element generates a block box that establishes a new block formatting context, defining where the formatting root lies."
Block formatting context, https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_display/Block_formatting_context states, "A block formatting context (BFC) is a part of a visual CSS rendering of a web page. It's the region in which the layout of block boxes occurs and in which floats interact with other elements." It goes on to give some cases that create a BFC. Floats are only one such case. Other cases include absolutely positioned elements, inline-blocks, table cells etc. The above tutorial page code uses inline-block. It also has <td> elements which may imply usage of 'table cell' (), as above MDN page states, "Table cells (elements with display: table-cell, which is the default for HTML table cells)."
Does not address above issues but explains block and inline layouts very well: Block and inline layout in normal flow, https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flow_layout/Block_and_inline_layout_in_normal_flow
inline-block respects width and height whereas inline does not. Perhaps that's why above table.tsx code uses inline-block (and min-w-full)
I don't think I have used 'display: table' and related CSS, so far. So the Tailwind CSS associated with table in app\ui\invoices\table.tsx is quite new to me. The question is how much time I must invest in studying it now. Perhaps a good approach would be to do a quick overview study and postpone a deeper study to when I have the need for better understanding of this code, say for some software development work.
I could not get in-depth articles on it. Here are two articles that cover the display: table related CSS but they do not seem to be so relevant to this tutorial app code (table.tsx):
Kicking Ass with display:table, https://mattboldt.com/kicking-ass-with-display-table/
CSS Display Table, https://www.yahoobaba.net/css/css-display-table
Some complex Tailwind CSS in table.tsx:
<tbody className="bg-white">
{invoices?.map((invoice) => (
<tr
key={invoice.id}
className="w-full border-b py-3 text-sm last-of-type:border-none [&:first-child>td:first-child]:rounded-tl-lg [&:first-child>td:last-child]:rounded-tr-lg [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg"
>
<td className="whitespace-nowrap py-3 pl-6 pr-3">
-----
Nested Brackets and Ampersand usage in Tailwind UI examples, https://stackoverflow.com/questions/73666015/nested-brackets-and-ampersand-usage-in-tailwind-ui-examples led me to https://tailwindcss.com/docs/hover-focus-and-other-states which uses & . From it: "You can create one-off group-* modifiers on the fly by providing your own selector as an arbitrary value between square brackets:" and "For more control, you can use the & character to mark where .group should end up in the final selector relative to the selector you are passing in:"
So I think I first need to read up about groups in Tailwind (in same page).
From: Styling based on parent state (group-{modifier}), https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
"When you need to style an element based on the state of some parent element, mark the parent with the group class, and use group-* modifiers like group-hover to style the target element:" .. I think I had gone through this section in some other context. The examples are fairly easy to understand.
Seems like the key section for table.tsx page code above, is: Using arbitrary variants, https://tailwindcss.com/docs/hover-focus-and-other-states#using-arbitrary-variants .
Took me quite a bit of searching to get to & MDN reference page: & nesting selector, https://developer.mozilla.org/en-US/docs/Web/CSS/Nesting_selector . The section "Examples" (internal link broken) in it gives a simple example of using &:
CSS without & (nesting selector):
.example {
font-family: system-ui;
font-size: 1.2rem;
}
.example > a {
color: tomato;
}
.example > a:hover,
.example > a:focus {
color: ivory;
background-color: tomato;
}
-----
Equivalent CSS with & (nesting selector):
.example {
font-family: system-ui;
font-size: 1.2rem;
& > a {
color: tomato;
&:hover,
&:focus {
color: ivory;
background-color: tomato;
}
}
}
----
Getting back to Tailwind code now, I decided to read up on square bracket selector in CSS as I don't think I have used it or studied about it in my non Tailwind CSS code. But later I realized that square bracket in Tailwind seems to not correspond to square bracket in CSS. But anyway, some little info. about square bracket in CSS is given below:
Attribute selectors, https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors/Attribute_selectors seems to be the key page.
An example usage is:
"[attr] a[title] Matches elements with an attr attribute (whose name is the value in square brackets)."
My understanding of above is that a[title] will match a (anchor) elements with title attribute.
[Seems like any HTML element including anchor tag can have title attribute, https://www.w3schools.com/tags/att_global_title.asp .]
There are other variations of attribute selectors like "[attr=value]".
Back to Tailwind. From 'Using arbitrary variants', https://tailwindcss.com/docs/hover-focus-and-other-states#using-arbitrary-variants :
Example HTML:
<ul role="list">
{#each items as item}
<li class="[&:nth-child(3)]:underline">{item}</li>
{/each}
</ul>
----
Example Generated CSS:
/* https://media.giphy.com/media/uPnKU86sFa2fm/giphy.gif */
.\[\&\:nth-child\(3\)\]\:underline:nth-child(3) {
text-decoration-style: underline
}
----
The '.\[\&\:nth-child\(3\)\]\:underline' part of generated CSS seems to be just the Tailwind class name with some characters escaped. The main CSS code for this class is:
nth-child(3) {
text-decoration-style: underline
}
But can't we simply use "nth-child(3):underline" as the Tailwind class? It seems that that will not work in Tailwind. There are TW utility classes for first and last child, odd and even child but not for nth-child(x) - it seems.
An interesting article which seems to be in line with my above statements: Tailwind CSS Nth-Child Selector Cheat Sheet, https://cruip.com/tailwind-css-nth-child-selector-cheat-sheet/#nth-selector-11
Another example from above using arbitrary variants TW documentation page:
"If you need spaces in your selector, you can use an underscore."
<div class="[&_p]:mt-4">
-----
All p elements within the div will be selected and have margin-top as 1 rem.
==============
Now the above table.tsx code can be understood. In :
[&:first-child>td:first-child]:rounded-tl-lg [&:first-child>td:last-child]:rounded-tr-lg [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg
---
In the first class spec. above, tr element's first child of first row is given rounded-tl-lg TW class.
For it, VSCode Intellisense shows:
.\[\&\:first-child\>td\:first-child\]\:rounded-tl-lg:first-child>td:first-child {
border-top-left-radius: 0.5rem /* 8px */;
}
----
Replacing the complex TW class name with simply classX, that essentially is CSS of :
.classX:first-child>td:first-child {
border-top-left-radius: 0.5rem /* 8px */;
}
This is assigned to tr element. So tr element's first child (first row) and its first td child element (first row, first column cell) is selected and given - border-top-left-radius: 0.5rem .
The other class specs. above can also be understood on similar lines.
============
16 Sept. 2024
In VSCode, for plain HTML files using Tailwind via CDN, Tailwind Intellisense was not showing even though the related extension had been installed.
The solution is to create an empty file named tailwind.config.js in the directory having the HTML file. Strange fix but I tried it and it works. Ref: Tailwind Auto complete is not working in plan HTML CSS JS project VS Code, https://www.youtube.com/watch?v=HGtz7P9xtmg , 4 mins.
---
I created some test table html files which try to replicate the table Tailwind CSS in next.js tutorial app, to try out changes to table Tailwind CSS for better understanding. tablev1.html recreates 3 rows of next.js tutorial app invoices. I made minor changes in background and border colour to make them more visible. In next.js app I could switch on dark mode using the Chrome extension I have, to check table border related stuff but tablev1.html does not change on dark mode (perhaps next.js app has some dark mode settings at higher layout.tsx file(s) level).
----------
Removing 'last-of-type:border-none' from class of last tr entry results in border being shown below last row, as expected. Note that border width is specified only for border bottom (and so other borders will not appear anyway).
Is [&:first-child>td:first-child] needed? Can it not simply be: first-child>td:first-child ?
Intellisense does not show anything for 'first-child>td:first-child:rounded-tl-lg' when applied to first tr, or when applied to tbody. So looks like that is not valid Tailwind code.
Tailwind Intellisense does not show anything while typing out the contents of following within square brackets:
[&:first-child>td:first-child]:rounded-tl-lg
It shows up only for the rounded-tl-lg part, after which it applies that to the whole specification including the square brackets (if I recall correctly).
https://tailwindcss.com/docs/hover-focus-and-other-states states that Tailwind has "Pseudo-classes, like :hover, :focus, :first-child, and :required". So first-child should be recognized by Intellisense when it is typed in by itself. But it is not!
Figured out that first is the keyword for first-child. So 'first:border-b-2' is recognized by Intellisense and shows the CSS for it as:
.first\:border-b-2:first-child {
border-bottom-width: 2px;
}
--------------
It also results in the first row having thicker border, even if it is specified for all three rows, as expected.
'first:>td:first:rounded-tl-lg' in tbody is not recognized though while typing, first: part of it got recognized (by Intellisense)
Moving 'first:border-b-2' from 1st tr to tbody does not work even though it is recognized as earlier by Tailwind Intellisense. The reason for that is given below.
https://developer.mozilla.org/en-US/docs/Web/CSS/:first-child states, "The :first-child CSS pseudo-class represents the first element among a group of sibling elements." So it can be used only at the tr (3 siblings) level or td (more siblings) level, and not at tbody level.
first:>td:first-child:rounded-tl-lg in 1st tr row does not work.
first:>td:first:rounded-tl-lg in 1st tr row also does not work.
So looks like using the > selector (child selector) in above cases needs the square bracket and & usage.
Tried '[first-child>td:last-child]:rounded-tr-lg' (removing &:) in 1st tr row. Intellisense did not recognize it and the top right edge of the table body became square instead of rounded.
https://tailwindcss.com/docs/hover-focus-and-other-states#using-arbitrary-variants states, "Just like arbitrary values let you use custom values with your utility classes, arbitrary variants let you write custom selector modifiers directly in your HTML." .. "Arbitrary variants are just format strings that represent the selector, wrapped in square brackets."
CSS related jargon is still not very clear to me. But given above experimentation, I think the above means that whatever comes after '[&:' till ']' is escaping into regular CSS.
'[&:first-child>td:first-child]:rounded-tl-lg' is expanded by Intellisense to:
.\[\&\:first-child\>td\:first-child\]\:rounded-tl-lg:first-child>td:first-child {
border-top-left-radius: 0.5rem /* 8px */;
}
---
Note that the CSS does not have & . I think the & in Tailwind CSS does NOT map to & (nesting selector) in CSS.
----
[https://tailwindcss.com/docs/adding-custom-styles#using-arbitrary-values gives an 'arbitrary value' example as '<div class="top-[117px]">', I have used such arbitrary values in app code. What put me off in this 'arbitrary variants' case is the &:]
Replacing the complex class name by simply classX, we have:
.classX:first-child>td:first-child {
border-top-left-radius: 0.5rem /* 8px */;
}
----
I made following changes to the file:
<style>
.classX:first-child > td:first-child {
border-top-left-radius: 0.5rem;
}
</style>
---
Then I modified the first row class specification as:
<tr
class="w-full border-b border-black py-3 text-sm last-of-type:border-none classX [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg"
>
----
The top left of the table body got rounded. As expected, top right was not rounded and bottom left and bottom right were both rounded.
tablev2.html has the code.
---------
I wondered whether I could use the .classX style specification as an inline style. I could not get first-child to be recognized as an inline style but could get simple stuff like 'color:blue' to get recognized in Intellisense and show up in the browser. tablev3.html has the code.
As per: How to add nth-child() style in inline styling?, https://stackoverflow.com/questions/25646683/how-to-add-nth-child-style-in-inline-styling and CSS Pseudo-classes with inline styles, https://stackoverflow.com/questions/5293280/css-pseudo-classes-with-inline-styles , "an inline style attribute can only contain property declarations" and you cannot use selectors, pseudo-classes/pseudo-elements etc.
================
18 Sept. 2024
HTML <th> scope Attribute, https://www.w3schools.com/tags/att_th_scope.asp
Header for col or row or ...
No effect in ordinary web browsers but can be read by screen readers.
---
Invoices table seems to have a small issue when the width and height of the window are reduced but not to the point where the sidenav moves from left to top. Long name (Delba de Oliveira) end gets on top of email beginning part.
Width of table cells/columns is not specified. The width of the columns for max window width seems to be a little arbitrary.
What is the default width of an HTML table cell <td>?, https://stackoverflow.com/questions/31926939/what-is-the-default-width-of-an-html-table-cell-td - one response states that it is up to the browser to decide.
=========================
What is this underscore: [...Array(5)].map((_,x) => x++);, https://www.reddit.com/r/javascript/comments/8842um/what_is_this_underscore_array5map_x_x/ : Comments explain that _ is used to denote an unused variable. Instead of _, 'unused' could be used as variable name.
Array.from(), https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from
Syntax for one usage: Array.from(arrayLike, mapFn)
First argument can be: "array-like objects (objects with a length property and indexed elements)."
JavaScript - Difference between Array and Array-like object, https://stackoverflow.com/questions/29707568/javascript-difference-between-array-and-array-like-object
Examples of Array-like objects:
The arguments object, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments : "arguments is an array-like object accessible inside functions that contains the values of the arguments passed to that function."
HTMLCollection, https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection
Why do you need to know about Array-like Objects?, https://daily.dev/blog/why-do-you-need-to-know-about-array-like-objects
Calling map() on non-array objects, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map#calling_map_on_non-array_objects : "The map() method reads the length property of this and then accesses each property whose key is a nonnegative integer less than length."
Tutorial app uses _ and Array.from on array-like object, in this code in app\lib\utils.ts:
return Array.from({ length: totalPages }, (_, i) => i + 1);
----
I think Array.from may be passing undefined as _ variable value for each invocation.
For totalPages = 5, return value would be [1,2,3,4,5].
-----
Pagination component in app\ui\invoices\pagination.tsx uses following code:
<div className="inline-flex">
<PaginationArrow
direction="left"
href={createPageURL(currentPage - 1)}
isDisabled={currentPage <= 1}
/>
...
----
Why use inline-flex here? It is contained in a div which is a normal flex and there are no elements before or after Pagination component. The code using Pagination in app\dashboard\invoices\page.tsx:
<div className="mt-5 flex w-full justify-center">
<Pagination totalPages={totalPages} />
</div>
----
I modified the code to use flex instead of inline-flex as follows, and it seems to work properly.
<div className="flex">
<PaginationArrow
direction="left"
href={createPageURL(currentPage - 1)}
isDisabled={currentPage <= 1}
/>
...
----
Perhaps the reason may be that code using Pagination may have inline elements before or after Pagination even if in this case they don't.
Example of inline-flex: Section 'Inline-flex' in What is the difference between inline-flex and inline-block in CSS?, https://www.geeksforgeeks.org/what-is-the-difference-between-inline-flex-and-inline-block-in-css/
---
Typescript seems to have a nice way to handle enum kind of variables/arguments. From app\ui\invoices\pagination.tsx :
function PaginationArrow({
href,
direction,
isDisabled,
}: {
href: string;
direction: 'left' | 'right';
isDisabled?: boolean;
}) {
----
So direction here is like an enum which can have 'left' or 'right' values only.
TS also has support for regular enums: https://www.typescriptlang.org/docs/handbook/enums.html
I think now I have completed detailed study of code related to Chapter 11.
========================
How to make a placeholder for a ‘select’ box?, https://www.geeksforgeeks.org/how-to-make-a-placeholder-for-a-select-box/ - Select tag does not seem to have a placeholder attribute.
app\ui\invoices\create-form.tsx uses following code for placeholder in select:
<option value="" disabled>
Select a customer
</option>
----
From https://developer.mozilla.org/en-US/docs/Web/CSS/::placeholder : "The ::placeholder CSS pseudo-element represents the placeholder text in an <input> or <textarea> element."
The tut app code uses ::placeholder for select element as well! I think that code gets ignored as when I changed it to "placeholder:text-red-900" there was no impact on the page display. The placeholder entry is marked as disabled which seems to result in lighter colour for it in the drop-down but the placeholder text (different from placeholder entry in drop down list) seems to be same colour as not-disabled entries in drop-down.
The ::placeholder CSS pseudo-element is used for Amount input field where it seems to be having an as-expected effect.
Invoice Status radio buttons of Pending and Paid, are in a fieldset with a legend. fieldset is not used elsewhere in the form.
After filling up create invoice form, instead of pressing 'Create Invoice' button, navigating away (without saving) to SideNav bar links like Customers, does not show any warning about unsaved data, and the user is shown the page he navigated to. Similarly no warning shown when navigating to links outside the app without saving.
In:
<input
id="paid"
name="status"
type="radio"
value="paid"
className="h-4 w-4 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2"
/>
---
focus:ring-2 seems to be the default as removing it does not make any impact.
Also the colour of the ring is not specified (blue is shown which may be the default).
Changing code in the earlier radio button CSS to:
focus:ring-4 focus:ring-red-900
---
had the expected impact of thicker red coloured ring on focus.
=======
https://nextjs.org/docs/app/api-reference/file-conventions/page explains params and searchparams props of Next.js Page.
Params is for getting dynamic route parameter.
Searchparams is for getting query string data.
=======================
Round 2 has some unanswered questions just above the sentence, "That finishes the notes for the tutorial page, "Adding Search and Pagination"." I think I should revisit those questions later on.
=============
While in an earlier round, I had noted how function.bind ('const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);') was used to pass invoice.id as an argument to the updateInvoice form action, along with formData which is provided by default, I think this technique slipped from my mind when I was developing my Gita web app. I don't clearly recall the scenario now but I think I wanted to pass some additional argument to a form action besides the formData but could not figure out how to do that.
Hopefully me putting up this note will drum this technique deeper into my head and I will remember it next time I need to do something similar.
==============
21 Sept. 2024
app\dashboard\invoices\error.tsx has:
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
---
In the above, 'error: Error & { digest?: string };' is a little obscure. VSCode Intellisense informs us that this Error (different from function Error) is an interface. What does type Error correspond to? VSCode Intellisense Go to Definition provides two definitions which are not so easy to understand.
https://nextjs.org/learn/dashboard-app/error-handling states about the error prop, "error: This object is an instance of JavaScript's native Error object."
Error, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error is easier to understand. VSCode lists cause, digest, message, name and stack as the properties of the interface/type of error prop. passed to Error component. The above MDN page for JavaScript Error lists cause, message, name and stack as properties of the Error object. Next.js code above adds digest as a property of the interface/type of the error prop.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error gives the Error object constructor methods.
===========================
In app\ui\invoices\create-form.tsx the form code has 'outline-2' for a few elements like <input> for amount. But I could not find outline (style) specification due to which I think the outline-2 is ignored. I confirmed it by changing outline-2 to outline-8 and there was no change in the form display. But then I added outline (default outline solid style) and now the outline was reflecting the outline-8 class. The colour was black instead of blue.
It seems that the blue outline that appears on the data-input fields of this form on tabbing through them, is the Tailwind and/or browser default. [So, it seems that, the developer need not code for this.]
=========================
Awesome tutorial that covers lot of ground and is at a good pace: React Hook Form - Complete Tutorial (with Zod), https://www.youtube.com/watch?v=cc_xmawJ8Kg, 28 mins, Jan. 2024 by Cosden Solutions. ... Code: https://github.com/cosdensolutions/code/tree/master/videos/long/react-hook-form-tutorial .. main file: https://github.com/cosdensolutions/code/blob/master/videos/long/react-hook-form-tutorial/src/App.tsx
React Hook Form - Get Started, https://react-hook-form.com/get-started
React Hook Form - Get Started, https://www.youtube.com/watch?v=RkXv4AXXC_4, 8 mins, Dec. 2021
Form validation without libraries like react-hook-form:
Form Validation using React | React Forms Handling & Validation Tutorial | React Sign up Form, https://www.youtube.com/watch?v=EYpdEYK25Dc , Dipesh Malvia, 21 mins, Oct. 2021 ... Code repo: https://github.com/dmalvia/React_Forms_Tutorials/tree/use-native .. main file: https://github.com/dmalvia/React_Forms_Tutorials/blob/use-native/src/App.js .. Note that 'Semantic UI' (CSS class library) is used - https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css (in public/index.html).
MDN HTML & JS stuff: Client-side form validation, https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation
========================
Easy paced video about React hooks but there may be some inaccuracies: Learn React JS Hooks | React Hooks Tutorial | React Hooks Explained | React Hooks for Beginners, https://www.youtube.com/watch?v=hJ5UEtdS8qE
https://react.dev/reference/react/hooks is good and accurate but is a more intense read.
Differences between refs and state, https://react.dev/learn/referencing-values-with-refs#differences-between-refs-and-state : Just above the section, "When a piece of information is used for rendering, keep it in state. When a piece of information is only needed by event handlers and changing it doesn’t require a re-render, using a ref may be more efficient." At the beginning of the section, "But in most cases, you’ll want to use state. Refs are an “escape hatch” you won’t need often."
Refs can be used for: "Storing timeout IDs", "Storing and manipulating DOM elements ...", "Storing other objects that aren’t necessary to calculate the JSX".
================
Chapter 14, "Improving Accessability", section "Client-Side validation", https://nextjs.org/learn/dashboard-app/improving-accessibility#client-side-validation states, "There are a couple of ways you can validate forms on the client." But it covers only one way which is using form validation supported by browser (e.g., required). It does not cover using libraries like React hook form and/or Zod.
[Seems to be an interesting article on first quick-browse:] Zod and React: A Perfect Match for Robust Validation, https://www.dhiwise.com/post/zod-and-react-a-perfect-match-for-robust-validation, LU Aug 2024.
================
https://zod.dev/?id=basic-usage explains difference between parse and safeParse methods with a simple example copy-pasted below:
import { z } from "zod";
// creating a schema for strings
const mySchema = z.string();
// parsing
mySchema.parse("tuna"); // => "tuna"
mySchema.parse(12); // => throws ZodError
// "safe" parsing (doesn't throw error if validation fails)
mySchema.safeParse("tuna"); // => { success: true; data: "tuna" }
mySchema.safeParse(12); // => { success: false; error: ZodError }
----------
The documentation for various primitives and also other documentation seems good. For example, https://zod.dev/?id=strings :
'z.string().email({ message: "Invalid email address" });' shows how useful zod can be.
====================
https://zod.dev/?id=basic-usage shows how to customize some common error messages for string schema:
const name = z.string({
required_error: "Name is required",
invalid_type_error: "Name must be a string",
});
Next.js tutorial code: In app\lib\actions.ts :
const FormSchema = z.object({
id: z.string(),
customerId: z.string({
invalid_type_error: 'Please select a customer.',
}),
amount: z.coerce
.number()
.gt(0, { message: 'Please enter an amount greater than $0.' }),
status: z.enum(['pending', 'paid'], {
invalid_type_error: 'Please select an invoice status.',
}),
date: z.string(),
});
----
From the same file:
const CreateInvoice = FormSchema.omit({ id: true, date: true });
const UpdateInvoice = FormSchema.omit({ id: true, date: true });
----
Don't know what role 'true' plays. The documentation has similar examples but does not explain the 'true' part.
Why can't a single object schema be used for both CreateInvoice and UpdateInvoice? Note that they are constants.
================
When I was going through initial rounds of next.js tutorial starting in late April/early May 2024, https://raviswdev.blogspot.com/2024/04/notes-on-learning-nextjs.html , the project code I developed used useFormState (e.g. in app\ui\invoices\create-form.tsx), But now in Sept. 2024, the next.js tutorial page has code with useActionState in place of useFormState!
Explanation of it is found in: React 19 RC, https://react.dev/blog/2024/04/25/react-19 dated April 25, 2024 :
"React.useActionState was previously called ReactDOM.useFormState in the Canary releases, but we’ve renamed it and deprecated useFormState."
RC in 'React 19 RC', seems to stand for Release Candidate. So it does not seem to be an official release yet; react documentation reference for useActionState, https://react.dev/reference/react/useActionState , (seen on 21 Sep. 2024) mentions React version as react@18.3.1. The page states, "The useActionState Hook is currently only available in React’s Canary and experimental channels."
[React 18 vs React 19 (RC): Key Differences and Migration Tips with Examples, https://dev.to/manojspace/react-18-vs-react-19-key-differences-and-migration-tips-18op .]
=======================
Error Handling in Zod, https://zod.dev/ERROR_HANDLING is a detailed page. Relevant content to understand next.js tutorial code plus some related stuff are given below:
ZodError is the validation error(s) object:
class ZodError extends Error {
issues: ZodIssue[];
}
----
ZodIssue is a discriminated union and not a class.
Discriminated unions, https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions gives a nice example of it. The key code is as follows:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
...
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
}
}
----
TypeScript is able to view above code as correct as it knows that radius is used only when shape.kind is circle and sideLength is used only when shape.kind is square.
"When every type in a union contains a common property with literal types, TypeScript considers that to be a discriminated union, and can narrow out the members of the union."
Back to ZodIssue ...
ZodIssue has 3 common properties:
code - enum describing the error issue for the schema field
path - (string | number)[] - e.g. ['addresses', 0, 'line1'] - The array identifies the schema field having this issue.
message - string - error message, e.g. Invalid type. Expected string, received number.
If the schema has nested fields then path, I think, will need to have enough array elements to identify the field having the issue.
A demonstrative example, https://zod.dev/ERROR_HANDLING?id=a-demonstrative-example is a good example of a quite simple but nested object schema being 'parsed' with incorrect ('improperly formatted') data, and so throwing an error of type z.ZodError with a list of issues.
Error handling for forms, https://zod.dev/ERROR_HANDLING?id=error-handling-for-forms first has an object schema with one level of nesting, which is conveniently handled in JSX code by using ZodError.format().
Flattening errors, https://zod.dev/ERROR_HANDLING?id=flattening-errors gives an example of an object schema which is only one level deep (no nesting) for which errors can be conveniently handled in JSX code by using ZodError.flatten(). The next.js tutorial uses ZodError.flatten() for its create invoice form. Related code:
In app\lib\actions.ts:
export async function createInvoice(prevState: State, formData: FormData) {
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
// If form validation fails, return errors early. Otherwise, continue.
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing Fields. Failed to Create Invoice.',
};
}
...[Code that inserts row in database snipped]
} catch (error) {
return {
message: 'Database Error: Failed to Create Invoice.',
};
}
----
In app\ui\invoices\create-form.tsx, the form level code is:
export default function Form({ customers }: { customers: CustomerField[] }) {
const initialState = { message: null, errors: {} };
const [state, dispatch] = useFormState(createInvoice, initialState);
return (
<form action={dispatch}>
...
----
In above code, for createInvoice, VSCode Intellisense provides:
(alias) function createInvoice(prevState: State, formData: FormData): Promise<{
errors: {
customerId?: string[] | undefined;
amount?: string[] | undefined;
status?: string[] | undefined;
};
message: string;
} | {
message: string;
errors?: undefined;
}>
import createInvoice
----
To show individual field level error in the form, the code for customer is:
<div id="customer-error" aria-live="polite" aria-atomic="true">
{state.errors?.customerId &&
state.errors.customerId.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
----
To show form level error message, the code is (note that createInvoice provides the form level error message for individual field error(s) case, and also for case of no individual field error but a database error on attempting to create the invoice in the database:
<div id="error-message" aria-live="polite" aria-atomic="true">
{state.message ? (
<p className="mt-2 text-sm text-red-500">{state.message}</p>
) : null}
</div>
----
============
In my tutorial project, I have not made changes to update invoice code to incorporate zod error messages like create invoice code. These changes seem to be quite straight forward stuff very similar to create invoice code. Perhaps that's why I skipped it. In this round, I don't want to get into updating the project code. I am simply studying the existing code to understand it better.
I have finished with chapters 12, 13 and 14. Next chapter is 15: Adding Authentication.
======================
23 Sep. 2024
What’s the double exclamation sign for in JavaScript?, https://medium.com/@chirag.viradiya_30404/whats-the-double-exclamation-sign-for-in-javascript-d93ed5ad8491
next.js tutorial code in auth.config.ts:
const isLoggedIn = !!auth?.user;
----
My understanding is that if user is undefined or null (or some other falsy value) !auth?user will evaluate to true and !!auth?user will evaluate to false.
But if user is defined and not-null (and not any other falsy value), !auth?user will evaluate to false and !!auth?user will evaluate to true.
oAuth for Beginners - How oauth authentication🔒 works ?, https://www.youtube.com/watch?v=VZH_lGxqFYU , 10 min. 42 secs., Feb. 2024, IT k Funde
OAuth 2 Explained In Simple Terms, https://www.youtube.com/watch?v=ZV5yTm4pT8g, 4 min. 31 secs, Jun. 2023, ByteByteGo.
Next-Auth Login Authentication Tutorial with Next.js App Directory, https://youtu.be/w2h54xz6Ndw, 40 min. 26 secs, Jul. 2023, Dave Gray ... GitHub: https://github.com/gitdagray/next-auth-intro
Seems to be a detailed guide on Next.js authentication: Authentication, https://nextjs.org/docs/app/building-your-application/authentication
Next.js authentication in tutorial uses a route handler. Route Handlers, https://nextjs.org/docs/app/building-your-application/routing/route-handlers
In the code examples in above page which is in section: Caching, https://nextjs.org/docs/app/building-your-application/routing/route-handlers#caching Response.json() seems to refer to json() method of NextResponse, https://nextjs.org/docs/app/api-reference/functions/next-response#json . It is different from json() method of Response web API (which is used in code just after fetch): Response: json() static method, https://developer.mozilla.org/en-US/docs/Web/API/Response/json_static .
Web API Request: https://developer.mozilla.org/en-US/docs/Web/API/Request
Web API Response: https://developer.mozilla.org/en-US/docs/Web/API/Response
import type { NextAuthConfig } from 'next-auth';
Above statement seems to import only the type information for NextAuthConfig. From https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html "import type only imports declarations to be used for type annotations and declarations. It always gets fully erased, so there’s no remnant of it at runtime."
==============
https://authjs.dev/reference/nextjs has entries for:
NextAuthConfig - https://authjs.dev/reference/nextjs#nextauthconfig
https://authjs.dev/reference/nextjs#pages (covers pages property which allows for specification of custom sign in and sign out (and error) pages). Tutorial code specifies custom sign in page (/login).
NextAuthResult - https://authjs.dev/reference/nextjs#nextauthresult
default() - https://authjs.dev/reference/nextjs#default-13 [This is the one coming into play in statements like below from middleware.ts in tutorial] :
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export default NextAuth(authConfig).auth;
------
It takes in one parameter of config or function that returns config, and returns NextAuthResult.
auth is a function of NextAuthResult object. It has various forms covered in a later section below, but the form that the tutorial uses seems to be:
(...args: [NextApiRequest, NextApiResponse]) => Promise<Session | null>)
The function takes in NextApiRequest and NextApiResponse parameters and returns a Session object or null.
Further as it is the default function of middleware.ts, my understanding is that 'import middleware from "@/middleware.ts"' in some other file will result in this auth function being referred to (named as) middleware function.
However, most examples of middleware functions I have seen do not have Response or NextApiResponse as an argument. In such a case, I think TypeScript should complain as NextApiResponse is not specified as optional argument in above definition. This is something I am not clear about.
Note that even if the middleware function does not have Response/NextApiResponse as an argument, it seems to be able to call methods (probably static) on NextResponse (interface) and so create response if needed (and also use static methods of Response Web API interface).
https://authjs.dev/reference/nextjs#auth has an example of auth being used as middleware:
export { auth as middleware } from "./auth"
----
Next.js middleware documentation, https://nextjs.org/docs/app/building-your-application/routing/middleware , has 'export function middleware(request: NextRequest)' or similar in almost all, if not all examples of middleware.ts.
------
VSCode intellisense for NextAuth in the line 'import NextAuth from 'next-auth';' in middleware.ts in tutorial:
(alias) function NextAuth(config: NextAuthConfig | ((request: NextRequest | undefined) => NextAuthConfig)): NextAuthResult
import NextAuth
Initialize NextAuth.js.
@example
import NextAuth from "next-auth"
import GitHub from "@auth/core/providers/github"
export const { handlers, auth } = NextAuth({ providers: [GitHub] })
Lazy initialization:
@example
import NextAuth from "next-auth"
import GitHub from "@auth/core/providers/github"
export const { handlers, auth } = NextAuth((req) => {
console.log(req) // do something with the request
return {
providers: [GitHub],
},
})
-----------
Above VSCode intellisense matches with default() documentation
=================
https://authjs.dev/reference/nextjs#authorized covers callbacks: authorized type (used in auth.config.ts in tutorial).
The example in this page uses 'return !!auth.user' . This auth is part of params for this callback and is specified as 'null | Session' with description being, 'The authenticated user or token, if any.' Using the 'Session' link goes to: https://authjs.dev/reference/nextjs#session-2 which I think is the wrong session entry. The right one seems to be: https://authjs.dev/reference/nextjs#session-3 . This Session has an optional user property described as "The shape of the returned object in the OAuth providers’ profile callback, available in the jwt and session callbacks, or the second parameter of the session callback, when using a database."
Related tutorial code from auth.config.ts
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
if (isOnDashboard) {
if (isLoggedIn) return true;
return false; // Redirect unauthenticated users to login page
} else if (isLoggedIn) {
return Response.redirect(new URL('/dashboard', nextUrl));
}
return true;
},
--------
In above code:
request is a NextRequest object.
Response.redirect is Web API redirect static method of Response interface, https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static.
===================
From: Rest parameters, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters :
function sum(...theArgs) {
let total = 0;
for (const arg of theArgs) {
total += arg;
}
return total;
}
----
"The rest parameter syntax allows a function to accept an indefinite number of arguments as an array".
https://en.wikipedia.org/wiki/Variadic_function : "In mathematics and in computer programming, a variadic function is a function of indefinite arity, i.e., one which accepts a variable number of arguments."
========
auth seems to be an important property of NextAuthResult - https://authjs.dev/reference/nextjs#auth
The description states, "A universal method to interact with NextAuth.js in your Next.js app. After initializing NextAuth.js in auth.ts , use this method in Middleware, Server Components, Route Handlers ( app/ ), and Edge or Node.js API Routes ( pages/ )."
That's a very generic statement. The syntax part of it is:
auth: (...args) => Promise<null | Session> & (...args) => Promise<null | Session> & (...args) => Promise<null | Session> & (...args) => AppRouteHandlerFn;
----
Not very helpful!
Finally, I went to the code, using 'Go to Definition' VSCode command for 'auth' in middleware.ts of tutorial. That took me to: node_modules\next-auth\src\index.tsx with following code for auth property in 'export interface NextAuthResult {':
auth: ((
...args: [NextApiRequest, NextApiResponse]
) => Promise<Session | null>) &
((...args: []) => Promise<Session | null>) &
((...args: [GetServerSidePropsContext]) => Promise<Session | null>) &
((
...args: [
(
req: NextAuthRequest,
ctx: AppRouteHandlerFnContext
) => ReturnType<AppRouteHandlerFn>,
]
) => AppRouteHandlerFn)
----
That makes it much clearer! And there is a long comment prior to this property type definition, which seems to be the same or similar to the documentation page: https://authjs.dev/reference/nextjs#auth
==========
https://authjs.dev/getting-started with Next.js selection, is making more sense now.
Interesting that Auth.js CLI provides a way to create AUTH_SECRET - npx auth secret.
https://authjs.dev/getting-started/authentication/credentials - tutorial app uses credentials which is simple but not recommended by NextAuth. OAuth and some other authentication mechanisms are recommended. The Dave Gray tutorial uses OAuth (with GitHub).
===================
Middleware matching paths in next.js: https://nextjs.org/docs/app/building-your-application/routing/middleware#matching-paths :
The term 'Matcher' is used for a config object with a matcher property in it and which object is in middleware.ts/.js and exported from it. Example of it is given below:
export const config = {
matcher: '/about/:path*',
}
---
Tutorial code uses:
export const config = {
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
};
--------
The documentation page above gives this example:
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
*/
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
],
}
----
Positive and Negative Lookahead, https://www.regular-expressions.info/lookaround.html gives a short and easy explanation.
Using that my understanding of this part of above regex: '(?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt)' is:
NOT api nor _next/static nor _next/image nor favicon.ico nor sitemap.xml nor robots.txt.
The opening and closing parentheses above seem to be "Capture and Use Quantifier" as explained in https://blog.devgenius.io/regex-parentheses-examples-of-every-type-aba8441be761.
So this regex part: '(?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*' will match any string (path) which is not one of the above.
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)' adds a / at the beginning. I think the outer parentheses are needed but don't know exactly what they do here.
Now coming to tutorial regex:
'/((?!api|_next/static|_next/image|.*\\.png$).*)'
I am tripping up on the '.*\\.png$' part. \\ seems to specify single (literal) \. But that does not seem to make sense here. The . in .png needs to be escaped. So '.*\.png$' would mean skip any .png file at root directory level e.g. /abc.png or /z.png. Does \\ being in a JavaScript string convert it to a single \ - I wonder. ... Yes, that seems to be it: https://www.freecodecamp.org/news/how-to-escape-strings-in-javascript/ . In other words, the actual regex without quotes of a string is:
/((?!api|_next/static|_next/image|.*\.png$).*)
==========
How to test above pattern using online regex test sites?
Am trying out https://regex101.com/ with ECMAScript option.
All / characters have to be escaped for it to be accepted as valid regex. So it becomes:
\/((?!api|_next\/static|_next\/image|.*\.png$).*)
The above site does not seem to handle /_next/static properly.
But https://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_regexp_test2 also seems to give similar results as regex101.com.
Tried https://regexr.com/ which gave same results as regex101.com. Experimented. Sticky flag seems to do the trick. Don't understand it much - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky has info on it - but I don't want to spend much time on it now.
Test info:
Expression (with y flag and no other flags):
\/((?!api|_next\/static|_next\/image|.*\.png$).*)
No Match cases:
/api
/api/xaa
/apix
/_next/static
/_next/statica
/_next/static/a/b/c/c
/_next/image
/_next/image/a
/login.png
/a/b/c.png
--------
Match cases:
/
/ap
/ap/x
/dashboard
/login
/a/b/c.jpg
=================
Seems to work as expected!
===============================
Tutorial page for authentication has following code for auth.ts:
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export const { auth, signIn, signOut } = NextAuth({
...authConfig,
});
----
Why does authConfig have to be spread in this way? Could we not have the following?
export const { auth, signIn, signOut } = NextAuth(authConfig); [Update: This is just intermediate code. Later additional properties are added to object passed to NextAuth(). So this question is answered.]
auth is exported in two files - auth.ts and middleware.ts. Related middleware.ts code:
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export default NextAuth(authConfig).auth;
...
----
Note that middleware.ts exports auth as default.
It seems that middleware.ts auth is not directly used by tutorial code. It perhaps is used by Next.js framework code which perhaps accesses it using default and so something like 'import middleware from 'middleware.ts'.
It further seems that even auth.ts auth is not directly used by tutorial code. But signIn and signOut exported constants from auth.ts are used by tutorial code.
=============
https://authjs.dev/getting-started/installation?framework=Next.js uses only auth.ts and not auth.config.ts. But it uses route.ts. It also does NOT use pages nor callbacks properties of authConfig or equivalent object passed to NextAuth() (imported from "next-auth").
https://authjs.dev/getting-started/authentication/credentials covers how to use credentials but its authorize() is different from tutorial as it throws an error if user is not found whereas tutorial code returns null.
https://authjs.dev/reference/nextjs#callbacks covers the callbacks including authorized. But its authorized checks whether user has a valid auth token. Both this code and the tutorial code's authorized() callback (in auth.config.ts) looka at auth parameter's user property to know whether the user is logged in or not and return false if the user is not logged in (which seems to result in user being shown login page).
Some points including questions about tutorial for which I have partial answers to some:
1) auth.ts NextAuth initialization provides pages (login page), callbacks (authorized) and credentials provider with its authorize() function (in providers array).
2) auth.config.ts provides only pages (login page) and callbacks (authorized) to middleware (as providers array is empty in auth.config.ts).
3) authorized callback function may be getting invoked by middleware for each request OR only when user goes through signIn() & authorize() methods successfully, at which time it is passed a session object or null (in a parameter named 'auth' which makes it quite confusing). The authorized callback examines the session object to know if user is logged in or not and takes appropriate redirect action if needed (like login page if user is not logged in, and dashboard page if a logged in user goes to a non-dashboard page like /).
4) login page & form indirectly (through authenticate action in app\lib\actions.ts) call signIn() method of auth.ts.
5) I think this signIn() method may be calling authorize() method of Credentials provider specified in auth.ts. The authorize() method checks user login credentials and return user object if credentials are OK or null if credentials are not OK. [From https://authjs.dev/getting-started/authentication/credentials "This (credentials) provider is designed to forward any credentials inserted into the login form (.i.e username/password) to your authentication service via the authorize callback on the provider configuration."]
6) signIn() seems to throw an AuthError if authorize() method returns null. This error is caught by authenticate action which then returns a suitable error message. But if there is no error authenticate action does not return anything.
7) Login form shows the returned error if login fails. If login succeeds, authorized() method may be getting invoked by middleware due to which user is redirected to dashboard.
8) SignOut() is straightforward, I think and so no further comment on it.
9) The tutorial app remembers user login across sessions. Who does that and how is it done? Authjs must be doing it. It probably uses JWT. [https://github.com/nextauthjs/next-auth/issues/11295 : A comment in it: "Auth.js creates an encrypted JWT to securely store information in a cookie, but you can consider it as an implementation detail."
=============
https://authjs.dev/getting-started/session-management/custom-pages covers signIn custom page usage.
https://authjs.dev/getting-started/session-management/get-session - getting user (session) object anywhere in the app seems to be easy. 'const session = await auth()'
https://authjs.dev/getting-started/session-management/protecting#nextjs-middleware gives a simple usage of authorized callback. "With Next.js 12+, the easiest way to protect a set of pages is using the middleware file. You can create a middleware.ts file in your root pages directory with the following contents."
For details, it links to https://authjs.dev/reference/nextjs#authorized - "Invoked when a user needs authorization, using Middleware." The Middleware link takes one to: https://nextjs.org/docs/pages/building-your-application/routing/middleware . But we are using App Router and so the correct page is: https://nextjs.org/docs/app/building-your-application/routing/middleware . The page does not seem to have any reference to authorized() (callback) (of authjs).
==============
I think I have invested lot of time on this chapter. In this round, I have got to learn a lot more details about authjs and middleware in Next.js as used in this chapter, than in my earlier rounds. There still are some gaps but I think I should get into that only when required. Perhaps it may need fair amount of experimentation to develop a clear understanding and plug most, if not all, of these gaps.
========================
Chapter 16, Metadata is straightforward ... no further comments.
Chapter 17 is a next steps chapter and not really part of the course.
==============
24 Sep. 2024
Now I plan to go through the tutorial app source code on my PC and see if there is code that I have not covered and need to study.
=====================.
Next.js: Authentication (Best Practices for Server Components, Actions, Middleware), https://www.youtube.com/watch?v=N_sUsq_y10U , 12 min. 13 secs., Jun. 2024 by Delba (seems to be part of Next.js team).
--------
Notes from quickly going through tutorial app code (project on PC):
1) I have deliberately omitted studying intricate styling like shimmer (seems to be animation) used in app\ui\skeletons.tsx with a corresponding (customization) entry in tailwind.config.ts.
2) I have deliberately omitted studying details of small level of Tailwind customization in tailwind.config.ts (has few other stuff in addition to shimmer). It uses a plug-in @tailwindcss/forms which seems to be this: https://github.com/tailwindlabs/tailwindcss-forms - "A plugin that provides a basic reset for form styles that makes form elements easy to override with utilities."
3) Tutorial code on my PC (older version) uses pending from useFormStatus in app\ui\login-form.tsx. In the current version of tutorial on next.js site - https://nextjs.org/learn/dashboard-app/adding-authentication#updating-the-login-form , it uses isPending from useActionState.
4) Skipped studying small utility component Button: app\ui\button.tsx . It is used in login-form.tsx and at least two other components (create and edit invoice forms).
5) CustomersTable in app\ui\customers\table.tsx does not seem to be used by other app code. So I don't think I have studied it.
=======================
I think that finishes this 3rd round (on 24 Sep. 2024).
Comments
Post a Comment