SSW Foursquare

Rules to Better Next.js - 9 Rules

Want to build a web app with Next.js? Check SSW's Next.js consulting page.

  1. Do you know why Next.js is awesome?

    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:

    1. 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.
    2. TypeScript: Next.js provides an integrated TypeScript experience, including zero-configuration set up and built-in types for Pages, APIs, and more.
    3. 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.
    4. 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.
    5. 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.
    6. Dynamic Import: Next.js supports lazy loading external libraries with import() and React components with next/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 of React.lazy and Suspense, components can delay hydration until the Suspense boundary is resolved.
    7. Automatic code splitting: Next.js automatically splits code into smaller chunks, which can improve website performance by reducing the initial load time.
    8. 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.
    9. 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.
    10. 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.
    11. 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.
    12. 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.

  2. Do you use Typescript?

    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

    1. 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.
    2. Enhanced Code Expressivity

      • Syntax sugar - Improves code readability and intuitiveness.
      • Automatic imports - Streamline module integrations.
    3. Wider Browser Support

      • Multi-version targeting - Use a single TypeScript codebase across various JavaScript versions (e.g., ES5, ES6).
    4. 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

    1. Learning curve - Developers unfamiliar with statically typed languages might face an initial learning challenge.
    2. Compilation step - An additional step to compile TypeScript to JavaScript can sometimes be perceived as a minor inconvenience.
    3. 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.

  3. Do you know how to fetch data in Next.js?

    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:

    1. Server-side data fetching with getServerSideProps
    2. Static site generation (SSG) with getStaticProps
    3. 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 run npm build, Next.js will run the code inside the getStaticProps 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.

  4. Do you know the best libraries to fetch data in React?

    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 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:

    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:

    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.

  5. Do you know when to use dynamic imports in Next.js?

    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's React.lazy and Suspense.

    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 the showComponent state variable is true. When condition is then set to true, the paragraph component in the loading 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.

  6. Do you use dynamic routing in Next.js?

    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 use getStaticPaths in order for dynamic routing to work.

    getStaticPaths

    The getStaticPaths function is used alongside getStaticProps 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 404
    • true - The page will be generated on demand if not found and stored for subsequent requests
    • blocking - Similar to true, except NextJS will not respond to the request until the page has finished generating
  7. Do you use these useful React Hooks?

    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. Call useState 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:

    1. The current state of this state variable, initially set to the initial state you provided.
    2. A function that lets you update its value.
    • 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 docs

    2. 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.

    • 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 docs

    3. 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

    • 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 and useMemo

    ⚠️ 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
    • 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 even memo 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 from createContext(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 docs

    4. 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

    • 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 docs

    5. 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 to useState.

    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

    • 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 docs

  8. Do you know how to migrate React projects to Next.js?

    While 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

    migration terminal
    Figure: example terminal output in converting CRA applications to Next.js

    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 or getStaticProps, 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 or react-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.

    react router dom
    Figure: Bad example - keeping `react-router-dom` in the Next.js app

    next routing
    Figure: Good example - adapting your project to use Next.js file-based routing

    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 functions getStaticProps and getSeverSideProps to fetch data based on the request URL.

  9. Do you know the best package manager for React?

    When it comes to package management in JavaScript, 3 major players dominate the scene. Have you considered which one offers the best synergy with your development goals?

    While all three package managers have their strengths, the choice often boils down to the specific needs of a project and the preferences of the development team. Yarn offers a balanced blend of speed and reliability, npm provides familiarity and wide adoption, and pnpm shines in terms of efficiency and space-saving.

    1. npm

    npm logo

    Overview: npm has long been the backbone of JavaScript development. It is the default package manager for the Node.js JavaScript runtime environment and has been widely adopted by the developer community.

    Notable Incident: In 2016, the removal of the "left-pad" package from npm caused widespread issues, making developers reconsider their reliance on the platform.

    Strengths:

    • Mature & Widely Adopted: npm has a long history and vast package repository.
    • Integrated with Node.js: Being the default for Node.js makes it straightforward for many developers.

    yarn logo

    Overview: Introduced by Facebook, Yarn was developed as an alternative to npm, addressing some of the issues developers faced with npm.

    Strengths:

    • Speed: Yarn is known for its faster package installation times compared to npm.
    • Offline Support: Once you've installed a package with Yarn, it can be reinstalled without an internet connection, preventing potential disruptions like the "left-pad" incident.
    • Deterministic Installs: Yarn generates a lock file to ensure consistent installations across different systems.

    pnpm logo

    Overview: pnpm is a newer entrant in the package manager arena, but it brings unique features to the table.

    Strengths:

    • Efficiency: pnpm's installation speed is even faster than Yarn's and npm's due to its unique approach of linking packages from a global cache.
    • Disk Space Savings: By linking to a global cache, pnpm ensures packages aren't redundantly stored across multiple projects.
    • Strict Package Isolation: pnpm ensures that projects get exactly what they need and no additional, potentially conflicting packages.
We open source. Powered by GitHub