Skip to main content

Why Did React Introduce Server Components?

· 11 min read
Hyunmo Ahn
Front End Engineer @ Line+

Purpose

React 18 was released in 2022, and the Next.js App Router was released in 2023, making this a somewhat outdated investigation. However, while using the Next.js page router and app router, I found an article explaining the differences and reasons for the changes, particularly why the app router was introduced. This document summarizes my understanding of that article.

Story

Before reading the article, consider the following scenarios:

Imagine you're at a job interview for a Frontend Developer position and are asked the following questions. Or perhaps you're in a meeting trying to convince your team to use the Next.js app router for a new or existing project. Or maybe you're contemplating which technology is more suitable between the Next.js page router and app router.

While simply stating that the app router is a new technology and advocating for its use as a Frontend Developer can be persuasive, understanding the answers to the questions below can provide a more compelling argument.

Question
  • Q. What is the difference between SSR (Server Side Rendering) and CSR (Client Side Rendering)?
  • Q. What limitations in the existing method (Next.js page router) led to the development of the Next.js app router?
  • Q. Why is Suspense used in React?

If you know the answers to these questions, you might not need to read the full text. However, if you want a more reliable source, you can refer to the React 18 Architecture introduction. If you only want the answers, you can skip to the Result.

The following text is a summary of my understanding, mixed with my opinions, after reading the Architecture article.

Exceeding Local Storage Capacity

· 3 min read
Hyunmo Ahn
Front End Engineer @ Line+

Introduction

Have you ever considered the capacity when using LocalStorage in your project?

Depending on the purpose of localStorage, it might not be something you often think about. However, if you're storing data that accumulates over time, the data can exceed the localStorage capacity.

What happens when it exceeds? 초과하면 과연 어떻게 동작할까요?

The answer is It throws an error.

Figure 1Local Storage Exceed Error

Solution

When the error occurs, the setStorage statement terminates due to the throw, and the error propagation begins.

If your project handles error propagation well, the error message will be logged, and if not, the project might stop.

To prevent this situation, you should use a try-catch block to catch the error and add logic to handle it.

const setStorage = (key, value) => {
try {
window.localStorage.setItem(key, value);
} catch (e) {
// Stop Throw Error
console.error('Local Storage Exceed Error', e);
}
}

Of course, an error indicates that the localStorage capacity has been exceeded, so the user will continue to encounter Local Storage Exceed Errors and won't be able to save data.

Therefore, in addition to error handling, you should also include logic to clear the data.

const setStorage = (key, value) => {
try {
window.localStorage.setItem(key, value);
} catch (e) {
// Stop Throw Error
console.error('Local Storage Exceed Error', e);
window.localStorage.clear();
}
}

Event Propagation in React

· 8 min read
Hyunmo Ahn
Front End Engineer @ Line+

Introduction

When mixing React's event handler method with vanilla Javascript's event handler method, event propagation may not work as intended. For example, in a structure like button1, button2, clicking button2 can trigger the event handler of button1 as well.

const buttonEl = document.getElementById('button1');
buttonEl.addEventListener('click', () => {
console.log('button1 clicked');
});

const handleClick = (e) => {
e.stopPropagation();
console.log('button2 clicked');
};

return (
<button id='button1'>
<button id='button2' onClick={handleClick}>Click me</button>
</button>
);
// When button2 is clicked
button2 clicked
button1 clicked

Solution

Event propagation in React differs from that in vanilla Javascript because React handles event propagation using a delegation method. (comment)

In React (post React 17), event listeners are registered on the rootDOM. Therefore, event listeners within React propagate as expected according to the DOM structure, but vanilla Javascript event propagation may not behave as anticipated.

While it's best to avoid mixing the two types of events, sometimes it's unavoidable when using third-party libraries or handling parts of the code you can't control. In such cases, you must block event propagation according to the behavior of each method.

// Unify event listener to vanilla Javascript
const buttonEl = document.getElementById('button1');
buttonEl.addEventListener('click', () => {
console.log('button1 clicked');
});

const button2El = document.getElementById('button2');
button2El.addEventListener('click', (e) => {
e.stopPropagation();
console.log('button2 clicked');
});

return (
<button id='button1'>
<button id='button2'>Click me</button>
</button>
);

// Or
// Unify event listener to React
const handleClick1 = (e) => {
console.log('button1 clicked');
};

const handleClick = (e) => {
e.stopPropagation();
console.log('button2 clicked');
};

return (
<button id='button1' onClick={handleClick1}>
<button id='button2' onClick={handleClick}>Click me</button>
</button>
);

Server Actions in Next.js are executed sequentially

· 5 min read
Hyunmo Ahn
Front End Engineer @ Line+

Introduction

In Next.js, server actions are used as a way to call functions via the server, even in a client environment. Server actions are executed sequentially, meaning that even if multiple functions are called, they are executed one at a time, so caution is needed when using them.

Behavior

Let's look at a brief example. There are two functions in this example.

'use server';
import { format } from 'date-fns';

export const callServerFn = async (count)=> {
return new Promise((resolve) => setTimeout(() => {
const time = format(new Date(), 'HH:mm:SS.sss');
console.log(`Server Call #${count}: `, time);
resolve(count)
}, 1000));
}

The callServerFn function takes 1 second to respond and is used as a server action with 'use server'. This logic is executed on the Next server. Logs are used to check the server response timing.

// client.tsx
'use client';

export const CallButton = () => {
const handleClick = async () => {
const result = await Promise.all([
callServer(1),
callServer(2),
callServer(3),
])
}

return (<button onClick={handleClick}>Hello World</button>);
}

The CallButton component calls the callServer function three times when the button is clicked and returns the result when all functions are completed.

Server Call #1:  01:23:22.010
Server Call #2: 01:23:24.011
Server Call #3: 01:23:25.012

When the button is pressed, three calls are made simultaneously, but the log results show that they are called sequentially, once every second.

How to implement line-spacing in CSS

· 2 min read
Hyunmo Ahn
Front End Engineer @ Line+

Introduction

In CSS, line-height is used to adjust line spacing. However, in a project I worked on, there was a requirement to support line-spacing. This article explains how to implement line-spacing using CSS.

Line Spacing

Line Gap

Line Gap

Line Gap

Line Spacing (5px)

Line Height

Line Gap

Line Gap

Line Gap

Line Height (fontSize + 5px)

Cheat Sheet

Unlike line-height, which applies padding both above and below the text, line-spacing applies padding only between lines. The difference here is that line-spacing does not add padding at the beginning and end of the text block.

You can implement line-spacing using margins.

const lineHeight = 20; // origin line-height
const lineSpacing = 10;
const margin = - (lineSpacing / 2);

const LineSpacingText = () => {
return (
<p style={{
marginTop: `${margin}px`,
marginBottom: `${margin}px`,
lineHeight: `${lineHeight + lineSpacing}px`,
}}>
Hello, World!
</p>
);
};

Playground

Line Spacing 0px

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet.

Line Spacing (0px)

Line Height (0px + fontSize)

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet.

Line Height (0px + fontSize)

Reasons to Avoid Nested Components

· 4 min read
Hyunmo Ahn
Front End Engineer @ Line+

Introduction

During a project, I encountered an issue where unnecessary re-renders were occurring.

The problem was not just rendering, but the fact that the DOM was being completely redrawn with each render. The culprit was the use of nested components in the code.

In this article, I will explain why you should avoid using nested components.

Cheat Sheet

// Bad
const List = ({ hasWrapper, borderStyle, children }) => {
const Border = () => {
return <p style={borderStyle}>{children}</p>;
}

if (hasWrapper) {
return (
<Wrapper>
<Border />
</Wrapper>
);
}

return <Border/>;
};
// Good
const List = ({ hasWrapper, borderStyle, children }) => {
if (hasWrapper) {
return (
<Wrapper>
<Border borderStyle={borderStyle}>
{children}
</Border>
</Wrapper>
);
}

return (
<Border borderStyle={borderStyle}>
{children}
</Border>
);
};

const Border = ({ borderStyle, children }) => {
return <p style={borderStyle}>{children}</p>;
}

As shown above, you might use a nested component to reuse the code for Border and the props of List. However, in such cases, the children are unnecessarily repaint in the DOM with each render.

How to Avoid Re-rendering Caused by Callback Function Props

· 7 min read
Hyunmo Ahn
Front End Engineer @ Line+

In React, when passing a function as component props, one needs to be careful with managing references to avoid unnecessary re-renders. Although most case we use useCallback to prevent unnecessary re-renders, I found an alternative solution sharing.

The code that inspired this article is radix-ui/primitives's useCallbackRef. This article will discuss the use cases and logic of useCallbackRef.

Below is the useCallbackRef code we are going to discuss. Can you guess in what situations it might be used?

// useCallbackRef.js
import { useRef, useEffect, useMemo } from 'react';

export function useCallbackRef(callback) {
const callbackRef = useRef(callback);

useEffect(() => {
callbackRef.current = callback;
});

return useMemo(() => ((...args) => callbackRef.current?.(...args)), []);
}

i18n and typescript

· 29 min read
Hyunmo Ahn
Front End Engineer @ Line+

Intro

I am using typescript and i18next for our project, I would like to summarize the experience of strongly applying the type check of the i18n JSON file.

About I18n

First, the following is how i18n was being used.

i18n is used to display text in multiple languages on the same webpage, meaning to display it in a language with one key, multiple values.

const i18nJSON = {
'simple-example': 'This is example text',
'values-example': 'I need to show {0} value',
'line-break-example': 'Hello. \n I am FE developer'
} as const

First, there is an object in the form of Key-Value as above. The object may be a JSON or an object of a typescript.

import tFunction from 'utils';

tFunction('simple-example') // This is example text
tFunction('values-example', [15]) // I need to show 15 value
tFunction('line-break-example') // Hello <br /> I am FE developer

Second, tFunction is used to insert the i18n key to obtain a string that fits the key. In some cases, the string including the value that fits each text may be returned by inserting a variable such as values-example.

In the last line-break-example, the \n line break character is converted to the <br/> tag so that it can be line break on React.

caution

This article uses a function called tFunction as a method for applying i18n. Since it mainly deals with content at the type level, not JS logic, it will not deal with what actually happens to internal logic. It can be said that it plays the same role as a function such as i18next.t.

How about I18n return type

Here, it may be seen what value the returned value in tFunction is. Then, what is the type of each return value?

import tFunction from 'utils';

tFunction('simple-example')
// This is example text
// string
tFunction('values-example', [15])
// I need to show 15 value
// string
tFunction('line-break-example')
// Hello <br /> I am FE developer
// ReactElement

It may be considered that L3 and L6 are string types. However, in L9, ReactElement is returned because there is \n in the i18n text.

note

To support the line break by returning the string without returning to ReactElement, dangerouslySetInnerHTML shall be used.

However, since there are many limitations, return to ReactElement instead of string.

If the variable values-example contains JSX such as atag instead of 15, the return type of values-example is also different.

import tFunction from 'utils';

tFunction('values-example', [15])
// I need to show 15 value
// string

tFunction('values-example', [<a href="/about">more</a>])
// I need to show <a href="/about">more</a> value
// ReactElement

Even if the values-example I18n key is used as above, the type must vary depending on what value comes to values. This is because a tag must also be returned to a component that is a ReactElement, not a string, as in <br/>.

What is matter?

It was found that the i18n Text type varies as string and ReactElement depending on the case. So what is the problem?

tFunction is that it does not intelligently infer types as mentioned above.

What is the problem if the type is not properly inferred here?

<input
type='input'
placeholder={tFunction('line-break-example')} // type error
/>

In many cases, typically when using HTML tags, attributes defined as strings may contain i18n values rather than strings. In this case, [object Object] is displayed in the placeholder instead of the xlt text.

Of course, it's obviously strange to have an a tag in the placeholder or a line change like \n. However, there may be cases where the i18n key is incorrectly used, or there may be cases where the i18n text is incorrectly registered.

In other words, there are the above problems in that the type is not properly inferred when using typescript, and it can cause another problem.

In this article, we will talk about how to define the type by format of i18n text. This article is basically related to typescript Template Literal Types, although i18n is an example.

PRE-REQUIRED
  • General knowledge of typescript 4.1+
  • Interest in Template Literal Types
  • The experience of i18n system(optional)

How to use OAS generator in Front-end environment?

· 20 min read
Hyunmo Ahn
Front End Engineer @ Line+

I am using OAS-generator in project recently. When deciding to use it, there were many things that required consideration and confirmation. And, I'm going to write about my experience using OAS-generator because it seems to have a lot of good things after using.

Perhaps those who are curious about what OAS-generator is, those who know but are worried about using it, and those who are already using it but are hesitant to use it well, I hope that you will learn good motifs and experiences by reading this article.

What this article says is as follows.

  • What is the OAS-generator.
  • The pros and cons of using OAS-generator.
  • How to use OAS-generator.
    • Configuration
    • Custom Templates
  • Optimization
Pre-required
  • The experience to develop front-end using Rest API
  • Can read the mustache Grammar(Optional)
    • Even if you don't know, there's no problem reading this article. But, you want to use OAS-generator, you should know it.

Deep dive to immer

· 27 min read
Hyunmo Ahn
Front End Engineer @ Line+

This article basically takes time to learn about immer immer. If you don't know immer, I recommend reading next chapter first.

What is my curious?

Question

Q1. How does immer change the mutable update way to immutable update way?

immer functions to return data immutably even when using the object built-in method that changes to be mutable. Let's find out how this function works internally.

This example is following basic example of immer official docs

import produce from 'immer';

const baseState = [
{
title: "Learn TypeScript",
done: true,
},
{
title: "Try Immer",
done: false,
},
]

const nextState = produce(baseState, (draft) => {
draft.push({ title: "Tweet about It" });
draft[1].done = true;
})

console.log(baseState === nextState) // false
console.log(nextState)
/*
[
{
title: "Learn TypeScript",
done: true,
},
{
title: "Learn TypeScript",
done: true,
},
{
title: "Tweet about It",
},
]
*/
Question

Q2. How does immer use structural sharing?

*structural sharing: When coping an object, the same reference is used for an object that has not been changed.

To update object immutably means that original object is copied to new object. In other word, copy needs to cost. When immer copy object, the unchanged reference copies the object using the structural sharing method that is reused. Let's find out what kind of structural sharing is used in immer.

Question

Q3. immer sometimes updates data through return rather than mutable updating the draft within produce function, in which case the logic is different?

When using an immer, there is a case of returning a new object instead of the mutable update method suggested above. This is same as the method of returning objects from javascript immutably regardless of the immer. immer officially is guiding this way and There will be many developers who use both methods, the method of changing objects to be mutable and method of changing objects to be immutable. Let's see what logic differences these differences cause in the immer.

// mutable method
const nextState = produce(baseState, (draft) => {
draft.push({ title: "Tweet about It" });
draft[1].done = true;
})

// immutable method
const nextState = produce(baseState, (draft) => {
return {
...baseState,
{ ...baseState[1], done: true },
{ title: "Tweet about It" },
}
})
PREREQUISITES
  • Experience using an immer or redux-toolkit
  • Understanding of Proxy (optional)