reactjs 如何在React中切换一个图标以触发另一个图标的禁用?

qoefvg9y  于 2023-01-04  发布在  React
关注(0)|答案(1)|浏览(155)

我正在使用react和redux工具包开发一个应用程序。我正在使用API。我希望得到帮助的部分是:我有一个收藏夹图标。此图标会复制电影并将其显示在收藏夹部分。还有另一个图标用于观看的电影。因此,如果我单击"观看",它应该只禁用我单击的电影卡的收藏夹图标。但是,它会禁用所有电影卡的所有收藏夹图标。观看的图标称为眼睛,收藏夹图标称为星
这是眼睛组件(监视)

import { useState } from "react";
    import { BsEyeSlash, BsEyeFill } from "react-icons/bs";
    import { useDispatch, useSelector } from "react-redux";
    import {
        add_watched,
        remove_watched,
        add_occupied,
        remove_occupied,
    } from "../features/animeSlice";
    
    const Eye = ({ anime, type }) => {
        const [active_eye, setActive_eye] = useState(false);
        const dispatch = useDispatch();
    
        const toggle_eye = () => {
            if (!active_eye) {
                setActive_eye((prev) => {
                    return !prev;
                });
                dispatch(add_watched(anime));
                dispatch(add_occupied(type));
            } else {
                setActive_eye((prev) => {
                    return !prev;
                });
                dispatch(remove_watched(anime?.mal_id));
                dispatch(remove_occupied());
            }
        };
    
        return (
            <div>
                {!active_eye ? (
                    <BsEyeSlash
                        className="text-xl text-green-500 icons_1"
                        onClick={toggle_eye}
                    />
                ) : (
                    <BsEyeFill
                        className={"text-xl text-red-500 icons_1"}
                        onClick={toggle_eye}
                    />
                )}
            </div>
        );
    };
    
    export default Eye;

这是星形组件(收藏夹)

import { useState } from "react";
    import { AiOutlineStar, AiFillStar } from "react-icons/ai";
    import { add_favourite, remove_favourite } from "../features/animeSlice";
    import { useDispatch, useSelector } from "react-redux";
     
    const Star = ({ anime }) => {
        const { value} = useSelector((state) => state.anime);
        const [active_star, setActive_star] = useState(false);
        const dispatch = useDispatch();
    
        const toggle_star = () => {
            if (!active_star) {
                setActive_star((prev) => {
                    return !prev;
                });
                dispatch(add_favourite(anime));
                
            } else {
                setActive_star((prev) => {
                    return !prev;
                });
                dispatch(remove_favourite(anime?.mal_id));
            }
        };
    
        return (
            <div>
                {!active_star ? (
                    <AiOutlineStar
                        className={
                            value === "occupied"
                                ? "text-xl text-gray-300 pointer-events-none"
                                : "text-xl text-yellow-500 icon_1"
                        }
                        onClick={toggle_star}
                    />
                ) : (
                    <AiFillStar
                        className={
                            value === "occupied"
                                ? "text-xl text-gray-300 pointer-events-none"
                                : "text-xl text-yellow-500 icon_1"
                        }
                        onClick={toggle_star}
                    />
                )}
            </div>
        );
    };
    export default Star;

这是redux工具包切片

import { createSlice } from "@reduxjs/toolkit";
        
        const initialState = {
            favourite: [],
            watched: [],
            value: "",
            page:""
        };
        
        const animeSlice = createSlice({
            name: "anime",
            initialState,
            reducers: {
                add_favourite(state, { payload }) {
                    state.favourite.push(payload);
                },
                remove_favourite(state, { payload }) {
                    state.favourite = state.favourite.filter(
                        (anime) => anime?.mal_id !== payload
                    );
                },
                add_watched(state, { payload }) {
                    state.watched.push(payload);
                    
                    
                },
                remove_watched(state, { payload }) {
                    state.watched = state.watched.filter((anime) => anime?.mal_id !== payload);
                },
                add_occupied(state, { payload }) {
                    state.value = payload;
                },
                remove_occupied(state) {
                    state.value = "";
                },
                pageNumber(state, { payload }) {
                    state.page = payload
                }
            },
        });
        
        export const {
            add_favourite,
            remove_favourite,
            add_watched,
            remove_watched,
            add_occupied,
            remove_occupied,
            pageNumber
        } = animeSlice.actions;
        export default animeSlice.reducer;
    
    This is the component that holds the component of eye and star
import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import Eye from "./Eye";
import Star from "./Star";


const TopAnime = ({ anime }) => {
    //pagination
    
    //

    
    //
    let colorYear = (year) => {
        if (year === "N/A") {
            return "text-violet-500";
        } else {
            return "";
        }
    };

    return (
        <section className="grand px-4 pt-2 pb-5 w-64 bg-white/5 bg-opacity-80 backdrop-blur-sm rounded-lg cursor-pointer font-poppins animate-slideup">
            <div className="pb-1 wrapper_icons">
                <div className="wrapper_hover">
                    <Eye anime={anime} type="occupied" />
                    <Star anime={anime} />
                </div>
                
            </div>
            <Link to={`/anime/${anime?.mal_id}`}>
                <div className="wrapper_1 flex flex-col items-center justify-center">
                    <div className="h-[313px] w-[219px]">
                        <img
                            src={anime?.images?.jpg?.large_image_url}
                            alt={anime?.title}
                            className="h-full w-full"
                        />
                    </div>
                </div>
                <div className="flex flex-col mt-3">
                    <p className="text-sm text-white truncate mx-1">
                        {anime?.title_english ? anime?.title_english : anime?.title}
                    </p>
                    <div className="flex justify-between items-center text-sm text-yellow-500 mx-1">
                        <p className={colorYear(anime?.year ? anime?.year : "N/A")}>
                            {anime?.year ? anime?.year : "N/A"}
                        </p>
                        <p
                            className={
                                anime?.score <= 7
                                    ? "text-cyan-500"
                                    : anime?.score <= 5
                                    ? "text-red-600"
                                    : "text-green-500"
                            }
                        >
                            {anime?.score}
                        </p>
                    </div>
                </div>
            </Link>
        
        </section>
                    
    );
};

export default TopAnime;
This is where TopAnime is rendered
import { useGetAnimeQuery } from "../features/API";
import { useDispatch, useSelector } from "react-redux";
import { useState, useEffect } from "react";
import TopAnime from "../components/TopAnime";
import Spinner from "../components/Spinner";
import { NavLink, Link } from "react-router-dom";
import Paginator from "../components/Paginator";
//import ReactPaginate from "react-paginate";
import Pagination from "@mui/material/Pagination";

const Home = () => {
    
    const [page, setPage] = useState(1);
    

    const {
        data: manga = [],
        isLoading,
        isFetching,
        error,
    } = useGetAnimeQuery(page);
    const { data, pagination } = manga;
    
    //destructuring objects
    //const { data, pagination } = manga;

    //const top_anime = data;
    const total = Math.ceil(pagination?.items?.total / 24)
    
    //const current_page = pagination?.current_page;
    //const per_page = pagination?.items?.per_page;

    //const { items: pages } = total;

/*  let fetchData = async (page = 1) => {

        let res = await fetch(
            `https://api.jikan.moe/v4/top/anime?page=${page}&limit=24`
        );
        let query = await res.json();
        const { data, pagination } = query;
        let totalPages = Math.ceil(pagination?.items.total / 24);
        setPageCount(totalPages);
        setData(data);
        
    };

    useEffect(() => {
        
        
        fetchData();
    }, []);

 */
import { useGetAnimeQuery } from "../features/API";
    import { useDispatch, useSelector } from "react-redux";
    import { useState, useEffect } from "react";
    import TopAnime from "../components/TopAnime";
    import Spinner from "../components/Spinner";
    import { NavLink, Link } from "react-router-dom";
    import Paginator from "../components/Paginator";
    //import ReactPaginate from "react-paginate";
    import Pagination from "@mui/material/Pagination";
    
    const Home = () => {
        
        const [page, setPage] = useState(1);
        
    
        const {
            data: manga = [],
            isLoading,
            isFetching,
            error,
        } = useGetAnimeQuery(page);
        const { data, pagination } = manga;
        
        //destructuring objects
        //const { data, pagination } = manga;
    
        //const top_anime = data;
        const total = Math.ceil(pagination?.items?.total / 24)
        
        //const current_page = pagination?.current_page;
        //const per_page = pagination?.items?.per_page;
    
        //const { items: pages } = total;
    
    /*  let fetchData = async (page = 1) => {
    
            let res = await fetch(
                `https://api.jikan.moe/v4/top/anime?page=${page}&limit=24`
            );
            let query = await res.json();
            const { data, pagination } = query;
            let totalPages = Math.ceil(pagination?.items.total / 24);
            setPageCount(totalPages);
            setData(data);
            
        };
    
        useEffect(() => {
            
            
            fetchData();
        }, []);
    
     */
        
        const handleChange = (event, value) => {
            setPage(value);
        };
    
        const display = data?.map((anime) => (
            <TopAnime anime={anime} key={anime.mal_id} />
        ));
    
    
    
        //const pageCount = Math.ceil(pagination?.items?.total / 24);
    
         if (isLoading) {
            return <Spinner color_1={"#141e30"} color_2={"#243b55"} />;
        } else if (isFetching) {
            return <Spinner color_1={"#141e30"} color_2={"#243b55"} />;
        } else if (error) {
            console.log("ERROR =>", error.message);
        } 
    
        return (
            <section className="bg-gradient-to-r from-[#141e30] to-[#243b55]">
                <div className="container font-poppins">
                    <div className="grid grid-cols-4 gap-3 place-items-center px-20">
                        {/* {top_anime &&
                            top_anime?.map((anime) => (
                                <TopAnime anime={anime} key={anime.mal_id} />
                            ))} */}
                        {display}
                    </div>
                    <div className="button text-yellow-500 flex items-center justify-center mt-2 pb-2 cursor-pointer">
                        {/* <Paginator paginated={paginated} NumP={pagination?.last_visible_page} /> */}
                        {/* <ReactPaginate
                            previousLabel={"Previous"}
                            nextLabel={"Next"}
                            onPageChange={(page) => fetchData(page.selected + 1)}
                            pageCount={pageCount}
                            className="flex space-x-2"
                            activeClassName="active"
                        /> */}
                        <Pagination count={total} page={page} onChange={handleChange} defaultPage={1} boundaryCount={3} color="secondary" sx={{button:{color:'#ffffff'}}} />
                    </div>
                </div>
            </section>
        );
    };
    export default Home;
ljsrvy3e

ljsrvy3e1#

问题

代码/当前实现的问题是只有***一个***state.anime.value和***一个***"占用"值。每次切换电影的"已观看"状态时,Redux状态都会设置/取消设置单个state.anime.value状态。换句话说,您可以切换N部电影的"已观看"状态,state.anime.value值为"occupied"。但随后仅将1部电影切换回"未观看",并且state.anime.value被重置回""。所有Star组件读取该单个状态值,这就是所有星星一起切换的原因。

溶液

如果您希望***一个***特定的电影在Eye图标切换时切换Star组件,那么我认为解决方案比您在代码中实现它要简单一些。
"eye"和"star"组件有效地复制了Redux状态,但无法很好地与之同步。它通常被视为复制状态的React反模式。state.anime.favouritestate.anime.watched数组是应用了解哪些内容被标记为"受关注"和哪些内容被收藏所需的全部内容。
更新animeSlice以将anime.mal_id属性存储在对象中而不是数组中,以便在UI中提供O(1)查找。删除anime.value状态,因为它是不必要的。
animeSlice.js

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  favourite: {},
  watched: {},
  page: ""
};

const animeSlice = createSlice({
  name: "anime",
  initialState,
  reducers: {
    add_favourite(state, { payload }) {
      state.favourite[payload] = true;
    },
    remove_favourite(state, { payload }) {
      delete state.favourite[payload];
    },
    add_watched(state, { payload }) {
      state.watched[payload] = true;
    },
    remove_watched(state, { payload }) {
      delete state.watched[payload];
    },
    pageNumber(state, { payload }) {
      state.page = payload;
    }
  }
});

export const {
  add_favourite,
  remove_favourite,
  add_watched,
  remove_watched,
  pageNumber
} = animeSlice.actions;

export default animeSlice.reducer;

更新EyeStar组件以从存储中读取***true***状态。这些组件应将anime.mal_id传递给已调度的操作,并使用anime.mal_id检查受监视/收藏夹Map对象。
眼睛

import { BsEyeSlash, BsEyeFill } from "react-icons/bs";
import { useDispatch, useSelector } from "react-redux";
import { add_watched, remove_watched } from "../features/animeSlice";

const Eye = ({ anime }) => {
  const dispatch = useDispatch();
  const { watched } = useSelector((state) => state.anime);

  const isWatched = watched[anime.mal_id];

  const toggle_eye = () => {
    dispatch((isWatched ? remove_watched : add_watched)(anime.mal_id));
  };

  const Eye = isWatched ? BsEyeFill : BsEyeSlash;
  const className = [
    "text-xl icons_1",
    isWatched ? "text-green-500" : "text-red-500"
  ].join(" ");

  return (
    <div>
      <Eye className={className} onClick={toggle_eye} />
    </div>
  );
};

export default Eye;

Star

import { AiOutlineStar, AiFillStar } from "react-icons/ai";
import { add_favourite, remove_favourite } from "../features/animeSlice";
import { useDispatch, useSelector } from "react-redux";

const Star = ({ anime }) => {
  const { favourite, watched } = useSelector((state) => state.anime);
  const dispatch = useDispatch();

  const isFavorited = favourite[anime.mal_id];
  const isWatched = watched[anime.mal_id];

  const toggle_star = () => {
    dispatch((isFavorited ? remove_favourite : add_favourite)(anime.mal_id));
  };

  const Star = isFavorited ? AiFillStar : AiOutlineStar;
  const className = [
    "text-xl",
    isWatched ? "text-gray-300 pointer-events-none" : "text-yellow-500 icon_1"
  ].join(" ");

  return (
    <div>
      <Star className={className} onClick={toggle_star} />
    </div>
  );
};
export default Star;

相关问题