Rules to Better Next.js - 10 Rules
Want to build a web app with Next.js? Check SSW's Next.js consulting page.
React is a powerful JavaScript library for building user interfaces. However, it doesn't provide built-in support for server-side rendering (SSR). This lack of SSR can lead to slow website load times and poor search engine optimization (SEO). That's where Next.js comes in. Next.js is a framework built on top of React that provides several features and benefits for building high-performance websites.
A quick summary of Next.js (pay attention to the first 2.5 min)
Video: Next.js in 100 Seconds // Plus Full Beginner's Tutorial (11 min)A deeper dive on Next.js
Video: Theo Browne: Next.js is a backend framework (11 min)✅ Reasons to choose Next.js
Here are some reasons to consider using Next.js instead of React alone:
- Incremental Static Regeneration: Next.js allows you to create or update static pages after you’ve built your site. Incremental Static Regeneration (ISR) enables you to use static-generation on a per-page basis, without needing to rebuild the entire site. With ISR, you can retain the benefits of static while scaling to millions of pages.
- TypeScript: Next.js provides an integrated TypeScript experience, including zero-configuration set up and built-in types for Pages, APIs, and more.
- Internationalized Routing: Next.js has built-in support for internationalized (i18n) routing since v10.0.0. You can provide a list of locales, the default locale, and domain-specific locales and Next.js will automatically handle the routing.
- API routes: API routes provide a solution to build your API with Next.js. Any file inside the folder
pages/api
is mapped to/api/*
and will be treated as an API endpoint instead of a page. They are server-side only bundles and won't increase your client-side bundle size. - Server-side rendering: Next.js provides built-in support for SSR, which can significantly improve website performance and SEO by rendering pages on the server and sending HTML to the client.
- Dynamic Import: Next.js supports lazy loading external libraries with
import()
and React components withnext/dynamic
. Deferred loading helps improve the initial loading performance by decreasing the amount of JavaScript necessary to render the page. Components or libraries are only imported and included in the JavaScript bundle when they're used.next/dynamic
is a composite extension ofReact.lazy
andSuspense
, components can delay hydration until the Suspense boundary is resolved. - Automatic code splitting: Next.js automatically splits code into smaller chunks, which can improve website performance by reducing the initial load time.
- Built-in CSS, image, and font optimization: Next.js has built-in support for optimizing CSS, images and fonts. These can help reduce website load times and improve performance.
- Automatic Static Optimization: Next.js automatically determines that a page is static (can be prerendered) if it has no blocking data requirements. This feature allows Next.js to emit hybrid applications that contain both server-rendered and statically generated pages.
- MDX: MDX is a superset of markdown that lets you write JSX directly in your markdown files. It is a powerful way to add dynamic interactivity, and embed components within your content, helping you to bring your pages to life.
- Incremental adoption: Next.js allows developers to add server-side rendering and other advanced features incrementally to an existing React application, making it easy to adopt and integrate into existing projects.
- Codemods: Next.js provides Codemod transformations to help upgrade your Next.js codebase when a feature is deprecated. Codemods are transformations that run on your codebase programmatically. This allows for a large amount of changes to be applied without having to manually go through every file.
Summary
By using Next.js instead of React alone, developers can reduce the pain points users may experience and build high-performance websites more efficiently. However, it's important to note that Next.js may not be the best choice for every project, and developers should evaluate their project's specific needs and requirements before making a decision.
Typescript is the best choice when writing Angular and React applications. Angular is even written in Typescript itself!
Video: Typescript in 100 Seconds✅Advantages of Using TypeScript
-
Type Safety
- Error detection: - Identify and correct errors during the build phase, preventing runtime surprises.
- Top-notch tooling - Utilize enhanced features like autocomplete, Intellisense, efficient code navigation, and linting.
- Streamlined refactoring - Superior tooling simplifies refactoring compared to plain JavaScript.
- Embrace the latest - Leverage the latest language innovations for cleaner, more concise code.
-
Enhanced Code Expressivity
- Syntax sugar - Improves code readability and intuitiveness.
- Automatic imports - Streamline module integrations.
-
Wider Browser Support
- Multi-version targeting - Use a single TypeScript codebase across various JavaScript versions (e.g., ES5, ES6).
-
Boosted Code Confidence and Maintainability
- Minimized risk - Reduce the likelihood of bugs and bolster code reliability.
- Time efficiency - Dedicate less time to unit tests with increased code trustworthiness.
- Early bug detection - Identify and rectify issues early.
- Clarity - Craft cleaner, more transparent code.
❌ Disadvantages of TypeScript
- Learning curve - Developers unfamiliar with statically typed languages might face an initial learning challenge.
- Compilation step - An additional step to compile TypeScript to JavaScript can sometimes be perceived as a minor inconvenience.
- Integration with some libraries - Not all JavaScript libraries come with TypeScript definitions by default.
🔍 Explore TypeScript further at the official TypeScript website.
🎥 If you prefer video content, have a look at SSW TV Videos on TypeScript.
-
Next.js is great, as it gives you the ability to run code on the server-side. This means there are now new ways to fetch data via the server to be passed to Next.js app. Next.js also handles the automatic splitting of code that runs on the server and the client, meaning you don't have to worry about bloating your JavaScript bundle when you add code that only runs on the server.
Server Side Fetching
There are three primary ways with the Next.js Pages Router to fetch data on the server:
- Server-side data fetching with
getServerSideProps
- Static site generation (SSG) with
getStaticProps
- Hybrid static site generation with incremental static regeneration (ISR) enabled in
getStaticProps
getServerSideProps
getServerSideProps
allows for server-side fetching of data on each request from the client, which makes it great for fetching of dynamic data. It can also be used for secured data, as the code within the function only runs on the server.The below example shows an example of how we can use
getServerSideProps
to fetch data. Upon each user's request, the server will fetch the list of posts and pass it as props to the page.// pages/index.tsx export const getServerSideProps = async (context) => { const res = await fetch("https://jsonplaceholder.typicode.com/posts"); const posts = await res.json(); return { props: { posts } }; }; export default function Page(props) { return ( <div> {props.posts.map((post) => ( <div> <h2>{post.title}</h2> <p>{post.body}</p> </div> ))} </div> ); }
This is great for dynamic data that may not be best suited for
getStaticProps
such as fetching from a database or an API route with data that changes often.The
context
parameter also has a lot of useful information about the request, including the request path, cookies sent from the client, and more that can be found on the official Next.js documentation.You can use https://next-code-elimination.vercel.app/ to verify what code is sent to the client when using
getServerSideProps
.getStaticProps
We can develop a staticly generated site in Next.js by using
getStaticProps
. Having a statically generated site is great for SEO, as it makes it much easier for Google to index your site compared to a site with complex JavaScript logic, which is harder for web crawlers to understand. When you runnpm build
, Next.js will run the code inside thegetStaticProps
method and generate associated static HTML or JSON data.For example, using dynamic routing we can create a static page to show post data based on the URL:
// pages/[slug].tsx export const getStaticProps = async ({ params }) => { const id = params.slug; const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`); const post = await res.json(); return { props: { post }, }; }; export const getStaticPaths = async () => { const res = await fetch("https://jsonplaceholder.typicode.com/posts"); const posts = res.json(); const paths = posts.map((post) => ({ params: { slug: post.id }, })); return { paths, fallback: false }; }; export default function Page(props) { return ( <div> <h2>{props.post.title}</h2> <p>{props.post.body}</p> </div> ); }
Incremental Static Regeneration (Hybrid)
Server-side generation is great for SEO, however if we have a data source that may change between builds, we may need to regenerate the static data generated at build time. With incremental static regeneration (ISR), we can add an expiry time for static data, and Next.js will automatically refetch the data if the expiry time has been reached. However, this will not block the current request where it has noticed that the data has expired with a long loading time - it will fetch the data only for the next request.
export const getStaticProps = async () => { const res = await fetch(`https://jsonplaceholder.typicode.com/comments`); const comments = await res.json(); return { props: { comments }, revalidate: 60, }; };
This means that if 60 seconds or more has passed after the last time
getStaticProps
was run and the user makes a request to the page, it will rerun the code inside getStaticProps, and render the newly fetched data for the next page visitor.Client Side Fetching
If you want to fetch secured data from a component (not a page) without exposing confidential information to the user (e.g. keys, IDs), the best way to do this is to create a basic API route to fetch this data, which allows for storage of sensitive information on the server, unable to be exposed to the client.
This would be written in the component like so:
const Component = () => { const [data, setData] = useState(null); useEffect(() => { fetch("/api/your-api-route") .then((res) => res.json()) .then((data) => { setData(data); }); }, []); return <> ... </>; };
Then place a file in the /pages/api directory named with the required API route path (i.e.
pages/api/{{ API_ROUTE_HERE }}.ts
):// pages/api/your-api-route.ts import { NextApiRequest, NextApiResponse } from "next"; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { if (req.method == "GET") { const res = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await res.json(); res.status(200).send(data); } else { res.status(405).json({ error: "Unsupported method" }); } }
This is a great workaround for the limitation of only being able to use the above server-side fetching functions at a page-level - as it allows for server-side fetching from components. However, keep in mind that this may result in performance impacts from blocking calls to API routes.
This is also a great way to reduce the occurrence of CORS errors, as you can proxy API data through a simple Next.js API route.
- Server-side data fetching with
While using a regular
useEffect
to run when a component is loaded to fetch data is super easy, it may result in unnecesary duplicate requests for data or unexpected errors when unmounting components. It is best to use a library that can provide hooks for fetching data, as not only does it solve the above issues, but also comes with useful features such as caching, background updates, and pre-fetching.Below is an example of a standard data fetch in React:
const Component = () => { const [data, setData] = useState({}); const [loading, setLoading] = useState(true); useEffect(() => { fetch("https://jsonplaceholder.typicode.com/todos/1") .then(res => res.json()) .then(json => { setData(json); setLoading(false); }) }, []) return ( {loading ? <> {/* Display data here */} </> : <p>Loading...</p> } ) }
Figure: The traditional way of fetching data in React
This example is not ideal, as it means every time we reload this page component, or if we make the same request on another page, there will be an unnecessary request made instead of pulling the data from a cache.
Below are the two recommended options that both serve effectively the same purpose in providing developers with useful hooks for fetching data. These libraries not only give developers a wide range of other features, but also reduces the amount of boilerplate code they have to write.
TanStack Query (previously React Query) - Recommended
TanStack Query is a feature-rich data fetching library developed by Tanstack. It can be used with existing data fetching libraries such as Axios, GraphQL packages such as graphql-request, or just plain fetch.
Video: React Query in 100 Seconds by Fireship (2 mins)Here's a basic example of how you can use Tanstack Query:
import { useQuery, QueryClient, QueryClientProvider, } from "react-query"; const queryClient = new QueryClient(); function useTodos() { return useQuery("todos", async () => { const res = await fetch("/api/todos"); const json = await res.json(); return json; }) } export const Page = () => { const { status, data, error, isFetching } = useTodos(); if (status === "error") return <div>Error loading data: {error}</div> if (status === "loading") return <div>Loading...</div> return ( <QueryClientProvider client={queryClient}> <div> <div>{/* Display todos here */}</div> {isFetching && <p>Re-fetching data in the background...</p>} </div> </QueryClientProvider> }
This code employs the useQuery hook for asynchronous data fetching and a QueryClientProvider to manage the query cache in the component tree.
Some features of Tanstack Query:
- Request caching - key values pairs using
useQuery
- Duplicate request flattening -
- Background data fetching - using the
isFetching
value - Automatic retries - failed fetches are retried with the
retry
andretryDelay
options inuseQuery
, allowing you to specify the number of retries before giving up - Built-in pagination - using the
data.hasMore
value - Automatic revalidation - data revalidated on window focus - learn more
- Prefetching data - using
prefetchQuery
- Optimistic updates - if a request made fails, but a state variable in UI has already been updated optimistically, Tanstack Query can revert to the old UI state - learn more
- Suspense - built-in support for React 18's Suspense with the
{ queries: { suspense: true }}
option added toQueryClient
- Scroll Restoration - maintains the exact position you are scrolled on a webpage - learn more
- React Query DevTools - a Chrome extension that allows for easy debugging of data fetches + caching - learn more
You can find out more about Tanstack Query at tanstack.com/query.
SWR
SWR is an alternative to Tanstack Query developed by Vercel, the team behind Next.js. Much like Tanstack Query, SWR is library-agnostic, meaning you can use whatever data fetching library you are comfortable with.
Here's a basic example of how you can use the library's fetching hook:
const fetcher = (url) => fetch(url).then((res) => res.json()); export const Page = () => { const { data, error, isLoading } = useSWR("/api/todos", fetcher); if (error) return <div>Error loading data</div>; if (loading) return <div>Loading...</div>; return <div>{/* Display todos here */}</div>; };
Some features of SWR:
- Small bundle size - only 4.4 kB
- Caching - automatic caching of requests to avoid making duplicate requests
- Duplicate request flattening - a global cache to prevent different components fetching the same data
- Automatic revalidation - can happen when either the page is focused or when a user reconnects to the internet
- Pagination - using the
useSWRInfinite
hook - Suspense - Built-in support for React 18's Suspense with the
{ suspense: true }
option - Next.js - Integration with Next.js's SSR/SSG capabilities
- Real-time data - support for technologies such as WebSockets and SSE with the
useSWRSubscription
hook
Note: Currently, the vast majority of SWR APIs are not compatible with the App router in Next.js 13.
You can find out more about using SWR at swr.vercel.app.
RTK Query
Additionally, RTK Query, part of the Redux Toolkit, is a similar library to SWR and React Query with tight integration with Redux and seamless type-safe importing sourced from OpenAPI specifications.
Here's a basic example of how you can use RTK Query:
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; const todosApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: "/api" }), endpoints: (builder) => ({ getTodos: builder.query<Array<Todo>, void>({ query: () => "todos", }), }), }); const { useGetTodosQuery } = todosApi; // For use with Redux const todosApiReducer = todosApi.reducer; const TodoPage = () => { const { data, isError, isLoading } = useGetTodosQuery(); if (isLoading) return <p>Loading...</p>; if (isError) return <p>Error fetching todos</p>; return <div>{/*( Display todos here */}</div>; };
Some features of RTK Query:
- Seamless Redux integration: Designed as part of the Redux Toolkit, RTK Query is intrinsically designed to work with Redux, providing a cohesive data management experience. Learn more
- OpenAPI schema code generation: Auto-generates end-to-end typed APIs based on OpenAPI schemas, drastically reducing boilerplate and ensuring type safety. Learn more
- Caching - cache management based on endpoint and serialized arguments - learn more
- Automatic retries - built-in mechanism to automatically retry failed queries, enhancing resilience - learn more
- Prefetching - fetches data in anticipation of user actions to enhance UX - learn more
- Parallel and dependent queries: Efficient handling of multiple simultaneous or dependent data fetching. Learn more
Discover more about RTK Query in Redux Toolkit's official documentation at redux-toolkit.js.org/rtk-query/overview.
- Request caching - key values pairs using
Components with large imports loaded in on runtime may result in a much worse UX for users of your web app. Next.js can help with this by using dynamic imports to only load these large components when they are rendered on the page.
When using the Next.js pages router, we can use
next/dynamic
to lazy load components, based on React'sReact.lazy
andSuspense
.const HeavyComponent = dynamic(import("./components/HeavyComponent"), { loading: () => <p>Loading...</p>, }); export const Page = () => { const [showComponent, setShowComponent] = useState(false); return ( <> ... {showComponent && <HeavyComponent />} ... </> ); };
This means that the
<HeavyComponent>
element will only be loaded in when theshowComponent
state variable is true. When condition is then set totrue
, the paragraph component in theloading
field will display until the component has been loaded onto the page.This works by packing the heavy component into a separate JavaScript bundle, which Next.js then sends to the client when the
showComponent
variable is true.You can learn more about how to use
next/dynamic
in the official Next.js documentation.NextJS supports dynamic routes out of the box, meaning you can create routes from dynamic data at request or build time. This is especially useful for sites such as blogs that have large amounts of content.
Dynamic routes allow developers to accommodate unpredictable URLs. Instead of defining a static path, segments of the path can be dynamic.
Why Use Dynamic Routes?
- Flexibility: Easily cater to a wide variety of content without setting up individual routes.
- Optimization: Efficiently serve content based on real-time data or user-specific requirements.
Folder Structure
To tap into this feature, wrap your folder's name in square brackets, for instance,
[filename].tsx
or[slug].tsx
.The directory structure should mirror the dynamic nature of the routes. Here's a standard representation:
pages/ |-- [slug]/ | |-- index.tsx |-- [id]/ | |-- settings.tsx
Figure: Here, both slug and id are dynamic route segments
For scenarios where routes need to capture multiple path variations, Next.js introduces the "catch-all" feature. This can be employed by prefixing an ellipsis "..." to the dynamic segments.
To delve deeper into the intricacies of Dynamic Routes, consider exploring the official Next.js documentation.
getStaticProps
When you export getStaticProps, your page will be pre-rendered at build time. You can use
getStaticProps
to retrieve data that will be used to render the page. For example, you might receive a file name from the requested URL, i.e./page/{{ FILENAME }}
, which you can then use in an API call to get the props for that page:export const getStaticProps = async ({ params }) => { const apiUrl = {{ API URL }} + params.filename; const response = await fetch(apiUrl); return { props: { data: response } } }
The props from above can then be used from your page, and the data will be filled depending on the dynamic route the user has navigated to:
export default function Page( props: InferGetStaticPropsType<typeof getStaticProps> }) { <p> props.data </p> }
When using
getStaticProps
, you must also usegetStaticPaths
in order for dynamic routing to work.getStaticPaths
The
getStaticPaths
function is used alongsidegetStaticProps
and returns a list of paths, which NextJS will use to generate the dynamic pages.export const getStaticPaths = async () => { const apiUrl = {{ API URL }}; const response = await fetch(apiUrl); return { paths: response, fallback: false, } }
paths
is the list of pages you want to generate.
fallback
is a boolean value that determines how NextJS handles routes that are not generated at build time, and can be set to:false (default)
- Any request for a page that has not been generated will return a 404true
- The page will be generated on demand if not found and stored for subsequent requestsblocking
- Similar to true, except NextJS will not respond to the request until the page has finished generating
React Hooks streamline state management and lifecycle processes in functional components, resulting in cleaner, more performant code. These are the most common and useful hooks that you should use in your React project:
1. useState: Managing Local State 🧠
The
useState
hook lets you add state to functional components. CalluseState
at the top level of your component to declare one or more state variables.import { useState } from "react"; export default function Counter() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); } return <button onClick={handleClick}>You pressed me {count} times</button>; }
Figure: Using useState for a counter component
Naming Convention: It's a common convention to name state variables using the pattern [count, setCount] with array destructuring.
useState
returns an array with exactly two items:- The current state of this state variable, initially set to the initial state you provided.
- A function that lets you update its value.
✅ Recommended Usage
- Updating Objects and Arrays: Manage and adjust objects and arrays in the state. Remember, always create new references instead of mutating
- Avoiding Recreating the Initial State: Ensure the initial state is set only once, avoiding recalculations in subsequent renders
- Resetting State with a Key: Reset the component's state by altering its key
- Storing Information from Previous Renders: On rare occasions, adjust state as a reaction to a rendering process
⚠️ Pitfalls
- State Updates: A change in state doesn't instantly reflect within the current executing code. It determines what
useState
will return in future renders - Initializer Function: When you pass a function to
useState
, it gets called only during the initialization phase - State Updates with Functions: When deriving new state values from the previous state, it's better to use an updater function as an argument of the setter function instead of the new value i.e.
setObj(prev => { key: value ...prev }
. This ensures you're working with the most up-to-date state
Read more about
useState
on the offical docs2. useEffect: Side Effects & Lifecycles 🔄
In React functional components, useEffect serves as your toolkit to execute side effects, reminiscent of lifecycles in class-based components. Through dependencies, you can control when these effects run, granting granular control over side effect operations.
import { useState, useEffect } from "react"; export default function Counter() { const [count, setCount] = useState(0); useEffect(() => { const intervalId = setInterval(() => { setCount((c) => c + 1); }, 1000); return () => clearInterval(intervalId); }, []); return <h1>{count}</h1>; }
Figure: useEffect Count example
It's similar in concept to Angular's ngOnChanges lifecycle hook. While ngOnChanges in Angular detects and reacts to changes in input-bound properties, React's useEffect serves a broader purpose.
✅ Recommended Usage
- External System Connection: Link React components to other systems like APIs, networks, or third-party libraries
- Custom Hooks Encapsulation: Nest your effect logic inside custom hooks for clarity and better structure
- Non-React Widget Control: Bridge the gap between React components and non-React widgets
- Data Fetching: While
useEffect
can fetch data, it's optimal to use the framework's standard mechanisms or custom hooks - Reactive Dependencies: Identify reactive items (e.g., props, state) that influence your effect. When these alter, the effect kicks in again
- State Updates: Adjust state values referencing their former versions using
useEffect
- Access to Recent Props & State:
useEffect
ensures the most recent props and state are at your disposal - Distinct Server/Client Content: With effects operational solely on the client, you can orchestrate unique content for server and client views
⚠️ Pitfalls
- Placement: Ensure
useEffect
remains at the top level of your components/custom hooks. Bypass calling it within loops or conditionals - Avoid Overuse: Turn to
useEffect
mainly for external synchronization - Strict Mode Nuances: In strict mode, a preliminary setup+cleanup cycle, exclusive to development, verifies your cleanup's alignment with your setup
- Dependencies Oversight: If dependencies comprise inner-component objects or functions, they might trigger frequent effect repetitions
- Visual Effects Caution: A visual glitch before an effect suggests
useLayoutEffect
might be a better pick - Server Rendering:
useEffect
is client-centric and doesn't engage during server-side rendering
Read more about
useEffect
on the offical docs3. useContext: Using Context Seamlessly 🌍
useContext
is a pivotal React Hook, giving you the power to both read and subscribe to context values right within your component.import { createContext, useContext } from "react"; // Create a context const ThemeContext = createContext({ background: "light", foreground: "dark", }); function ThemedButton() { const theme = useContext(ThemeContext); return ( <button style={{ background: theme.background, color: theme.foreground }}> I am styled by theme context! </button> ); } export default function App() { return ( <ThemeContext.Provider value={{ background: "black", foreground: "white" }}> <ThemedButton /> </ThemeContext.Provider> ); }
Figure: A Themed button example using useContext
✅ Recommended Usage
- Reading and Subscribing to Context: Directly access and subscribe to context values straight from your component
- Passing Data Deeply: Bypass manual prop-drilling, letting you transmit data deeply through the component hierarchy
- Updating Data Passed via Context: Easily modify context values and integrate them with state for seamless updates across various components
- Specifying a Fallback Default Value: If no context provider is present upstream,
useContext
will resort to the default value established during context creation - Overriding Context: For tailored requirements, override the context in specific parts of the component tree by enveloping it in a provider with a distinct value
- Optimizing Re-renders: Amplify performance when transferring objects or functions through context using techniques such as
useCallback
anduseMemo
⚠️ Pitfalls
- Provider Position: The context search strategy employed by
useContext
is top-down, always eyeing the nearest provider. It disregards providers present in the invoking component - Server rendering: Similar to
useEffect
context providers can only be initialized and used within client components when using Next.js. Refer to the Next.js official documentation for information about using context providers in server components. - Re-rendering Children: Context shifts compel React to re-render all child components stemming from the provider with a changed value. The assessment hinges on the
Object.is
comparison, meaning that evenmemo
cannot fend off updates stemming from refreshed context values - Duplicate Modules: Be wary of build systems churning out duplicate modules (e.g., due to symlinks). This can disintegrate context as both the provider and consumer must be the exact same object, passing the
===
comparison test - Provider Without a Value: An absent value prop in a provider translates to
value={undefined}
. The default fromcreateContext(defaultValue)
comes into play only when there's a complete absence of a matching provider - Provider Cannot be Accessed: If
useContext
is used in a component that is not wrapped by a provider, this can cause client-side errors as the value accessed will be null
Read more about
useContext
on the offical docs4. useRef: Direct DOM Access & Persistent References 🎯
The
useRef
hook in React allows you to access and interact with DOM elements or maintain a mutable reference to values across renders without triggering a re-render.import { useRef } from "react"; function MyComponent() { const inputRef = useRef(null); function handleFocus() { inputRef.current.focus(); } return ( <> <input ref={inputRef} /> <button onClick={handleFocus}>Focus the input</button> </> ); }
Figure: On button click focus the input using useRef
✅ Recommended Usage
- Referencing a Value:
useRef
lets you reference a value that doesn't affect the rendering of your component - Manipulating the DOM: By attaching the returned ref object as a ref attribute to a JSX node, React sets its current property to that DOM node, allowing direct DOM manipulations
- Avoiding Recreating the Ref Contents: React preserves the initial ref value and doesn't recreate it during subsequent renders. This is beneficial for computationally expensive values
- Storing Information from Previous Renders: Refs persist their data across renders and can store information that doesn’t initiate a re-render
- Accessing Another Component's DOM Nodes: With
React.forwardRef()
, you can expose refs of the DOM nodes inside custom components
⚠️ Pitfalls
- Mutable current Property: Although
ref.current
is mutable, avoid mutating objects used in rendering - No Re-render on Change: Adjusting
ref.current
doesn’t trigger a re-render; React doesn’t detect changes to the ref - Avoid Reading/Writing During Rendering: Refrain from accessing or altering
ref.current
while rendering, except for its initialization - Strict Mode Double Render: In strict mode, React may execute your component twice for side effect detection. This double execution results in the ref object being created twice, though one is discarded
- Pure Component Behavior: React assumes your component is a pure function. Interacting with a ref during rendering contradicts this presumption
Read more about
useRef
on the offical docs5. useReducer: Advanced State Logic 📊
The
useReducer
is a React Hook that lets you add a reducer to your component, providing a more predictable state management method compared touseState
.import React, { useReducer } from "react"; function counterReducer(state, action) { switch (action.type) { case "increment": return { count: state.count + 1 }; case "decrement": return { count: state.count - 1 }; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(counterReducer, { count: 0 }); return ( <div> <p>Count: {state.count}</p> <button onClick={() => dispatch({ type: "increment" })}>Increment</button> <button onClick={() => dispatch({ type: "decrement" })}>Decrement</button> </div> ); }
Figure: React Counter Component Using useReducer
✅ Recommended Usage
- Adding a Reducer to a Component:
useReducer
allows you to manage your component's state using a reducer function - Predictable State Updates: Reducers specify how the state transitions from one state to the next, making state updates more predictable
- Handling Complex State Logic: It's suitable for managing state logic that's more complex than what useState can handle
- Avoiding Recreating the Initial State: React saves the initial state once and ignores it on subsequent renders. This is useful for values that are expensive to compute
- Dispatching Actions: Actions describe user interactions or events that trigger state changes. By convention, actions are objects with a type property
- Batching State Updates: React batches state updates, ensuring that the screen updates after all event handlers have run
⚠️ Pitfalls
- State Mutations: State in reducers should be treated as immutable. Avoid mutating state directly; always return new state objects
- Incomplete State Updates: Ensure that every branch in your reducer returns all parts of the state
- Unexpected State Values: If your state unexpectedly becomes undefined, it's likely due to missing state in one of the reducer cases or a mismatched action type
- Too Many Re-renders: This error typically indicates that you're unconditionally dispatching an action during render, leading to an infinite loop
- Impure Reducers: Reducers should be pure functions. Impurities can lead to unexpected behaviors, especially in strict mode where reducers might be called twice
Read more about
useReducer
on the offical docsWhile React is great for developing high-quality web apps, Next.js offers a wide range of features that React doesn't have, like static site generation (SSG), server-side rendering (SSR) and API routes.
Codemod
A great automated way to migrate your create-react-app site to Next.js is by using the
@next/codemod
tool.npx @next/codemod cra-to-next
This tool is a great starting point, but make sure to check all intended functionality has been ported correctly.
Data Fetching
It is also important to understand that when moving over the client-side functionality of the React app, it will not be using any of the powerful server-side Next.js features. If you can find parts of your application that can be moved to
getServerSideProps
orgetStaticProps
, make sure to manually add this functionality.Routing
It is important to keep in mind that Next.js uses file-based routing, so there must be additional care in migrating React applications that use
react-router
orreact-router-dom
. While these libraries will still function as intended in Next.js by running this code on the client, many of the advantages of using Next's file-based routing will not be realised.By using the
[id].tsx
, we can create a dynamic route, where the ID can be accessed via props. This can then be used by either client-side React code or the Next.js specific server-side functionsgetStaticProps
andgetSeverSideProps
to fetch data based on the request URL.When working with Node.js, choosing the right package manager can significantly impact your project's performance, consistency, and ease of use. While npm is the default, developers often seek alternatives like Yarn, Bun, or pnpm for various advantages. But which one should you use?
1. pnpm (Recommended ✅)
- Efficient Disk Space Usage: pnpm uses a content-addressable file system to store all files in a single place on the disk. This means multiple projects can share the same packages, reducing disk space usage
- Fast and Reliable: With pnpm, package installations are faster because it avoids duplicating files in
node_modules
. Instead, it creates hard links, which makes the process quicker and more efficient - Strict Dependency Management: pnpm enforces stricter rules for dependency resolution. Unlike npm and Yarn, pnpm prevents "phantom dependencies," ensuring that your project is more predictable and less prone to errors
2. npm
npm is the default package manager bundled with Node.js. It is straightforward to use and integrates seamlessly with the Node ecosystem.
Notable Incident: In 2016, the removal of the "left-pad" package from npm caused widespread issues, making developers reconsider their reliance on the platform.
Pros:
- Comes pre-installed with Node.js, so no additional setup is needed
- Vast package registry with millions of packages
Cons:
- Slower compared to pnpm and Yarn
- Issues with dependency resolution and "phantom dependencies."
3. Yarn
Yarn was developed by Facebook to address some of npm's shortcomings, such as speed and reliability.
Pros:
- Faster than npm, especially with the offline cache feature
- Better dependency management and deterministic builds with Yarn's
yarn.lock
file
Cons:
- Slightly more complex to configure compared to npm
- Still not as space-efficient as pnpm
4. Bun
Bun is a newer entrant that aims to be an all-in-one tool for Node.js, combining package management with a fast JavaScript runtime and bundler.
Pros:
- Extremely fast, built from the ground up in Zig, a systems programming language
- Includes built-in support for TypeScript and JSX, making it attractive for modern web development
Cons:
- Relatively new and less mature than the other options
- Smaller community and less extensive documentation
While npm, Yarn, and Bun each have their strengths, pnpm is the recommended package manager for most Node.js projects. Its efficient use of disk space, faster installations, and stricter dependency management make it a superior choice. However, the best package manager for you may depend on your specific project's needs and your team's preferences.
Core Web Vitals are super important metrics to measure how good your page's performance is. It's also incredibly important to how Google ranks page results.
The most important Core Web Vitals at time of writing is Largest Contentful Paint (LCP), Interaction To Next Paint (INP) and Cumulative Layout Shift (CLS).
Types of Web Vitals
Largest Contentful Paint (LCP)
LCP measures the time it takes for the largest element in the current viewport to load, i.e. is measuring how long it takes most of the page to load. Read more on Google's page on Largest Contentful Paint (LCP).
Interaction To Next Paint (INP)
INP measures the responsiveness of the page, i.e. the latency when you interact with elements on the page. Read more on Google's page on Interaction To Next Paint (INP).
Cumulative Layout Shift (CLS)
CLS measures how much elements have shifted on the page from the first load. For example, adding an element after a
fetch
call has completed will result in a higher CLS value. Read more on Google's page on Cumulative Layout Shift (CLS).Measuring Web Vitals
Framework-Agnostic (web-vitals)
To capture these metrics in most frontend environments, you would use the
web-vitals
npm package.import { onCLS, onFID, onLCP } from 'web-vitals'; function sendToTracker (metric) { // Send to an aggregate of your choice i.e. Azure App Insights, Google Analytics, Sentry, etc. } onCLS(sendToTracker); onFID(sendToTracker); onLCP(sendToTracker);
Next.js
Next.js has a built in custom React hook to track vitals, with additional information relating to Next.js performance such as hydration and rendering time.
import { useReportWebVitals } from 'next/web-vitals' function App { useReportWebVitals((metric) => { switch (metric.name) { case "CLS": case "FID": case "LCP": case "Next.js-hydration": case "Next.js-render": // Send to an aggregate of your choice i.e. Azure App Insights, Google Analytics, Sentry, etc. break; } }); return <>{/* ... */}</> }
Using Web Vitals Data
When ingesting Core Web Vitals data, it's important to extract only the important information - as this data will likely be coming from every page visitor.
The primary focus of optimization work should be focused on the 75th percentile of the worst scores, as that usually represents the average device that users will be accessing your site on. It's also important to focus on improving higher percentiles, such as the 90th (P90), 95th (P95) and 99th (P99).
There are a variety of services that you can use for collecting data Core Web Vitals data:
- Sentry - see Sentry's marketing page on monitoring Web Vitals
- Google Analytics - see Google's how-to page on monitoring Web Vitals with GA4 and BigQuery
- Azure Application Insights - see the Microsoft documentation on metric tracking with App Insights
To track this data on App Insights you can use
trackMetric
:applicationInsights.trackMetric( { name: "CLS", average: metric.value }, { page: currentPagePath } );