1

I am trying to sort an array and reflect its sort result immediately by useState hook. I already new that react is detecting its state change by Object.is(), so trying to spread array before using useState like below;

const [reviews, setReviews] = useState([...book.reviews]);

    useEffect(() => {
        const sortedReviews = [...reviews]
            .sort((review1: IReview, review2: IReview) =>
                sortBy(review1, review2, sortRule));

        setReviews([...sortedReviews])
        }, [sortRule])

After sorting I checked the value of variable sortedReviews and it was sorted as expected, but react did not re-render the page so this sorting was not reflected to UI.

I already searched solutions and it seemed many could solve the issue by spreading an array to before calling useState like this stack overflow is explaining: Why is useState not triggering re-render?.
However, on my end it is not working.. Any help will be very appreciated. Thank you!

[Added] And my rendering part is like below;

    <>
        {
            sortedReviews
                .map((review: IReview) => (
                    <ReviewBlock id={review.id}
                                 review={review}
                                 targetBook={book}
                                 setTargetBook={setBook}/>
                ))
        }
    </>
Takuya.N
  • 143
  • 1
  • 9
  • what make you sure that your component is not re-rendering? – miraj Apr 04 '21 at 23:54
  • Hi miraj, I thought it was not re-rendered because the order was not sorted as the array `sortedReviews `. For example, when I sorted `sortedReviews ` based on CreatedDate, it was not ordered that way on the page while the value of the variable was. Am i making sense? – Takuya.N Apr 05 '21 at 00:12
  • you put `sortRule` in the dependency array. make sure it does not change in every re-render. if so then your component will fall in a loop. – miraj Apr 05 '21 at 00:16
  • another thing is `setReviews` will be called asynchronously. it may set the given value before your `sort` function finish. – miraj Apr 05 '21 at 00:19
  • but I confirmed `sortedReviews` was sorted as expected, so `setReviews` should receive ideal ordered arrays, so sorting should be processed within `useEffect` as expected.. – Takuya.N Apr 05 '21 at 00:30
  • i got your point. the problem is not with sortReviews. `setReviews` is getting `reviews` as you initialized `sortReviews` with it. – miraj Apr 05 '21 at 00:50
  • Thank you for checking further. I tried as `const sortedReviews =[...reviews].sort.....`, to make `sortedReviews` from cloned array, not exact from `reviews`. However, I am still experiencing the same issue... – Takuya.N Apr 05 '21 at 01:02
  • oops! missed the `.` – miraj Apr 05 '21 at 01:05
  • `reviews` is missing in your dependency list. – DedaDev Apr 05 '21 at 01:08
  • I don't think we need to put `reviews` in dep list of `useEffect` as it should be updated only when `sortRule` would be updated. I checked useEffect is working as expected for sorting (i.e. `reviews` has the correctly sorted items). My problem here is this sorting on `reviews` is not reflected on the page as `useState` is not triggering re-render.. – Takuya.N Apr 05 '21 at 01:25

2 Answers2

3

Sometimes I facing this issue too, in my case I just "force" render calling callback.

First solution:

const [reviews, setReviews] = useState([...book.reviews]);

    useEffect(() => {
        const sortedReviews = [...reviews]
            .sort((review1: IReview, review2: IReview) =>
                sortBy(review1, review2, sortRule));

        setReviews(()=> [...sortedReviews]) // <-- Here
        }, [sortRule])

second solution: You can use useRef to get data in real time, see below:

    const [reviews, setReviews] = useState([...book.reviews]);
    const reviewsRef = useRef();
     
            useEffect(() => {
                const sortedReviews = [...reviews]
                    .sort((review1: IReview, review2: IReview) =>
                        sortBy(review1, review2, sortRule));
        
                setReviewRef([...sortedReviews]) 
                }, [sortRule])

function setReviewRef(data){
       setReview(data);
       reviewsRef.current = data;
    }  

So, instead use the state reviews use reviewsRef.current as u array

I hope you can solve this!

Hermanyo H
  • 134
  • 3
1

When components don't re-render it is almost always due to mutations of state. Here you are calling a mutating operation .sort on reviews. You would need to spread the array before you mutate it.

useEffect(() => {
  const sortedReviews = [...reviews].sort((review1: IReview, review2: IReview) =>
    sortBy(review1, review2, sortRule)
  );

  setReviews(sortedReviews);
}, [sortRule]);

But there are other issues here. reviews is not a dependency of the useEffect so we would want to use a setReviews callback like setReviews(current => [...current].sort(....

In general sorted data makes more sense as a useMemo than a useState. The sortRule is a state and the sortedReviews are derived from it.

The way that you are calling sortBy as a comparer function feels a bit off. Maybe it's just a confusing name?

Also you should not need to include the type IReview in your callback if book is typed correctly as {reviews: IReview[]}.

If you include your sortBy function and sortRule variable then I can be of more help. But here's what I came up with.

import React, { useState, useMemo } from "react";

type IReview = {
    rating: number;
}

type Book = {
    reviews: IReview[]
}

type SortRule = string; // just a placeholder - what is this really?

declare function sortBy(a: IReview, b: IReview, rule: SortRule): number;

const MyComponent = ({book}: {book: Book}) => {
    const [sortRule, setSortRule] = useState("");

    const reviews = book.reviews;

    const sortedReviews = useMemo( () => {
        return [...reviews].sort((review1, review2) =>
            sortBy(review1, review2, sortRule)
        );
    }, [sortRule, reviews]);
...
}

Typescript Playground Link

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102
  • Hi Linda, thank you so much for checking. – Takuya.N Apr 05 '21 at 01:39
  • Hi Linda, thank you so much for checking. `SortRule ` is actually enum, and default is like(0); ```enum SortRule { like, new }``` – Takuya.N Apr 05 '21 at 01:45
  • I tried as you kindly showed, and it almost working but rendering part is not working still. I confirmed `sortedReviews ` is processed as expected when switching the selector, but after switching `SortRule` to new from like, UI is still showing default order; based on Like. I updated the above question desc to add my rendering part referring `sortedReviews`. May i ask your opinion if this part could be causing the issue? – Takuya.N Apr 05 '21 at 01:46
  • You probably want to add `key={review.id}` to `` but I don't think that's your problem and I don't see anything else wrong in the render. – Linda Paiste Apr 05 '21 at 02:03