useEffect()

Σε γενικές γραμμές η useEffect χρησιμοποιείται όταν χρειάζεται να τρέξουν εντολές αφού πρώτα γίνει το αρχικό render (mount) ή κάποιο re-render.

Μπορούμε να έχουμε useEffect με εξαρτήσεις (dependencies) και χωρίς εξαρτήσεις.

Επίσης, προαιρετικά, μπορούμε να τρέξουμε εντολές και μετά το unmount.

Στην πρώτη ενότητα θα δούμε useEffect χωρίς εξαρτήσεις.

useEffect χωρίς εξαρτήσεις (dependencies)

Ας ξεκινήσουμε με το παρακάτω απλό παράδειγμα.

export default function App() {

  function handleVideoPlay() {
    let video = document.querySelector("video");
    video.play();
  }

  function handleVideoPause() {
    let video = document.querySelector("video");
    video.pause();
  }

  return (
    <>
      <video src = "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" loop playsInline />
      <br />
      <button onClick = {handleVideoPlay}>play</button>
      <button onClick = {handleVideoPause}>pause</button>
    </>
  );
}

Μια αλλαγή που πρέπει να κάνουμε είναι η χρήση μιας useRef() έτσι ώστε να μην χρησιμοποιούμε την document.querySelector() ή άλλες σχετικές για τη διαχείριση του DOM.

Καταλήγουμε στο παρακάτω.

import { useRef } from 'react';

export default function App() {
  const videoRef = useRef(null);

  function handleVideoPlay() {
    videoRef.current.play();
  }

  function handleVideoPause() {
    videoRef.current.pause();
  }

  return (
    <>
      <video ref = {videoRef} src = "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" loop playsInline />
      <br />
      <button onClick = {handleVideoPlay}>play</button>
      <button onClick = {handleVideoPause}>pause</button>
    </>
  );
}

Μπορούμε επίσης να χρησιμοποιήσουμε μια state variable όπως στο παράδειγμα που ακολουθεί. Με την αλλαγή στην τιμή της state variable προκαλείται re-render.

import { useRef, useState } from 'react';

export default function App() {
  const videoRef = useRef(null);
  const [isPlaying, setIsPlaying] = useState(false);

  function handleVideoStatus() {
    setIsPlaying(!isPlaying);
    if(isPlaying)
      videoRef.current.pause();
    else
      videoRef.current.play();
  }

  return (
    <>
      <video ref = {videoRef} src = "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" loop playsInline />
      <br />
      <button onClick = {handleVideoStatus}>{isPlaying ? 'Pause' : 'Play'}</button>
    </>
  );
}

Αν τώρα επιχειρήσουμε να εκτελέσουμε εντολές πριν το rendering θα έχουμε λάθος όπως παρακάτω. Δηλαδή αν επιχειρηθεί να επιλεγεί το video tag με την εντολή videoRef.current.pause(); πριν ολοκληρωθεί το mounting (δηλαδή η εγκατάσταση του DOM στον browser) θα προκύψει λάθος. Μπορείτε να το δείτε στο console log.

import { useRef, useState } from 'react';

export default function App() {
  const videoRef = useRef(null);
  const [isPlaying, setIsPlaying] = useState(false);

  if (isPlaying) {
    videoRef.current.play();
  } else {
    videoRef.current.pause();
  }

  function handleVideoStatus() {
    setIsPlaying(!isPlaying);
  }  

  return (
    <>
      <video ref = {videoRef} src = "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" loop playsInline />
      <br />
      <button onClick = {handleVideoStatus}>{isPlaying ? 'Pause' : 'Play'}</button>
    </>
  );
}

Για να διορθωθεί το λάθος θα χρησιμοποιήσουμε μια useEffect(). Με την useEffect() ο κώδικας που προκαλεί το πρόβλημα περιέχεται στην useEffect() και έτσι αποφεύγεται η πρόωρη εκτέλεση.

import { useRef, useState, useEffect } from 'react';

export default function App() {
  const videoRef = useRef(null);
  const [isPlaying, setIsPlaying] = useState(false);

  useEffect(() => {
    if (isPlaying) {
      videoRef.current.play();
    } else {
      videoRef.current.pause();
    }
  });

  function handleVideoStatus() {
    setIsPlaying(!isPlaying);
  }

  return (
    <>
      <video ref = {videoRef} src = "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" loop playsInline />
      <br />
      <button onClick = {handleVideoStatus}>{isPlaying ? 'Pause' : 'Play'}</button>
    </>
  );
}

useEffect με εξαρτήσεις (dependencies)

Η δεύτερη (προαιρετική) παράμετρος της useEffect είναι ένας πίνακας (array) στον οποίο περιέχονται οι state variables στις οποίες θα πρέπει να κληθεί η useEffect όταν αυτές αλλάξουν και προκληθεί ένα re-rendering.

Με τον τρόπο αυτό αποφεύγουμε εκτέλεση περιττών εντολών όταν κάποια μεταβλητή αλλάξει χωρίς να χρειάζεται να καλείται η useEffect.

Στην περίπτωση που ο πίνακας είναι κενός, τότε η useEffect καλείται μόνο μία φορά στο πρώτο rendering (ή mount).

Αυτό ισχύει στο παρακάτω παράδειγμα. Μπορείτε να το ελέγξετε με ένα console.log()

import { useRef, useState, useEffect } from 'react';

export default function App() {
  const videoRef = useRef(null);
  const [isPlaying, setIsPlaying] = useState(false);

  useEffect(() => {
    if (isPlaying) {
      videoRef.current.play();
    } else {
      videoRef.current.pause();
    }
  }, []);

  return (
    <>
      <video ref = {videoRef} src = "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" loop playsInline />
      <br />
      <button onClick = {() => setIsPlaying(!isPlaying)}>{isPlaying ? 'Pause' : 'Play'}</button>
    </>
  );
}

Αν θέλαμε να καλείται σε κάθε αλλαγή της isPlaying θα έπρεπε να έχουμε περάσει τη μεταβλητή αυτή στον πίνακα. Δηλαδή:

useEffect(() => {
	if (isPlaying) {
		videoRef.current.play();
	} else {
		videoRef.current.pause();
	}
}, [isPlaying]);

Στο συγκεκριμένο παράδειγμα βέβαια δεν χρειάζεται ο πίνακας γιατί θέλουμε πάντα να καλείται η useEffect() μετά από κάθε rendering και έτσι την κρατάμε όπως στην αρχή. Δηλαδή:

useEffect(() => {
	if (isPlaying) {
		videoRef.current.play();
	} else {
		videoRef.current.pause();
	}
});

Στην περίπτωση που έχουμε πολλές state variables αλλά θέλουμε να καλείται η useEffect() μόνο όταν αλλάζει κάποια ή κάποιες από αυτές, τότε αυτές τις περνάμε στον πίνακα ή στις εξαρτήσεις (dependencies).

import { useRef, useState, useEffect } from 'react';

export default function App() {
	const videoRef = useRef(null);
	const [isPlaying, setIsPlaying] = useState(false);
	const [text, setText] = useState('');

	useEffect(() => {
		if (isPlaying) {
			console.log('Calling video.play()');
			videoRef.current.play();
		} else {
			console.log('Calling video.pause()');
			videoRef.current.pause();
		}
	}, [isPlaying]);

	return (
		<>
		<input value={text} onChange={e => setText(e.target.value)} />
		<video ref = {videoRef} src = "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" loop playsInline />
		<br />
		<button onClick = {() => setIsPlaying(!isPlaying)}>{isPlaying ? 'Pause' : 'Play'}</button>
		</>
	);
}

Επειδή η useEffect τρέχει μετά από κάθε render, υπάρχει ο κίνδυνος να "πέσετε" σε ατέρμονα βρόχο. Δείτε το παρακάτω παράδειγμα.

const [count, setCount] = useState(0);
useEffect(() => {
	setCount(count + 1);
});

Cleanup function

Υπάρχουν περιπτώσεις όπου η useEffect() επιστρέφει μια συνάρτηση (Cleanup function) η οποία καλείται όταν το component κάνει unmount ή κάθε φορά που η useEffect() χρειάζεται να ξανα-κληθεί.

Στο παρακάτω παράδειγμα η useEffect επιστρέφει μια cleanup συνάρτηση την οποία το react κράταει στην μνήμη του και την καλεί όταν γίνει το unmount (δηλαδή αφαιρεθεί το DOM από τον browser).

import { useEffect } from 'react';

export default function App() {

	useEffect(() => {
		const connection = createConnection();
		connection.connect();

		return () => connection.disconnect();
	}, []);

	return <h1>Welcome to the chat!</h1>;
}

function createConnection() {
	return {
		connect() {
			console.log('✅ Connecting...');
		},
		disconnect() {
			console.log('❌ Disconnected.');
		}
	};
}