How to get single value in react-select dropdown - reactjs

I am trying to create a multi-select dropdown indicator (the second element shown here) using react-select.
The purpose is to show all blog post categories on a blog page, and then to only render the blog posts that are selected in the dropdown indicator.
The tags are extracted from their posts based on a GraphQL query and stored in useState variables "tags" and "renderedPosts".
How do I simply get a value from the dropdown when a category is added or removed? Reading the react-select API, I get this:
getValue () => ReadonlyArray<...>
I don't know how to use that, VS Code simply screams when I try add an arrow function as an attribute in the Select.
I understand there is supposed to be a "value" by default on the Select but if I try to use it I get undefined.
I don't know if mapping to the default value is a problem or if it has anything to do with the ContextProvider (which was necessary). There are other attributes I cannot get to work either, like maxMenuHeight (which is supposed to take a number).
Allow me to share my code (you can probably ignore the useEffect):
export default function Blog({ data }) {
const { posts } = data.blog
const [tags, setTags] = useState([])
const [renderedPosts, setRenderedPosts] = useState([])
// Context necessary to render default options in react-select
const TagsContext = createContext();
// setting all tags (for dropdown-indicator) and rendered posts below the dropdown (initially these two will be the same)
useEffect(() => {
const arr = [];
data.blog.posts.map(post => {
post.frontmatter.tags.map(tag => {
if (!arr.some(index => index.value === tag)) {
arr.push({ value: tag, label: tag })
}
})
});
setTags([arr]);
setRenderedPosts([arr]);
}, []);
function changeRenderedPosts(value???){
setRenderedPosts(value???)
}
return (
<Layout>
<div>
<h1>My blog posts</h1>
<TagsContext.Provider value={{ tags }}>
<Select
defaultValue={tags.map(tag => tag) }
isMulti
name="tags"
options={tags}
className="basic-multi-select"
classNamePrefix="select"
// HOW DO I PASS THE VALUE OF THE ADDED/DELETED OPTION?
onChange={() => changeRenderedPosts(value???)}
maxMenuHeight= {1}
/>
</TagsContext.Provider>
// posts to be rendered based on renderedPosts value
{posts.map(post => {
EDIT: The closest I have now come to a solution is the following:
function changeRenderedTags(options){
console.log(options) //logs the remaining options
setRenderedTags(options) //blocks the change of the dropdown elements
}
return (
<Layout>
<div>
<h1>My blog posts</h1>
<TagsContext.Provider value={{ tags }}>
<Select
...
onChange={(tagOptions) => changeRenderedTags(tagOptions)}
I click to delete one option from the dropdown and I get the other two options in "tagOptions". But then if I try to change "renderedTags", the update of the state is blocked. I find this inexplicable as "setRenderedTags" has nothing to do with the rendering of the dropdown or its data!

with isMulti option true, you get array of options(here it's tags) from onChange callback. so I guess you could just set new tags and render filtered posts depending on the selected tags like below?
const renderedPosts = posts.filter(post => post.tags.some(tag => tags.includes(tag)))
...
onChange={selectedTags => {
setTags(selectedTags ? selectedTags.map(option => option.value) : [])
}}
...

I finally solved it - I don't know if it is the best solution but it really works! Sharing it here in case anyone else is in the exact same situation, or if you are just curious. Constructive criticism is welcome.
export default function Blog({ data }) {
const { posts } = data.blog
const [tags, setTags] = useState([])
const [renderedTags, setRenderedTags] = useState([])
const TagsContext = createContext();
useEffect(() => {
const arr = [];
posts.map(post => {
post.frontmatter.tags.map(tag => {
if (!arr.some(index => index.value === tag)) {
arr.push({ value: tag, label: tag })
}
})
});
setTags([...arr]);
}, [posts]);
useEffect(() => {
setRenderedTags([...tags]);
}, [tags])
return (
<Layout>
<div>
<h1>My blog posts</h1>
<TagsContext.Provider value={{ tags }}>
<Select
defaultValue={tags}
isMulti
name="tags"
options={tags}
className="basic-multi-select"
classNamePrefix="select"
onChange={(tagOptions) => setRenderedTags(tagOptions ? tagOptions.map(option => option) : [])}
value={renderedTags}
/>
</TagsContext.Provider>
{posts.map(post =>
(post.frontmatter.tags.some(i => renderedTags.find(j => j.value === i))) ?
<article key={post.id}>
<Link to={post.fields.slug}>
<h2>{post.frontmatter.title}</h2>
<p>{post.frontmatter.introduction}</p>
</Link>
<small>
{post.frontmatter.author}, {post.frontmatter.date}
</small>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
: null
)}
</div>
</Layout>
)
}

Related

React when parent component re-render, I loose child state. What I'm missing?

I'm sure I'm missing something about how state works in React.
My component GenericApiFilter is a list of checkboxes, result of API call:
State filters: filters available, result of API call in useEffect()
State selected: list of selected filters by the user
Prop onChange: invoked when the selection changes
export const GenericFilter = ({
apiUrl,
apiParams,
onChange = () => {},
}: GenericFilterProps) => {
const [filters, setFilters] = useState<Filter[]>([]);
const [selected, setSelected] = useState<Filter[]>([]);
useEffect(() => {
axios.get(apiUrl, { params: apiParams }).then(res => setFilters(res.data));
}, [apiUrl, apiParams]);
return (
<>
{filters.map(filter =>
<div key={filter.id}>
<label>{filter.name} ({filter.count})</label>
<input
type='checkbox'
checked={selected.includes(filter)}
disabled={!filter.enabled}
onChange={({ target: { checked } }) => {
const selection = (checked ? [...selected, filter] : selected.filter(f => f !== filter));
setSelected(selection);
onChange(selection);
}}
/>
</div>
)}
</>
);
}
The parent FiltersPanel uses the GenericFilter. What's the problem?
I check one or more checkbox and selection is retained
When I update the state calling setSizeParams (click on the button),i lost the selection: the filter re-renders without retaining the state
export const FiltersPanel= () => {
const [sizeParams, setSizeParams] = useState({});
return (
<>
<button className='btn btn-primary' onClick={() => {
setSizeParams(prev => ({ ...prev, time: Math.random() }));
}}>Update size params</button>
<GenericFilter apiUrl='/api/filter/size' apiParams={sizeParams} />
</>
);
};
What I'm missing here?
The problem is not about React, is about JS:
selected.filter(f => f !== filter));
Here you are comparing objects, and this will match just if you are comparing the same objects. As soon as you render the component again, those object change and the comparison will always return false.
Solution: work with primitive values (e.g. the id), and it should work:
return (
<>
{filters.map((filter) => (
<div key={filter.id}>
<label>
{filter.name} ({filter.count})
</label>
<input
type="checkbox"
checked={selected.includes(filter.id)}
disabled={!filter.enabled}
onChange={({ target: { checked } }) => {
const selection = checked
? [...selected, filter.id]
: selected.filter((f) => f !== filter.id);
setSelected(selection);
onChange(selection);
}}
/>
</div>
))}
</>
);
This is the problem
checked={selected.includes(filter)}
You are checking with includes against an object filter.
But when effect fires again in that component, the new filter objects arrive, hence that above includes check won't work anymore.
So you should use something like an id instead of storing object references.

Conditional rendering of React component if json key exists

I am trying to conditionally render a component with bad results. I have read many tutorials and Stack Overflow questions but I can't get this work. Could you help me?
The conditional component is a data visualization geographical map which should be rendered only when a fetched json file has "code" key. In other words I have dozens of jsons and some of them include geo map information but not all. I have been trying boolean and different kind of ternary operators in jsx but every time when mapless item is clicked in sidebar React tries to render Map child component and gives an error that "code" (key) is undefined. What could be reason for that? Below is my code:
App.js
function App() {
const [files, setFiles] = useState([]);
const [items, setItems] = useState([]);
const [product_id, setProduct_id] = useState(13);
const [mapCode, setMapcode] = useState(0);
useEffect(() => {
fetch("/sidebar")
.then((res) => res.json())
.then((data) => {
setItems(data);
});
}, []);
useEffect(() => {
fetch(`/data/${product_id}`)
.then((res) => res.json())
.then((data) => {
setFiles(data);
if ("code" in data[0]) setMapcode(1);
else setMapcode(0);
console.log(mapCode);
});
}, [product_id]);
function HandleSelection(e) {
setProduct_id(e);
}
Inside useEffect if fetched product data includes "code" key I change mapCode every time an item (product) is clicked in sidebar. console.log(mapCode) gives right kind of results.
Below is the essential code inside return() of App.js. There are couple of ways I have tried to get the conditional rendering work.
<div className="col-6">
<Files files={files} />
</div>
<div className="col-3">
{/*Boolean(mapCode) && <Map files={files} />*/}
{/*mapCode === true && <Map files={files} />*/}
mapCode === true ? <Map files={files} /> : <div>No map</div>
</div>
I have been wondering if useEffect is the right place to use setMapcode(0) and setMapcode(1)?
First you should set a boolean value to isMapCode instead of a number:
const [isMapCode, setIsMapcode] = useState(false);
useEffect(() => {
fetch(`/data/${product_id}`)
.then((res) => res.json())
.then((data) => {
setFiles(data);
setIsMapcode(data[0].code !== undefined);
});
}, [product_id]);
Therefore you should check its value inside a scope ({}):
<div className="col-3">
{isMapCode ? <Map files={files} /> : <div>No map</div>}
</div>
Notice that you don't need another state for it, you can check your files state:
const isMapCodeAvailable = data[0].code !== undefined;
<div className="col-3">
{isMapCodeAvailable ? <Map files={files} /> : <div>No map</div>}
</div>

Removing object from one array while simultaneously adding it to a different array - React hooks

I'm working on a Tinder-like app and trying to remove the current card from the array and move on to the next when clicking either the like or dislike button. Simultaneously, I am trying to add the card to a new array (list of liked or disliked). Adding the object to new array seems to work (although there's a delay and the button needs clicked twice - which also needs to be sorted), but as soon as I try to remove it from the current array it all crashes.
I tried looking at this solution: Removing object from array using hooks (useState) but I only ever get "TypeError: Cannot read property 'target' of undefined" no matter what I try. What am I missing?
This is the code:
import React, { useState, useEffect } from 'react';
import { Card, Button, Container } from 'react-bootstrap';
const url = 'https://swiperish-app.com/cards';
const SwiperCard = () => {
const [cardData, setCardData] = useState([]);
const [likedItem, setLikedItem] = useState([]);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(cardData => setCardData(cardData))
});
const handleRemoveItem = (event) => {
const name = event.target.getAttribute("name")
setCardData(cardData.filter(item => item.id !==name));
};
const likedCards = (itemId, itemImg, ItemTitle) => {
let likedArr = [...likedItem];
setLikedItem(likedItem => likedItem.concat({itemId, itemImg, ItemTitle}))
handleRemoveItem();
console.log(likedArr);
};
return (
<div id="contentView">
{cardData.map((item, index) => {
return(
<Card key={index} className="cardContainer" name={item.id}>
<Container className="btnContainer">
<div className="btnWrapper">
<Button className="btn" onClick={() => console.log(item.id)}>DISLIKE</Button>
</div>
</Container>
<Container className="cardContentContainer">
<Card.Img style={{width: "18rem"}}
variant="top"
src={item.image}
fluid="true"
/>
<Card.Body style={{width: "18rem"}}>
<Card.Title className="cardTitle">{item.title.toUpperCase()}</Card.Title>
<Card.Subtitle className="cardText">{item.body}</Card.Subtitle>
</Card.Body>
</Container>
<Container className="btnContainer">
<div className="btnWrapper">
<Button className="btn" onClick={() => likedCards(item.id, item.image,item.title) }>LIKE</Button>
</div>
</Container>
</Card>
)
})}
</div>
);
};
export default SwiperCard;
You can move cards between two arrays with
const likedCards = (item) => {
setLikedItem([...likedItem, item]);
let filtered = cardData.filter((card) => card.itemId !== item.itemId);
setCardData(filtered);
};
I suggest you to add empty array as second parameter of useEffect,since you are using as componentDidMount.
As second suggestion you can setLoading true before fetch and setLoading false after to reduce errors in render.
You're calling handleRemoveItem with no arguments, but that function is doing something with an event parameter, so you're going to get a TypeError.
It seems like handleRemoveItem really only needs to know about the item ID to remove, so you can simplify to:
const removeCard = id => {
setCardData(cardData.filter(item => item.id !== id));
};
const handleLike = (itemId, itemImg, ItemTitle) => {
setLikedItem([...likedItem, {itemId, itemImg, ItemTitle}]);
removeCard(itemId);
};
I also noticed that you're sometimes logging a state variable immediately after calling the setting. That won't work. It's not until the next call to useState on the next render when you'll receive the value, so if you want to log changes to state, I'd log in your render function, not in an event handler.

Change css class of a component by onClick on Icon in React using useState hook

I am new to React and I was hoping someone could help me with this issue. I am trying to render some images called 'cards' from an array based on the same data I've received from Axios. I basically need to render an array of card props which have an <i> tag with some font-awesome class attached to them. When I click on the "fa-search-plus" font-awesome icon, I want the parent of this icon <div> to trigger the onClick such that the css property of the sibling <img> of this <div> can be changed. For some reason with the following code, this does not seem to happen. Any fix is appreciated. Thanks!
const GameCards = (cards) => {
const [cardimgclass, setCardimgclass] = useState(true);
const onClick = (e) => {
e.preventDefault();
setCardimgclass(!cardimgclass);
};
const loadCardsByCategory = (cards) => {
var allCards = [];
if (cards)
cards.forEach((item, i) => {
allCards.push(
<div key={item._id} className="card-container">
<img
className={cardimgclass ? "card-reg" : "card-big"}
src={item.src}
alt="No file"
/>
<div onClick={(e) => onClick(e)}>
{" "}
<i className="fas fa-search-plus"></i>
</div>
</div>
);
});
return allCards;
};
const loadCards = (cards) => {
return (
<Fragment>
<div className="cardgallery">{loadCardsByCategory(cards)}</div>
</Fragment>
)
};
const loadCardsUsingMemo = useMemo(() => loadCards(cards), [cards]);
return <Fragment>{loadCardsUsingMemo}</Fragment>;
};
It looks like your primary problem is that you are not destructuring your props object:
const GameCards = cards => {
You need to change that to:
const GameCards = ({ cards }) => {
Also, remove the useMemo stuff. It is not helping you here. Here is a slimmed down version of your code. I'm changing the background-color property with the class, but the concept is the same. Hope that helps!
EDIT: Also note, as in the example your logic is currently changing all of the elements. If you want to only modify the class for one element you could use the method of passing the index (or better yet, the ID!). Here is an example:
const [cardimgclass, setCardimgclass] = useState();
...
const onClick = (e, item) => setCardimgclass(item._id)
...
<div onClick={e => onClick(e, item)} />

How would I only show data when searched in input?

I'm learning react and am making a simple app that allows users to search for countries from an api.
I have successfully fetched the api and set it to some state and created a filter component for searches in a text input.
However I would like to show nothing until the user types in the search input which would then show the countries matching the letters typed.
What is the most used way of doing this?
const App = () => {
const [countries, setCountry] = useState([])
const [search, setSearch] = useState('')
useEffect(() => {
console.log('effect')
axios
.get(`https://restcountries.eu/rest/v2/all`)
.then(response => {
console.log(response,'promise fulfiled')
setCountry(response.data)
})
}, [])
const handleSearch = (e) => {
setSearch(e.target.value)
}
const filter = countries.filter(country =>
country.name.toLowerCase().includes(search.toLowerCase())
);
return (
<div className="App">
Search Country:
<input type='text' onChange={handleSearch}/>
<Country filter={filter} />
</div>
);
}
const Country = ({filter}) => {
return(
<ul style={{listStyle: 'none'}}>
{
filter.map(country =>
<li key={country.name}>
{country.name}
</li>)
}
</ul>
)
This is the current code, however it shows all countries from the api. I would like to show no countries until user types in input box.
Thanks.
Just have a conditional render for <Country />.
Only display the list of countries if search is supplied.
{ search && <Country filter={filter} /> }
Demo

Resources