How to differentiates buttons and behaviour in a function

Viewed 37

I have a list of items; Each item has a "Details" button beside it.

When the "Details" button it is pressed, I would like to show under the list the details of that element.

So far so good. I managed to do it. Even if it doesn't seem the best way. Now the problem is:

When I press, for the first time, a button, it shows the details of that item. But when I press again, regardless of the button, it close it. This is because I don't understand how to differentiate them. So for closing the "Details" I can just hit any button, and I don't want it.

My desired behavior would be (pseudo code):

if details_not_showing:
   show_details(id==button_pressed)
else:
   if details_showing == details_from_button_pressed
      close_details()
   else
      show_details(id==button_pressed)

Hoping this make some sense, I leave you with my terrible code under this.

Imports

import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";

Function

function MonthArticles() {
  const { user_id, year, month } = useParams();
  const url =
    "http://127.0.0.1:8000/api/" + user_id + "/posts/" + year + "/" + month;
  const url_retrieve_author = "http://127.0.0.1:8000/api/retrieve-author";

  const formdata = new FormData();
  formdata.append("id", user_id);
  const requestOptions = {
    method: "POST",
    body: formdata,
  };

  const [article, setArticle] = useState([]);
  useEffect(() => {
    fetch(url, {
      method: "GET",
    })
      .then((res) => res.json())
      .then((data) => {
        setArticle(data);
      });
  }, []);

  const [author, setAuthor] = useState([]);
  useEffect(() => {
    fetch(url_retrieve_author, requestOptions)
      .then((res) => res.json())
      .then((data) => {
        setAuthor(data);
      });
  }, []);

  const [details, setDetails] = useState(false);
  const [articleId, setArticleId] = useState();

  return (
    <div>
      <h2>
        Articles writte in {year}, {month} - By{" "}
        {author.map((author) => (
          <div key={author.id}>{author.last_name}</div>
        ))}
      </h2>
      <h4>List of articles below:</h4>
      <ul>
        {article.map((article) => (
          <div key={article.id}>
            <li key={article.id}>
              {article.title}{" "}
              <button
                id={article.id}
                type='button'
                onClick={() => [
                  setDetails((currentDetails) => !currentDetails),
                  setArticleId(article.id),
                ]}
              >
                Details
              </button>
            </li>
          </div>
        ))}
      </ul>

      {details ? (
        <div>
          <h3>Showing details</h3>
          {article
            .filter((a) => a.id === articleId)
            .map((filteredArticle) => (
              <div>
                <h4>Post created in: {filteredArticle.date_created}</h4>
                <p>{filteredArticle.text}</p>
              </div>
            ))}
        </div>
      ) : null}
    </div>
  );
}

Thanks in advance

3 Answers

Just check on click if the id doesn't change then return null to hide the details, if not set the new article id to show the details.

function MonthArticles() {
  const { user_id, year, month } = useParams();
  const url =
    "http://127.0.0.1:8000/api/" + user_id + "/posts/" + year + "/" + month;
  const url_retrieve_author = "http://127.0.0.1:8000/api/retrieve-author";

  const formdata = new FormData();
  formdata.append("id", user_id);
  const requestOptions = {
    method: "POST",
    body: formdata,
  };

  const [article, setArticle] = useState([]);
  useEffect(() => {
    fetch(url, {
      method: "GET",
    })
      .then((res) => res.json())
      .then((data) => {
        setArticle(data);
      });
  }, []);

  const [author, setAuthor] = useState([]);
  useEffect(() => {
    fetch(url_retrieve_author, requestOptions)
      .then((res) => res.json())
      .then((data) => {
        setAuthor(data);
      });
  }, []);

  const [details, setDetails] = useState(false);
  const [articleId, setArticleId] = useState();

  return (
    <div>
      <h2>
        Articles writte in {year}, {month} - By{" "}
        {author.map((author) => (
          <div key={author.id}>{author.last_name}</div>
        ))}
      </h2>
      <h4>List of articles below:</h4>
      <ul>
        {article.map((article) => (
          <div key={article.id}>
            <li key={article.id}>
              {article.title}{" "}
              <button
                id={article.id}
                type='button'
                onClick={() => [
                  setDetails((currentDetails) => !currentDetails),
                  setArticleId(prev => {
                    if (prev === articleId) return null
                    return article.id
                  }),
                ]}
              >
                Details
              </button>
            </li>
          </div>
        ))}
      </ul>

      {details ? (
        <div>
          <h3>Showing details</h3>
          {article
            .filter((a) => a.id === articleId)
            .map((filteredArticle) => (
              <div>
                <h4>Post created in: {filteredArticle.date_created}</h4>
                <p>{filteredArticle.text}</p>
              </div>
            ))}
        </div>
      ) : null}
    </div>
  );
}

Currently, in your buttons onClick functions you do this:

onClick={() => [
      setDetails((currentDetails) => !currentDetails),
      setArticleId(article.id),
]}

The first line toggles whether or not the details secions is visible. All buttons currently toggle the details visibility. What you want is to only do this when it is the button corresponding to the currently displayed details.

onClick={() => {
      //If no details are visible, show them
      if(!details) setDetails(true);   
      //If details are visible, and this is the corresponding button, hide them
      else if(article.id == articleId) setDetails(false); 
      setArticleId(article.id);
}}

The main issue in your code is that details is toggled from true to false on any button click.

The solution with minimal changes would check the current articleId before toggling the details value.

onClick={() => [
  setDetails((currentDetails) => !currentDetails),
  setArticleId(article.id),
]}

Should be changed into:

onClick={() => {
  // `details` is set to `true` if a new article is clicked.
  // When pressing the button of the same article multiple
  // times, the value is toggled.
  setDetails(article.id !== articleId || !details);
  setArticleId(article.id);
}}

There is also a solution that only uses a single state, but requires more code to change.

// `false` if no details, otherwise an article id
const [detailsId, setDetailId] = useState(false);

// Lookup the article of which details must be shown.
// Performance can be improved by using the useMemo hook.
const details = article.find(article => article.id === detailsId);

This essentially uses detailsId as a boolean. It contains false if no details must be shown. But instead of true we use the article id, to store both the fact that details must be shown, and the id of the article that must be shown.

details looks up the article based on the detailsId. This variable (not a state) helps simplify the view later on.

Your onClick handler then becomes:

onClick={() => {
  // Set `detailId` to `false` if is the same as the current article id,
  // otherwise set the current article id.
  setDetailId(article.id !== detailId && article.id);
}}

Finally you need to update the view:

{details && (
  <div>
    <h3>Showing details</h3>
    <div>
      <h4>Post created in: {details.date_created}</h4>
      <p>{details.text}</p>
    </div>
  </div>
)}
Related