Search code examples
reactjsruby-on-railsstaterelational-databasehas-many

How do I correctly update User state in React with a has_many :trails, through: :reviews association, after deleting a review?


ERD for my API

Users have many reviews, and many trails through reviews, and vice versa.

Goal:

  1. On a user profile page, render a list of names of trails that user has reviewed, without duplicating any of the trail names. For example, if a user has reviewed a trail more than once, it should only display the trail name once. If a user has reviewed a trail multiple times, and deletes one of those reviews, the list should still display the trail name. If all of the user's reviews for that trail have been deleted, the trail name should be removed from the rendered list.

  2. When a review is deleted, update frontend user state so that user.reviews and user.trails are accurate, without making a GET request to do so.

What I've tried:

Create a set of unique trail names from user.trails and then render them in JSX by mapping over them

const uniqTrailNames = [...new Set(user.trails.map((trail) => {
  return trail.name
}))]
{(uniqTrailNames.map((name) => {
  return <p key={name}>{name}</p>
}))}

This is successful in rendering a unique list of trail names from the trails a user has reviewed. However, I'm struggling to setUser state correctly when a review is deleted.

Currently, my handleDelete() function successfully destroys the review in the database, and then calls setUser in order to update user.reviews and user.trails, but this is where I'm updating the trails property incorrectly. Filtering this way removes all user.trails objects with id's that match the review's associated trail_id, so even if the user had another review of the trail, the trail name doesn't render because the trails no longer exist in user.trails. How do I go about removing only a single trail object when deleting a review, so that user.trails remains accurate, if trail objects are essentially identical? Am I going about this the wrong way? It's part of my project requirement to showcase the full has_many through relationship, so that's why I've taken this approach so far.

import { useContext } from 'react'
import { UserContext } from '../contexts/UserContext'
import { TrailsContext } from '../contexts/TrailsContext'
import { NavLink } from 'react-router-dom'

function UserTrailReview({ review }) {

    const {user, setUser} = useContext(UserContext)
    const {trails, setTrails} = useContext(TrailsContext)
    const {
        id,
        trail_id,
        trail_rating,
        condition,
        content,
        formatted_date,
        trailname
    } = review

    const trail = trails.find((trail) => {
       return trail.id === trail_id
    })

    function handleDelete() {
        fetch(`/reviews/${id}`, {
            method: "DELETE"
        })
       .then(setUser({
            ...user,
            reviews: user.reviews.filter((review) => {
                return review.id !== id
            }),
            trails: user.trails.filter((trail) => {
                return trail.id !== trail_id
            })
        }))
        .then(() => {
            trail.reviews = trail.reviews.filter((review) => {
                return review.id !== id
            })
            const updatedTrails = trails.map((t) => {
                if (t.id === trail_id) {
                    return trail
                } else {
                    return t
                }
            })
            setTrails(updatedTrails)
        })
    }

Solution

  • A better way to avoid the duplication issue in the first place is to use the Active Record Query Method "distinct". In your models, wherever you have a has_many, through relationship, use distinct like so:

    has_many :trails, -> { distinct }, through: :reviews