React.js中基于属性值过滤Rick and Morty API角色的问题排查及通用过滤组件实现咨询
Hey there! Let's break down the issues in your code first, then walk through building a reusable filter component that works for species, status, and gender.
First: Fixing Your Current Filtering Logic
Your code has a few key issues that are preventing the filtered data from showing up:
1. Event handlers can't return JSX directly
Your handleSpeciesChange and handleStatusChange functions are trying to return components like <Human /> when the select changes—but event callbacks don't work that way. The return value gets ignored entirely. Instead, you need to update component state to trigger a re-render with the filtered data.
2. Your Human component has data handling bugs
- The Rick and Morty API returns an object with a
resultsarray (not a single character), socharacterFilters.namewill never exist. You need to access theresultsarray and map over it. - You're hardcoding
species=humanin the fetch call, but you already havematch.params.species—use that to make the component dynamic! - No loading state: You'll get errors if you try to render data before the fetch completes.
Here's a fixed version of that component (renamed to be reusable for any species):
export const SpeciesFilteredCharacters = ({ match }) => { const { species } = match.params; const [characters, setCharacters] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { setLoading(true); fetch(`https://rickandmortyapi.com/api/character?species=${species}`) .then((res) => res.json()) .then((data) => { setCharacters(data.results); setLoading(false); }) .catch((err) => { console.error(err); setLoading(false); }); }, [species]); // Re-run when species param changes if (loading) return <div>Loading characters...</div>; return ( <div className="grid grid-cols-5 gap-4"> {characters.map((character) => ( <div key={character.id}> <h2>{character.name}</h2> <img alt={character.name} src={character.image} /> </div> ))} </div> ); };
3. Fixing the Characters component's filtering
Instead of returning components in event handlers, use a single state object to track all filter values, then re-fetch data whenever those values change. Here's the revised core logic:
export const Characters = () => { const [characters, setCharacters] = useState([]); const [loading, setLoading] = useState(true); // Track all filter values in one state object const [filters, setFilters] = useState({ species: "all", status: "all", gender: "all" }); // Fetch data whenever filters change useEffect(() => { setLoading(true); // Build query parameters dynamically const queryParams = new URLSearchParams(); Object.entries(filters).forEach(([key, value]) => { if (value !== "all") queryParams.append(key, value); }); fetch(`https://rickandmortyapi.com/api/character?${queryParams}`) .then((res) => res.json()) .then((data) => { setCharacters(data.results); setLoading(false); }) .catch((err) => { console.error(err); setLoading(false); }); }, [filters]); // Handle all filter changes with one function const handleFilterChange = (e) => { const { name, value } = e.target; setFilters(prev => ({ ...prev, [name]: value })); }; if (loading) return <Loading />; return ( <div className="p-4 font-mono text-green-500"> <div className="flex flex-row"> {/* Update selects to bind to filter state */} <div className="m-4"> <label>Species</label> <select name="species" value={filters.species} onChange={handleFilterChange} > <option value="all">all</option> <option value="human">human</option> <option value="alien">alien</option> <option value="unknown">unknown</option> </select> </div> {/* Repeat for status and gender selects, using the same handleFilterChange */} <div className="m-4"> <label>Status</label> <select name="status" value={filters.status} onChange={handleFilterChange} > <option value="all">all</option> <option value="alive">alive</option> <option value="dead">dead</option> <option value="unknown">unknown</option> </select> </div> <div className="m-4"> <label>Gender</label> <select name="gender" value={filters.gender} onChange={handleFilterChange} > <option value="all">all</option> <option value="female">female</option> <option value="male">male</option> <option value="genderless">genderless</option> <option value="unknown">unknown</option> </select> </div> </div> <h1 className="text-4xl">Characters</h1> <Pagination data={characters} pageLimit={5} /> <div className="grid grid-cols-5 gap-4"> {characters.map((character) => ( <div key={character.id}> <Character character={character} /> </div> ))} </div> </div> ); };
Second: Building a Reusable Filter Component
Absolutely! A reusable filter component will cut down on duplicate code and make it easy to add/remove filters later. Here's how to build one:
Step 1: Create the generic filter component
This component takes a config object to render all filter controls dynamically:
export const FilterControls = ({ filters, onFilterChange }) => { // Define your filter configs here (easy to update later!) const filterConfigs = [ { label: "Species", name: "species", options: [ { value: "all", label: "all" }, { value: "human", label: "human" }, { value: "alien", label: "alien" }, { value: "unknown", label: "unknown" } ] }, { label: "Status", name: "status", options: [ { value: "all", label: "all" }, { value: "alive", label: "alive" }, { value: "dead", label: "dead" }, { value: "unknown", label: "unknown" } ] }, { label: "Gender", name: "gender", options: [ { value: "all", label: "all" }, { value: "female", label: "female" }, { value: "male", label: "male" }, { value: "genderless", label: "genderless" }, { value: "unknown", label: "unknown" } ] } ]; const handleChange = (e) => { const { name, value } = e.target; onFilterChange(prev => ({ ...prev, [name]: value })); }; return ( <div className="flex flex-row"> {filterConfigs.map((config) => ( <div key={config.name} className="m-4"> <label>{config.label}</label> <select name={config.name} value={filters[config.name]} onChange={handleChange} > {config.options.map((option) => ( <option key={option.value} value={option.value}> {option.label} </option> ))} </select> </div> ))} </div> ); };
Step 2: Use the component in Characters
Now you can replace all the individual select boxes with this one component:
export const Characters = () => { const [characters, setCharacters] = useState([]); const [loading, setLoading] = useState(true); const [filters, setFilters] = useState({ species: "all", status: "all", gender: "all" }); // Same fetch logic as before... if (loading) return <Loading />; return ( <div className="p-4 font-mono text-green-500"> {/* Use the reusable filter component */} <FilterControls filters={filters} onFilterChange={setFilters} /> <h1 className="text-4xl">Characters</h1> <Pagination data={characters} pageLimit={5} /> <div className="grid grid-cols-5 gap-4"> {characters.map((character) => ( <div key={character.id}> <Character character={character} /> </div> ))} </div> </div> ); };
This approach keeps your code clean, maintainable, and easy to extend—if you ever want to add a new filter (like origin), just update the filterConfigs array!
内容的提问来源于stack exchange,提问作者Alice378

