iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🦔

Seemingly Hacky Techniques Are Actually React-Aligned

に公開2件

Hello everyone. Are you using React? Many of you may feel that using React "correctly" is difficult.

The author is particularly strict about the correct usage of React and has published an article like this. If you are not an experienced React user, it's understandable that you find this article difficult.

https://zenn.dev/uhyo/articles/useeffect-taught-by-extremist

Regarding useEffect, the official React documentation also has a page titled 'You Might Not Need an Effect', which explains situations where useEffect tends to be used but is not appropriate.

https://ja.react.dev/learn/you-might-not-need-an-effect

Purpose of This Article

Regarding the technique of using key (which I will explain later in this article) among those explained in the official documentation above, I, in my less experienced past, did not think highly of it. Because it seemed like 'an un-React-like, quick-and-dirty trick'.

However, upon further reflection, I realized that this technique can also be understood in a way that fits React's ideal world (a world where declarative UI is thorough and procedural thinking is eliminated). Therefore, I now consider it not a gimmick but a valid technique that aligns with React's philosophy.

In this article, I will share my thoughts on this technique. I hope this helps you use such techniques with more confidence and avoid inappropriate use of useEffect.

Technique to Avoid useEffect by Using key

This article focuses on the following technique introduced in the official documentation:

This is a technique to reset state inside a component using key.

Here, we consider a common example: a user profile page and a profile editing form called from it. The profile editing form receives a user object as props from the parent component and sets initial values for the form based on that information.

const UserProfileForm: React.FC<{ user: User }> = ({ user }) => {
  const [name, setName] = useState(user.name);
  const [email, setEmail] = useState(user.email);

  return (
    <form>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
    </form>
  );
};

In the above implementation, even if the parent re-renders and passes a different user, the state (name and email) does not return to initial values. Since React preserves state across re-renders, this is normal behavior.

However, in such a component, there is often a requirement to reset the form contents with the new user information when the user passed from the parent changes. To meet this requirement, one might consider implementing it with useEffect as follows:

// ✖ Bad usage of useEffect
useEffect(() => {
  setName(user.name);
  setEmail(user.email);
}, [user]);

This is not a desirable use of useEffect. Because it uses useEffect as a way to 'detect that user has changed', rather than using it for its original purpose: 'expressing the side effects of a component being present'.

To solve this problem, the official documentation introduces a technique using key. On the side that uses UserProfileForm, set key like this:

<UserProfileForm key={user.id} user={user} />

This way, every time user changes, the UserProfileForm component is recreated, and as a result, its internal state is also reset. This achieves a component that satisfies the requirement.

Interpretation of the key Technique

In the past, I felt that this key technique was questionable. Because 'resetting component state at a specific timing' seemed like a procedural way of thinking, which goes against the declarative UI philosophy. Moreover, using key to reset state looks like hacking React's behavior, and even with key specified, the intention to 'reset state' is not clearly readable from the code.

However, recently when I read the official React documentation again, I found that it clearly states that the key technique can also be considered within the framework of declarative UI. Actually, by interpreting the behavior of key not as 'resetting state' but as 'indicating that a component is a different instance'—as an annotation to JSX—it becomes possible to interpret it in a way consistent with React's philosophy.

When we look at the re-rendering behavior when user changes, it would be as follows:

<UserProfileForm key="alice" user={user} />

↓render↓

<UserProfileForm key="bob" user={user} />

At this point, we should not see it as 'the key of a single UserProfileForm instance changed'. Instead, think of it as: 'The UserProfileForm instance with key alice disappears, and a new UserProfileForm instance with key bob appears'.

By default, React infers that if <UserProfileForm /> is rendered before and after, it is the same instance with changed props. By explicitly providing key, we tell React that it is a different instance.

// Interpretation without key: same instance with changed props
<UserProfileForm user={aliceUser} /> → <UserProfileForm user={bobUser} />

// Interpretation with key: simultaneous destruction and creation of instances
<UserProfileForm key="alice" user={aliceUser} /> → (alice instance removed)
(nothing) → <UserProfileForm key="bob" user={bobUser} /> (bob instance created)

With this way of thinking, the behavior where state appears to be reset can be understood as follows:

  • When user changes, a new instance of UserProfileForm is created, and the screen display is replaced by it.
  • The new instance starts from an initial state, so it looks like the state was reset.

This use of key is an expression to make React understand that when the user changes, a different UserProfileForm should be displayed—in other words, 'render a UserProfileForm for each user'.

This usage of key aligns with React's fundamental approach: 'compute UI from state via pure functions and efficiently update the screen through reconciliation'. The return value of a pure function is a component tree expressed in JSX syntax, and key is information appended to the component tree to adjust the interpretation of that tree.

Thinking this way, we see that key is not encroaching on the essence of declarative UI, but is used for technical reasons. In JSX (the component tree it represents), when the same component is rendered with different props, it cannot by default distinguish whether it is 'the same instance with changed props' or 'replaced by a different instance'; that is left to the runtime (React) inference. key is additional information to correct that inference, and necessary due to the technical constraints of JSX and React's architecture to express the information of 'replacement'.

Understanding with Hypothetical Code

As explained so far, this code tells React that when key changes, the UserProfileForm instance changes.

<UserProfileForm key={user.id} user={user} />

If this doesn't click for you, you might understand it by thinking in terms of hypothetical code like this. In other words, the intention is to prepare a different UserProfileForm for different users.

{user.id === "alice" && <UserProfileForm user={user} />}
{user.id === "bob" && <UserProfileForm user={user} />}
…(and so on for every user ID)…

The idea behind the first line of code is that, in intent, we want to show a dedicated UserProfileForm for each user[1]. However, it is actually impossible to hardcode UserProfileForm for every user ID. Therefore, in the source code, we write only one UserProfileForm and use key to communicate the difference in instances.

When key is a variable, you can think of it as wanting to tell React that "we are preparing a separate instance for every different value".

Relationship with Array Keys

key is also used when rendering lists in React. For example, code like this:

{users.map((user) => (
  <UserProfileForm key={user.id} user={user} />
))}

Many React users probably think of this usage when they hear key. Because React gives a warning if key is not specified, it is recognized as mandatory.

This use of key in arrays and the use of key for resetting state when props change, as explained earlier, are not separate. Rather, they are used for the same purpose and based on the same concept.

key is for enabling consistent identification of instances across multiple renders in a component tree expressed in JSX. With key, you can identify instances more accurately without relying on the React runtime's inference.

The reason specifying key is treated as mandatory in arrays is because when elements in an array may be reordered or removed, React has difficulty identifying instances accurately. Therefore, in this case, additional user-provided information is required.

In cases other than arrays, it is the same that key can correct instance identification. When you want to reset state, using key allows you to tell React that it is a different instance.

Conclusion

Specifying key in React is for providing information to consistently identify component instances. This allows React to manage the component lifecycle as intended by the programmer. The technique of using key to reset state when props change is for telling React that "the previous instance disappears and a new instance appears".

Although this mechanism is based on technical necessities, it reinforces React's core declarative UI framework. Therefore, it should be accepted as a technique consistent with the declarative UI way of thinking, not as a hack.

By accurately expressing the program's intent using key, the React runtime can correctly understand that intent, and as a result, the desired behavior is achieved. This way of "translating intent into code" is an important concept in software development in general, not just in React. With that in mind, the technique of using key can be considered one of the important skills for mastering React.

脚注
  1. As a side note, if you actually write it this way and user.id changes from alice to bob, the state of UserProfileForm will be reset. This is because they are in different positions in JSX, so the two UserProfileForm components are treated as different instances. ↩︎

GitHubで編集を提案

Discussion

Hiroki HoriuchiHiroki Horiuchi

ある程度は理解できるのですが、自分としてはコンポーネントの内部の都合が使う側に漏れているように感じてしまいます。
それと、配列の場合は key を使うことを lint が担保してくれますが、この場合使う側で必要かどうかを判断が必要なのが問題かと思います。せっかく typescript で props の型を定義しているのに、そこに含まれない key (省略可能)を使わざるえないのが。。。

11
uhyouhyo

コメントありがとうございます。その気持ち悪さは理解できます。今回の場合、最終的に UserProfileForm の仕様が「後から user propを変更しても何も起こらない」という仕様になったから、使う側で key の考慮が必要になったと理解することができると思います。 useEffect の良くない使い方を直す過程でコンポーネントの仕様変更が起こったということです。

コンポーネントの仕様が分かりにくかったり、直観的でなかったりすると、「コンポーネントの内部の都合が使う側に漏れている」と感じられることになります。今回はこのケースであり、ここはコンポーネントのインターフェースの改善の余地がありそうだと思います。

例えば、inputのdefaultValueに倣って、userではなくdefaultUserというprop名にするとかはありかもしれません。この辺りは、次回以降の記事でまた取り上げたいと思っています。

8