19. React Effect

김관주·2023년 12월 16일
0

웹시스템

목록 보기
19/22

Synchronizing with Effect

Rendering and Event Handling

  • So far, logics inside React components we have studied are rendering code and event handlers.
    • Rendering code is about calculating the result on the screen (taking props and state, transforming them, and returning JSX). Like a math formula, it should only calculate the results, but bot do anything else
    • Event handler are nested functions inside the components that do things rather than just calculate them (updating an input field, submitting an HTTP POST request to buy a product, or navigate the user to another screen).
      Event handlers contain “side effects” (they change the program’s state) caused by a specific user action (for example, a button click or typing).

Effects are different from events

  • We may need something else such as connecting to the chat server when the ChatRoom component is visible on the screen.
    • It is a side effect (not a pure calculation); thus, it can’t happen during rendering.
    • However, there is no single particular event like a click that causes ChatRoom to be displayed.
  • Effects let you specify side effects that are caused by rendering itself, rather than by a particular event.
    • Sending a message (an event). However, setting up a server connection is an Effect because it should happen no matter which interaction caused the component to appear.
      그러나 서버 연결을 설정하는 것은 effect입니다. effect는 컴포넌트가 나타나는 모든 상호작용에 관계없이 발생해야 하는 동작하기 때문이다.

      따라서 컴포넌트가 렌더링되거나 업데이트될 때마다 서버 연결을 설정하는 effect가 실행되어야 합니다

    • Effects run at the end of a commit after the screen updates. This is a good time to synchronize the React components with some external system (like network or a third-party library)

How to write an Effect

  1. Declare an Effect. By default, your Effect will run after every render.
  2. Specify the Effect dependencies. Most Effects should only re-run when needed rather than after every render. For example, a fade-in animation should only trigger when a component appears. Connecting and disconnecting to a chat room should only happen when the component appears and disappears, or when the chat room changes. You will learn how to control this by specifying dependencies.
  3. Add cleanup if needed. Some Effects need to specify how to stop, undo, or clean up whatever they were doing. For example, “connect” needs “disconnect”, “subscribe” needs “unsubscribe”, and “fetch” needs either “cancel” or “ignore”. You will learn how to do this by returning a cleanup function

Declare an Effect

  • Declare an Effect: the Effect will run after every render
import { useEffect } from 'react';
  • Call the useEffect Hook at the top level of your component and put codes in
function MyComponent() {
    useEffect(() => {
// Code here will run after *every* render
    });
    return <div/>;
}
  • Every time your component renders, React will update the screen and then run the code inside useEffect. In other words, useEffect “delays” a piece of code from running until that render is reflected on the screen.
    즉, effect를 사용하여 실행 중인 코드 조각을 해당 렌더가 화면에 반영될 때까지 "지연"합니다.

Effect : Video Example

import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
    const ref = useRef(null);
    if (isPlaying) {
        ref.current.play(); // Calling these while rendering isn't allowed.
    } else {
        ref.current.pause(); // Also, this crashes.
    }
    return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
    const [isPlaying, setIsPlaying] = useState(false);
    return (
        <>
            <button onClick={() => setIsPlaying(!isPlaying)}>
                {isPlaying ? 'Pause' : 'Play'}
            </button>
            <VideoPlayer
                isPlaying={isPlaying}
                src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
            />
        </>
    );
}

It is not allowed to do something with the DOM node during rendering

  • In React, rendering should be a pure calculation of JSX and should not contain side effects like modifying the DOM.
  • Moreover, when VideoPlayer is called for the first time, its DOM does not exist yet! There isn’t a DOM node yet to call play() or pause() on, because React doesn’t know what DOM to create until you return the JSX.
  • Solution: wrap the side effect with useEffect to move it out of the rendering calculation:

ref는 React에서 DOM 요소에 접근하기 위해 사용되는 기능입니다. useRef 훅을 사용하여 ref 변수를 생성하고, ref를 비디오 요소에 연결하기 위해 <video> 요소의 ref prop으로 전달하고 있습니다.

컴포넌트가 렌더링될 때, if문 내부에서 isPlaying의 상태에 따라 ref.current.play() 또는 ref.current.pause()가 호출되는데, 이 부분이 문제를 일으키는 원인입니다.

React에서는 컴포넌트의 렌더링 중에 DOM 요소를 조작하는 것을 허용하지 않습니다. 따라서 ref.current.play()ref.current.pause()를 렌더링 도중에 호출하는 것은 허용되지 않습니다.

이러한 동작을 구현하기 위해 React에서는 useEffect 훅을 사용할 수 있습니다. useEffect 훅은 컴포넌트가 렌더링된 후에 특정 동작을 수행하도록 할 수 있는 기능입니다.

Effect : Video Example

import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
    const ref = useRef(null);
    useEffect(() => {
        if (isPlaying) {
            ref.current.play();
        } else {
            ref.current.pause();
        }
    });
    return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
    const [isPlaying, setIsPlaying] = useState(false);
    return (
        <>
            <button onClick={() => setIsPlaying(!isPlaying)}>
                {isPlaying ? 'Pause' : 'Play'}
            </button>
            <VideoPlayer isPlaying={isPlaying}
                         src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
            />
        </>
    );
}

Solution: Update the screen first, then Effect runs.

  • By wrapping the DOM update in an Effect, you let React update the screen first. Then your Effect runs.
  • When your VideoPlayer component renders (either the first time or if it rerenders), a few things will happen. First, React will update the screen, ensuring the <video> tag is in the DOM with the right props. Then React will run your Effect. Finally, your Effect will call play() or pause() depending on the value of isPlaying.
  • Press Play/Pause multiple times and see how the video player stays synchronized to the isPlaying value:

Possible problem: infinite loop

  • By default, Effects run after every render. This is why code like this will produce an infinite loop:
const [count, setCount] = useState(0);
useEffect(() => {
    setCount(count + 1);
});
  • Effects run as a result of rendering. Setting state triggers rendering.
    • The Effect runs, it sets the state, which causes a re-render, which causes the Effect to run, it sets the state again, this causes another re-render, and so on.
  • Effects should usually synchronize your components with an external system. If there’s no external system and you only want to adjust some state based on other state, you might not need an Effect.

Specify the Effect dependencies

  • Effects run after every render. Often, this is not what you want:
    • Ex) you don’t want to reconnect to the chat server on every keystroke
  • You can tell React to skip unnecessarily rerunning the Effect by specifying an array of dependencies as the second argument to the useEffect call.
  • The problem is that the code inside of your Effect depends on the isPlaying prop to decide what to do, but this dependency was not explicitly declared. To fix this issue, add isPlaying to the dependency array:
useEffect(() => {
    if (isPlaying) {
        console.log('Calling video.play()');
        ref.current.play();
    } else {
        console.log('Calling video.pause()');
        ref.current.pause();
    }
}, []); // This causes an error
useEffect(() => {
    if (isPlaying) {
// codes
    }, [isPlaying]); // must be declared here

Effect : Video Example with Dependency Declared

import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
    const ref = useRef(null);
    useEffect(() => {
        if (isPlaying) { console.log('Calling video.play()');
            ref.current.play();
        } else { console.log('Calling video.pause()');
            ref.current.pause();
        }
    }, [isPlaying]);
    return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
    const [isPlaying, setIsPlaying] = useState(false);
    const [text, setText] = useState('');
    return ( <>
            <input value={text} onChange={e => setText(e.target.value)} />
            <button onClick={() => setIsPlaying(!isPlaying)}>
                {isPlaying ? 'Pause' : 'Play'}
            </button>
            <VideoPlayer isPlaying={isPlaying}
                         src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
            />
        </>
    );}

Effect and Dependency

  • Notice that you can’t “choose” your dependencies. You will get a lint error if the dependencies you specified don’t match what React expects based on the code inside your Effect.
  • The behaviors without the dependency array and with an empty [] dependency array are different:
useEffect(() => {
// This runs after every render
});
useEffect(() => {
// This runs only on mount (when the component appears)
}, []);
useEffect(() => {
// This runs on mount *and also* if either a or b have changed since the last render
}, [a, b]);

Add cleanup if needed (with ChatRoom example)

  • A ChatRoom component that needs to connect to the chat server when it appears
  • How do you keep the component connected while it is displayed to the user?
useEffect(() => {
    const connection = createConnection();
    connection.connect();
}, []);
  • The code inside the Effect does not use any props or state, so your dependency array is [] (empty). This tells React to only run this code when the component “mounts”, i.e. appears on the screen for the first time.
  • In Strict Mode, React mounts components twice (in development only!) to stress-test your Effects.
  • If your Effect breaks because of remounting, you need to implement a cleanup function.

ChatRoom Example

  • The component mounts and calls connection.connect() <= mount and re-mount issue.
    • Whenever remounted, it connects again.
  • More serious issue is the fact that the connection is still alive (i.e., doesn’t close) when the component unmounts.
import { useEffect } from 'react';
import { createConnection } from './chat.js';
export default function ChatRoom() {
    useEffect(() => {
        const connection = createConnection();
        connection.connect();
    }, []);
    return <h1>Welcome to the chat!</h1>;
}
export function createConnection() {
// A real implementation would actually
    connect to the server
    return {
        connect() {
            console.log('✅ Connecting...');
        },
        disconnect() {
            console.log('❌ Disconnected.');
        }
    };
}

ChatRoom Example with Cleanup functions

  • The return a cleanup function from your Effect fix the issue: return () => { connection.disconnect() };
  • React will call your cleanup function each time before the Effect runs again, and one final time when the component unmounts (gets removed).
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
export default function ChatRoom() {
    useEffect(() => {
        const connection = createConnection();
        connection.connect();
        return () => connection.disconnect();
    }, []);
    return <h1>Welcome to the chat!</h1>;
}
export function createConnection() {
// A real implementation would actually connect to the server
    return {
        connect() {
            console.log('✅ Connecting...');
        },
        disconnect() {
            console.log('❌ Disconnected.');
        }
    };
}

Fetching data

  • If your Effect fetches something, the cleanup function should either abort the fetch or ignore its result.
  • You can’t “undo” a network request that already happened, but your cleanup function should ensure that the fetch that’s not relevant anymore does not keep affecting your application.
useEffect(() => {
    let ignore = false;
    async function startFetching() {
        const json = await fetchTodos(userId);
        if (!ignore) {
            setTodos(json);
        }
    }
    startFetching();
    return () => {
        ignore = true;
    };
}, [userId]);

0개의 댓글