Hristo Enev
Hristo Enev

Reputation: 2541

useEffect hook misbehaves with setTimeout and state

I created a custom toast component in my exercise React application. It is working correctly until the moment I try to introduce an auto dismiss timeout functionality. Basically when you load a new toast it needs to dismiss itself after let say 5000ms.

If you want check the full code in my Github Repo that also have a live preview.

Easiest way to create toast is put invalid mail / password.

I believe I am doing something wrong with the useEffect hook or I am missing something. The problem is that when I am creating multiple toasts they disappear all at the same time. Also React is complaining that I didn't include remove as a dependency of the useEffect hook but when I do it becomes even worse. Can someone demystify why this is happening and how it can be fixed. I am a bit new to React.

Here is the file that creates a HOC around my main App component:

import React, { useState } from 'react';
import { createPortal } from 'react-dom';

import ToastContext from './context';
import Toast from './Toast';
import styles from './styles.module.css';

function generateUEID() {
  let first = (Math.random() * 46656) | 0;
  let second = (Math.random() * 46656) | 0;
  first = ('000' + first.toString(36)).slice(-3);
  second = ('000' + second.toString(36)).slice(-3);

  return first + second;
}

function withToastProvider(Component) {
  function WithToastProvider(props) {
    const [toasts, setToasts] = useState([]);
    const add = (content, type = 'success') => {
      const id = generateUEID();

      if (toasts.length > 4) {
        toasts.shift();
      }

      setToasts([...toasts, { id, content, type }]);
    };
    const remove = id => {
      setToasts(toasts.filter(t => t.id !== id));
    };

    return (
      <ToastContext.Provider value={{ add, remove, toasts }}>
        <Component {...props} />

        { createPortal(
            <div className={styles.toastsContainer}>
              { toasts.map(t => (
                  <Toast key={t.id} remove={() => remove(t.id)} type={t.type}>
                    {t.content}
                  </Toast>
              )) }
            </div>,
            document.body
        ) }
      </ToastContext.Provider>
    );
  }

  return WithToastProvider;
}

export default withToastProvider;

And the Toast component:

import React, { useEffect } from 'react';
import styles from './styles.module.css';

function Toast({ children, remove, type }) {
  useEffect(() => {
    const duration = 5000;
    const id = setTimeout(() => remove(), duration);
    console.log(id);

    return () => clearTimeout(id);
  }, []);

  return (
    <div onClick={remove} className={styles[`${type}Toast`]}>
      <div className={styles.text}>
        <strong className={styles[type]}>{type === 'error' ? '[Error] ' : '[Success] '}</strong>
        { children }
      </div>
      <div>
        <button className={styles.closeButton}>x</button>
      </div>
    </div>
  );
}

export default Toast;

Upvotes: 1

Views: 3513

Answers (1)

Hristo Enev
Hristo Enev

Reputation: 2541

Searching today for the solution I found it here

You will need to use useRef and its current property

Here is how I transformed the Toast component to work:

import React, { useEffect, useRef } from 'react';
import styles from './styles.module.css';

function Toast({ children, remove, type }) {
  const animationProps = useSpring({opacity: .9, from: {opacity: 0}});
  const removeRef = useRef(remove);
  removeRef.current = remove;

  useEffect(() => {
    const duration = 5000;
    const id = setTimeout(() => removeRef.current(), duration);

    return () => clearTimeout(id);
  }, []);

  return (
    <div onClick={remove} className={styles[`${type}Toast`]}>
      <div className={styles.text}>
        <strong className={styles[type]}>{type === 'error' ? '[Error] ' : '[Success] '}</strong>
        { children }
      </div>
      <div>
        <button className={styles.closeButton}>x</button>
      </div>
    </div>
  );
}

export default Toast;

Upvotes: 2

Related Questions