javascript - Cum să re-renderizezi imediat componenta copil după actualizarea sessionStorage folosind un hook personalizat


Scopul meu este să construiesc un sistem simplu de recenzii ale produselor folosind React, Next.JS și sessionStorage-ul browser-ului. Utilizatorul ar trebui să poată face clic pe un buton pentru a „Adăuga o recenzie”. Această acțiune va declanșa afișarea unui câmp de text și a unui buton de trimitere. Odată ce utilizatorul apasă butonul de trimitere, conținutul recenziei ar trebui să fie salvat în sessionStorage și să apară imediat într-o listă de recenzii.

Problema mea este că, deși pot actualiza sessionStorage după trimiterea recenziei, aplicația nu afișează imediat lista recenziilor existente. Dacă părăsesc pagina și revin, recenziile vor apărea, ceea ce înseamnă că hook-ul meu personalizat pare să funcționeze corect.

Iată codul ReviewForm.tsx:

export const ReviewForm: React.FC<Props> = ({ productId }): JSX.Element => {
  const [showForm, setShowForm] = useState<boolean>(false);
  const [storedValues, setStoredValues] = useSessionStorage<SessionStorage[]>(
    "products-reviews",
    []
  );

  const registerReview = (event: any) => {
    event.preventDefault();
    const reviewText = event.target.review.value;
    const productIndex = storedValues?.findIndex(
      (review) => review.productId === productId
    );

    if (productIndex === -1 || productIndex === undefined) {
      setStoredValues([...storedValues!, { productId, reviews: [reviewText] }]);
    } else {
      const reviews = [...storedValues![productIndex].reviews, reviewText];
      const updatedReviews = [...storedValues!];
      updatedReviews[productIndex].reviews = reviews;
      setStoredValues(updatedReviews);
    }

    setShowForm(false);
  };

  return (
    <div className={styles.reviewsContainer}>
      <button
        className={styles.addReviewButton}
        onClick={() => setShowForm(true)}
      >
        <span>Add a review</span>
      </button>
      {showForm && (
        <form
          className={styles.reviewForm}
          onSubmit={(event) => registerReview(event)}
        >
          <textarea className={styles.reviewInput} name="review" required />
          <button className={styles.reviewSubmitButton} type="submit">
            Submit
          </button>
        </form>
      )}
      <ReviewList productId={productId} />
    </div>
  );
};

Și iată componenta ReviewList.tsx, redată în interiorul ReviewForm.tsx:

export const ReviewList: React.FC<Props> = ({ productId }): JSX.Element => {
  const [reviews, _] = useSessionStorage<SessionStorage[]>(
    "products-reviews",
    []
  );

  const productReviews = reviews?.find(
    (review) => review.productId === productId
  )?.reviews;

  return (
    <ul>
      {productReviews?.map((review) => (
        <li key={Math.random() * 10000}>{review}</li>
      ))}
    </ul>
  );
};

În cele din urmă, iată hook-ul meu personalizat useSessionStorage:

export const useSessionStorage = <T>(
  key: string,
  initialValue?: T
): SessionStorage<T> => {
  const [storedValue, setStoredValue] = useState<T | undefined>(() => {
    if (!initialValue) return;
    try {
      const value = sessionStorage.getItem(key);
      return value ? JSON.parse(value) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  useEffect(() => {
    if (storedValue) {
      try {
        sessionStorage.setItem(key, JSON.stringify(storedValue));
      } catch (error) {
        console.log(error);
      }
    }
  }, [storedValue, key]);

  return [storedValue, setStoredValue];
};

Titlul întrebării mele spune „cum să refac randarea componentului copil” pentru că am observat că dacă șterg complet componenta ReviewList.tsx, aducând toată logica sa de randare în ReviewForm.tsx, aplicația mea se va comporta așa cum mă aștept.

Deci poate problema este legată de această relație dintre componente?

Orice sfat este binevenit.

Răspuns #1

Problema

Problema este în hook-ul tău useSessionStorage. Nu este de fapt sincronizat cu sesiunea de stocare, deoarece starea este de fapt stocată cu useState, fiind populată doar la montare.

Cum funcționează în cazul tău:

  1. Inițializezi PRIMUL STAT folosind useState (în interiorul hook-ului personalizat useSessionStorage) cu valoarea curentă din session storage la montarea componentului în ReviewList.tsx
  2. Inițializezi AL DOILEA STAT folosind useState (în interiorul hook-ului personalizat useSessionStorage) cu valoarea curentă din session storage la montarea componentului în ReviewForm.tsx
  3. Modifici AL DOILEA STAT și împingi schimbările în stocarea sesiunii cu useEffect

Așadar, PRIMA STARE nu este actualizată cu noua valoare până nu re-montezi componenta.

Soluția 1 (Va funcționa doar pentru sincronizarea între diferite file de browser)

Trebuie să inversăm fluxul de date de la useState -> sessionStorage la sessionStorage -> useState

export const useSessionStorage = <T>(
  key: string,
  initialValue?: T
): SessionStorage<T> => {
  const [storedValue, setStoredValue] = useState<T | undefined>(() => {
    if (!initialValue) return;
    try {
      const value = sessionStorage.getItem(key);
      return value ? JSON.parse(value!) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  const setStorageValue = useCallback((newValue: T) => {
    try {
      sessionStorage.setItem(key, JSON.stringify(newValue));
    } catch (error) {
      console.log(error);
    }
  }, []);

  /** This `useEffect` will make sure `storedValue` is always in sync with the `sessionStorage` */
  useEffect(() => {
    const listenToStorageEvent = (event: StorageEvent) => {
      if (event.storageArea === sessionStorage && event.key === key) {
        try {
          const newValue = JSON.parse(event.newValue!);

          if (storedValue !== newValue) {
            setStoredValue(newValue);
          }
        } catch (error) {
          console.log(error);
        }
      }
    };

    window.addEventListener("storage", listenToStorageEvent);

    return () => {
      window.removeEventListener("storage", listenToStorageEvent);
    };
  }, [key]);

  // We expose `setStorageValue` which works with `sessionStorage` instead of `setStoredValue` which works with local state
  return [storedValue, setStorageValue];
};

Soluția 2

Folosește evenimente personalizate pentru a putea sincroniza și același tab https://github.com/imbhargav5/rooks/blob/main/src/hooks/useSessionstorageState.ts

Soluția 3

Analizează întregul stocaj de sesiune la pornirea aplicației și pune-l ca stare într-un context. După aceea, la fiecare „set”, actualizează atât starea contextului, cât și sessionStorage. Această soluție are multe dezavantaje, cum ar fi predispoziția la erori datorită sincronizării manuale a stării cu stocajul de sesiune, re-redarea excesivă a întregului arbore de componente sub providerul de stocaj de sesiune la fiecare actualizare a valorii stocajului. Așadar, nici măcar nu voi adăuga exemple de cod aici.