My component re-render after i try to search for an element in a useState array - javascript

I am currently building a React project, so I made a search Input and when I type something in that Input field my hole component re-renders causing an API recall and deleting the text in my Input. I tried merging both the search component with Home component and the same problem appears.
I want my component to call the api only one time, and I am trying to filter the response depending on the input type.
please help!!
Here is my Home component:
import { useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import CountryThumb from '../Components/CountryThumb';
import ThemeContext from '../Components/ColorPalette';
import { Themes } from '../Components/ColorPalette';
import Search from '../Components/Search';
import Filter from '../Components/Filter';
const Grid = styled.main`
width: 100%;
display: grid;
grid-template-columns: repeat(4, 1fr);
column-gap: 60px;
row-gap: 40px;
#media (max-width: 375px) {
grid-template-columns: repeat(1, 1fr);
}
`;
export default function Home() {
const [Countries, setCountries] = useState([]);
const [SearchTerms, setSearchTerms] = useState('');
const { Theme } = useContext(ThemeContext);
const style = Theme == 'light' ? Themes.light : Themes.dark;
useEffect(() => {
getCountries();
}, []);
const Main = styled.main`
display: flex;
flex-wrap: wrap;
padding: 20px 100px;
background-color: ${Theme == 'light' ? style.background : style.background};
#media (max-width: 375px) {
padding: 40px 25px;
}
`;
const getCountries = () => {
axios
.get('https://restcountries.eu/rest/v2/all')
.then((res) => setCountries(res.data))
.catch((err) => console.log(err));
};
return (
<>
<Main>
<Search handleSearch={(e) => setSearchTerms(e.target.value)} />
<Filter />
<Grid>
{Countries.slice(0, 12)
.filter((e) => {
if (SearchTerms == '') {
return e;
} else if (
e.name.toLowerCase().includes(SearchTerms.toLowerCase())
) {
return e;
}
})
.map((e) => (
<CountryThumb props={e} />
))}
</Grid>
</Main>
</>
);
}
And here is my Search component:
import { useContext, useState } from 'react';
import styled from 'styled-components';
import ThemeContext, { Themes } from './ColorPalette';
function Search({ handleSearch }) {
const { Theme } = useContext(ThemeContext);
const style = Theme == 'light' ? Themes.light : Themes.dark;
const Svg = styled.svg`
width: 24px;
height: 24px;
color: ${style.text};
`;
const Wrapper = styled.div`
background-color: ${style.element};
border-radius: 5px;
box-shadow: 0 5px 10px ${style.shadow};
display: flex;
align-items: center;
padding: 0 20px;
margin: 40px 0;
`;
const CInput = styled.input`
border: none;
outline: none;
padding: 15px 120px 15px 20px;
font-size: 1rem;
color: ${style.text};
background: none;
`;
return (
<>
<Wrapper>
<Svg
xmlns='http://www.w3.org/2000/svg'
class='h-6 w-6'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
/>
</Svg>
<CInput
type='text'
name='Search'
onInput={handleSearch}
placeholder='Search for a country ...'
/>
</Wrapper>
</>
);
}
export default Search;

Whenever you change anything in state your component will rerender so it is normal behaviour. However you have dependency array in useEffect that calls api so this function should run only one time, maybe you didnt have array before and forgot to save.

Related

Will I get the latest state value in this case?

I wanted to know whether my onAdd function in <NewItemButton> will get the latest value of text state.
import { useState } from "react";
import {
NewItemFormContainer,
NewItemInput,
NewItemButton
} from "./styles";
type NewItemFormProps = {
onAdd(text: string): void
}
const NewItemForm = (props: NewItemFormProps) => {
const [text, setText] = useState("");
return (
<NewItemFormContainer>
<NewItemInput
value={text}
onChange={(e) => setText(e.target.value)}
/>
<NewItemButton onClick={() => props.onAdd(text)}>
Create
</NewItemButton>
</NewItemFormContainer>
);
}
export default NewItemForm;
If it does not get the latest value, what other ways can you suggest me? One that comes to my mind to use Refs (forwardRef) and send it directly to the html input element and then call props.onAdd with the current value. But the thing is that I am using styled-components and my NewItemInput looks like this
export const NewItemInput = styled.input`
border-radius: 3px;
border: none;
box-shadow: #091e4240 0px 1px 0px 0px;
margin-bottom: 0.5rem;
padding: 0.5rem 1rem;
width: 100%;
background: #484747;
color: #f1f1f1;
`
So how will do that?

Passing Props in SolidJS

I came across something weird while trying to pass props in SolidJS. I've created a store using createStore which I pass through the component tree using Context.Provider. I also have the helper function useStore which lets me access the store anywhere in the component tree (I'm experimenting with React design patterns in SolidJS). I have two components Anime.jsx (parent) and EpisodeList.jsx (child). I'm fetching data when the Anime component mounts and then populate the store with the setter provided by createStore.After which I pass the fetched data to EpisodeList. However, accessing the props of EpisodeList returns an empty proxy (Not sure why, but I think the EpisodeList component isn't re-rendered when store is updated with store.currentAnimeData). I've attached the output below of the console.log statements below.
Any help regarding this would be highly appreciated.
###################################
# Anime.jsx (Parent component)
###################################
const Anime = (props) => {
const [store, setStore] = useStore();
const getAnimeData = async () => {
const currentAnimeId = store.currentAnime.animeId;
const currentAnimeData = await firebase.getAnimeData(currentAnimeId);
setStore(
produce((store) => {
store.currentAnimeData = currentAnimeData;
})
);
};
onMount(() => {
getAnimeData();
});
return (
<>
<div
className={css`
width: 100%;
min-height: 20px;
margin: 8px 0px 5px 0px;
padding: 0px 10px;
box-sizing: border-box;
font-size: 20px;
word-wrap: break-word;
line-height: 1;
`}
>
<span
className={css`
font-size: 20px;
color: #e32451;
`}
>
{"Watching: "}
</span>
{store.currentAnime.name}
</div>
<Search></Search>
<EpisodeList animeData={store.currentAnimeData.episodes} />
</>
);
};
#####################################
# EpisodeList.jsx (child component)
#####################################
const EpisodeList = (props) => {
console.log(props);
console.log(props.animeData);
...... # UI stuff
return (
<div
className={css`
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
padding-bottom: 5px;
margin-top: 10px;
`}
>
<ScrollActionUp onmousedown={[scroll, true]} onmouseup={onmouseup}>
➭
</ScrollActionUp>
<div
className={css`
width: 100%;
height: 432px;
box-sizing: border-box;
padding: 10px 10px 0px 10px;
overflow: hidden;
`}
ref={scrollRef}
>
<For each={animeData.episodes}>
{(episode, index) => {
return (
<Episode status={episode.watched} episode={episode}></Episode>
);
}}
</For>
</div>
<ScrollActionDown onmousedown={[scroll, false]} onmouseup={onmouseup}>
➭
</ScrollActionDown>
</div>
);
};
###############
# store.jsx
###############
import { createContext, createSignal, useContext } from "solid-js";
import { createStore } from "solid-js/store";
const StoreContext = createContext();
export function ContextProvider(props) {
const [store, setStore] = createStore({});
return (
<StoreContext.Provider value={[store, setStore]}>
{props.children}
</StoreContext.Provider>
);
}
export function useStore() {
return useContext(StoreContext);
}

React: Moving component to different div on click

I'm very new to React so any advice would be appreciated on how to move an agent thumbnail to the teamComp div when it is clicked.
I'm also lost as to how to tackle filtering the data through a dropdown menu. Like how would I update the page without refreshing so that only the agents with the selected roles appear.
Anything would help, like I said before, I am a complete beginner to React and feel like I am underutilizing a lot of what makes React powerful.
App.js
import { useEffect, useMemo, useState } from "react";
import AgentCard from "./components/agentCard";
import Select from "react-select"
function App() {
const options = useMemo(
() => [
{value: "controller", label: "Controller"},
{value: "duelist", label: "Duelist"},
{value: "initiator", label: "Initiator"},
{value: "sentinel", label: "Sentinel"},
],
[]
);
const [agentDetails, setAgentDetails] = useState([]);
const getAllAgents = async () => {
const res = await fetch("https://valorant-api.com/v1/agents/");
const results = await res.json();
const agentNames = [],
agentImages = [],
agentRoles = [],
agentDetails = [];
for (let i = 0; i < Object.keys(results["data"]).length; i++) {
if (results["data"][i]["developerName"] != "Hunter_NPE") {
agentNames.push(results["data"][i]["displayName"]);
agentImages.push(results["data"][i]["displayIcon"]);
agentRoles.push(results["data"][i]["role"]["displayName"]);
}
else {
continue;
}
}
for (let i = 0; i < agentNames.length; i++) {
agentDetails[i] = [agentNames[i], [agentImages[i], agentRoles[i]]];
}
agentDetails.sort();
setAgentDetails(agentDetails);
};
useEffect(() => {
getAllAgents();
}, []);
return (
<div className="app-container">
<h2>Valorant Team Builder</h2>
<div className="teamComp">
</div>
<Select options={options} defaultValue={options} isMulti/>
<div id="agent_container" className="agent-container">
{agentDetails.map((agentDetails) => (
<AgentCard
img={agentDetails[1][0]}
name={agentDetails[0]}
role={agentDetails[1][1]}
/>
))}
</div>
</div>
);
}
export default App;
agentCard.js
import React from 'react'
const agentCard = ({role, name, img}) => {
return (
<div className="card-container">
<div className="img-container">
<img src={img} alt={name} />
</div>
<div className="info">
<h3 className="name">{name}</h3>
<small className="role"><span>Role: {role}</span></small>
</div>
</div>
)
}
export default agentCard
index.css
#import url('https://fonts.googleapis.com/css?family=Muli&display=swap');
#import url('https://fonts.googleapis.com/css?family=Lato:300,400&display=swap');
* {
box-sizing: border-box;
}
body {
background: #EFEFBB;
background: -webkit-linear-gradient(to right, #D4D3DD, #EFEFBB);
background: linear-gradient(to right, #D4D3DD, #EFEFBB);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'Lato';
margin: 0;
}
h1 {
letter-spacing: 3px;
}
.agent-container {
display: flex;
flex-wrap: wrap;
align-items: space-between;
justify-content: center;
margin: 0 auto;
max-width: 1200px;
}
.app-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 3rem 0.5rem;
}
.card-container {
background-color: #eee;
border-radius: 20px;
box-shadow: 0 3px 15px rgba(100, 100, 100, 0.5);
margin: 10px;
padding: 20px;
text-align: center;
}
.card-container:hover {
filter: brightness(70%);
transition: all 150ms ease;
}
.img-container img {
margin-top: 1.5rem;
height: 128px;
width: 128px;
}
.name {
margin-bottom: 0.2rem;
}
.teamComp h3 {
float: left;
}
Moving cards
To move a card to a different list you need a new state array that will represent "the members of the team". Something like:
const [team, setTeam] = useState([]);
Render the items in team inside the "teamComp" <div>, the same way you do it in the agent container.
Then add the new function prop to the card and use it in the onClick handler in the card <div>:
<AgentCard
key={agentDetails[0]}
img={agentDetails[1][0]}
name={agentDetails[0]}
role={agentDetails[1][1]}
handleClick={moveToTeam}
/>
...
<div className="card-container" onClick={() => handleClick(name)}>
and in this function, add the agentDetails item to the team state and remove it from the agentDetails state. Make sure that you supply new arrays when setting state:
const moveToTeam = (name) => {
const newTeam = [...team, agentDetails.find((agent) => agent[0] === name)];
const newAgentDetails = agentDetails.filter((agent) => agent[0] !== name);
setTeam(newTeam);
setAgentDetails(newAgentDetails);
};
Filtering
For filtering you need another state that contains all selected options:
const [options, setOptions] = useState(allOptions);
where allOptions is an array of all available options, and it should not change.
Add the onChange handler to the <Select> component:
<Select
options={allOptions}
onChange={(selectedOptions) => setOptions(selectedOptions)}
defaultValue={allOptions}
isMulti
/>
and finally use options to filter cards:
<div id="agent_container" className="agent-container">
{agentDetails
.filter(
(agentDetails) =>
options.filter((option) => option.label === agentDetails[1][1])
.length > 0
)
.map((agentDetails) => (
<AgentCard
key={agentDetails[0]}
img={agentDetails[1][0]}
name={agentDetails[0]}
role={agentDetails[1][1]}
handleClick={moveToTeam}
/>
))}
</div>
You can see the complete example on codesandbox.
I left most of the names in place, although I think using agentDetails for different things is confusing. The data structures can also be improved, but I left them unchanged as well.

React child components state is undefined but can see state using console.log

I have a parent component that gets data from an API end point using fetch. This data displays like it should. The parent component passes an element of an array of objects to the child component. In the child component, when I do a console log I can see the state when it's undefined and when the state is set. The issue that I am having is when I try to access a key of the state (i.e. ticket.title) I get an error saying that ticket is undefined. Any help with would be great.
TicketList
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import TicketDetails from "./TicketDetails"
export default function TicketList() {
const [tickets, updateTickets] = useState([])
const [ticketIndex, updateticketIndex] = useState("0")
useEffect(() => {
async function fetchTickets() {
const response = await fetch("/api/v1/tickets")
const json = await response.json()
updateTickets(json.data)
}
fetchTickets()
}, [])
return (
<Wrapper>
< div >
<TableTitle>
<h3>Tickets</h3>
<button type="submit">Create A Ticket</button>
</TableTitle>
{
tickets.map((ticket, index) => (
<ListInfo key={ticket._id} onClick={() => updateticketIndex(index)}>
<Left>
<p>{ticket.project}</p>
<p>{ticket.title}</p>
<p>{ticket.description}</p>
</Left>
<Right>
<p>{ticket.ticketType}</p>
<p>{ticket.ticketStatus}</p>
<p>{ticket.ticketPriority}</p>
</Right>
</ListInfo>
))
}
</div>
<TicketDetails key={tickets._id} data={tickets[ticketIndex]} />
</Wrapper>
);
}
const Wrapper = styled.div`
display: flex;
background: white;
grid-area: ticketarea;
height: calc(100vh - 4.25rem);
`
const ListInfo = styled.div`
display: flex;
justify-content: space-between;
width: 100%;
padding: .5rem .75rem;
border-bottom: solid 1px #ccc;
`;
const Left = styled.div`
display: flex;
flex: 2;
flex-direction: column;
p {
padding: .25rem;
}
`;
const Right = styled.div`
display: flex;
flex: 1;
flex-direction: column;
align-items: end;
width: 500px;
p {
padding: .25rem;
}
`;
const TableTitle = styled.div`
display: flex;
justify-content: space-between;
padding: 1rem 1rem;
border-bottom: solid 1px #ccc;
button {
padding: .5rem;
}
`;
TicketDetails
import React, { useEffect, useState } from 'react'
// import TicketInfo from './TicketInfo'
import TicketNotes from "./TicketNotes"
import styled from "styled-components"
export default function TicketDetail(data) {
const [ticket, setTicket] = useState(data)
useEffect(() => {
setTicket(data)
}, [data])
console.log(ticket.data)
return (
<Main>
<TicketInfo key={ticket._id}>
<h2>{ticket.title}</h2>
<Info>
<div>
<InfoItem>
<p>Project</p>
<p>{ticket.project}</p>
</InfoItem>
<InfoItem>
<p>Assigned Dev</p>
<p>{ticket.assignedDev}</p>
</InfoItem>
<InfoItem>
<p>Created By</p>
<p>{ticket.submitter}</p>
</InfoItem>
</div>
<div>
<InfoItem>
<p>Type</p>
<p>{ticket.ticketType}</p>
</InfoItem>
<InfoItem>
<p>Status</p>
<p>{ticket.ticketStatus}</p>
</InfoItem>
<InfoItem>
<p>Priority</p>
<p>{ticket.ticketPriority}</p>
</InfoItem>
</div>
</Info>
<Description>{ticket.description}</Description>
</TicketInfo>
<TicketNotes />
<TicketComment>
<textarea name="" id="" cols="30" rows="10" />
<button type="submit">Submit</button>
</TicketComment>
</Main>
)
}
const TicketInfo = styled.div`
margin: .5rem;
h2{
padding: 0.5rem 0;
}
`;
const Description = styled.p`
padding-top: .5rem;
`;
const Info = styled.div`
display: flex;
justify-content: space-between;
border-bottom: solid 1px #ddd;
`;
const InfoItem = styled.section`
margin: .5rem 0;
p:nth-child(1) {
text-transform: uppercase;
color: #ABB1B6;
font-weight: 500;
padding-bottom: .25rem;
}
`;
const Main = styled.div`
background: white;
`
const TicketComment = styled.div`
display: flex;
flex-direction: column;
width: 40rem;
margin: 0 auto ;
input[type=text] {
height: 5rem;
border: solid 1px black;
}
textarea {
border: solid 1px black;
}
button {
margin-top: .5rem;
padding: .5rem;
width: 6rem;
}
`;
There are a few issues here, let's tackle them in order.
Tickets are undefined
When TicketList is mounted, it fetches tickets. When it renders, it immediately renders TicketDetail. The tickets fetch request won't have finished so tickets is undefined. This is why TicketDetail errors out. The solution is to prevent rendering TicketDetail until the tickets are available. You have a few options.
A bare bones approach is to just prevent rendering until the data is available:
{ !!tickets.length && <TicketDetails key={tickets._id} data={tickets[ticketIndex]} />
This uses how logical operators work in JS. In JS falsey && expression returns falsey, and true && expression returns expression. In this case, we turn ticket.length into a boolean. If it is 0 (i.e. not loaded, therefore false), we return false, which React simply discards. If it is greater than 0 (i.e. loaded, therefore true), we render the component.
This doesn't really result in a positive UX though. Ideally this is solved by showing some kind of Loading spinner or somesuch:
{
!!tickets.length
? <TicketDetails . . . />
: <LoadingSpinner />
}
Child data access
In TicketDetail it seems like you meant to destructure data. Currently you are taking the entire prop object and setting it to ticket. Fixing this should resolve the other half of the issue.
Paradigms
You didn't specifically ask for this, but I’d like to back up and ask why you are putting this prop into state? Typically this only done when performing some kind of ephemeral edit, such as pre-populating a form for editing. In your case it looks like you just want to render the ticket details. This is an anti-pattern, putting it into state just adds more code, it doesn’t help you in any way. The convention in React is to just render props directly, state isn't needed.

Pass background as prop in styled-components

I am trying to pass background as a prop in styled-components but I'm not sure how to pass this via my cdn function as it doesn't output correctly in the css
this is the output of the css:
background: url(https://local.dev:5601/pub/media/icons/menu/function (props) { return props.background;}.png);
How can I pass the props via my cdn function?
config.js
export function cdn(path) {
return `https://local.dev:5601/pub/media/${path}`;
}
class Config {
cdn
}
export default Config;
App.js
import React from "react";
import styled from 'styled-components'
const Circle = styled.span`
display: block;
position: relative;
width: 35px;
height: 35px;
cursor: pointer;
border-radius: 50%;
background: url("${cdn('icons/menu/' + (props => props.background) + '.png')}");
margin: 5px;
`
const CircleWrap = styled.div`
display: flex;
flex-wrap: wrap;
padding: 0 0 5px 0;
`
class App extends React.Component {
selectColor = (color) => {
this.props.selectColor(color);
}
render() {
return (
<SettingDrop
title={"Profilfarbe"}
closeDropdown={this.props.closeDropdown}
openDropdown={this.props.openDropdown}
isOpen={this.props.isOpen}
isHidden={this.props.isHidden}
isValid={this.props.isValid}
icon={<ProfilFarbe />}
>
<CircleWrap>
{console.log(this.props.colors)}
{this.props.colors.map( color =>
<Circle
background={color.hash}
onClick={()=>this.selectColor(color.alias)}
>
</Circle>
)}
</CircleWrap>
</SettingDrop>
);
}
};
the following syntax will work:
background: url("${cdn('icons/menu/')}${props => props.background}.svg");

Categories

Resources