Creating dynamic responses to button clicks in React - javascript

I've been asked to create a FAQ page with a chatbot style. The way I'm trying to do this is by creating a paragraph element with some text along the lines of "Hello, ask me a question!" with five buttons in a row underneath containing the FAQs. The idea is that the user should be able to click on one of the buttons and reveal the answer to the question in a new paragraph element and the remaining 'unanswered' questions should appear in a row of buttons below that.
So far I've only managed to create an accordion style page where the questions are in a column and the answer is revealed when they're clicked on. This works but I'd really like to try to meet my brief and get a better understanding of React at the same time. My current code is below - any help would be appreciated!
import React, { useState } from 'react';
import classes from './Chat.module.css';
import { Container, Row, Col } from 'react-bootstrap';
const ChatItem = (props) => {
const [isOpen, setIsOpen] = useState(false);
const clickHandler = () => {
setIsOpen(true);
console.log(isOpen);
};
return (
<Container>
<Row md={5}>
<button className={classes.conversation_btn} onClick={clickHandler}>
{props.question}
</button>
</Row>
<Row>
<Col mdPush={7} md={5}>
<div
className={`${classes.text_box} ${
isOpen ? classes.open : classes.closed
}`}
>
{props.answer}
</div>
</Col>
</Row>
</Container>
);
};
const ChatList = (props) => {
return (
<div>
{props.items.map((item) => (
<ChatItem
key={item.id}
id={item.id}
question={item.question}
answer={item.answer}
/>
))}
</div>
);
};
export default ChatList;

Related

Why do 2 same div container produce different result when navigating through keyboard on radio button?

I would like to wrap radio button group with 200px width. And I expect keyboard navigation to behave the same, which is to move between radio buttons through right/left arrow back and forth. Weird thing is if I implement parent div by wrapping this Container component const Container = ({ children }) => ( <div style={{ width: "200px" }}>{children}</div>);, after moving to different radio button once, it stops. I can't no longer navigate radio buttons. But if I implement parent div using plain html code <div style={{ width: "200px"}}>, keyboard navigation works fine. When I inspect using dev tools, semantically, they both appear the same. So I'm having trouble figuring out why Container component impacts keyboard navigation while hard coding in html doesn't. Anybody knows why? https://codesandbox.io/s/radio-button-example-3t8l80?file=/App.js
import { Stack, RadioButton } from "#shopify/polaris";
import { useState, useCallback } from "react";
function RadioButtonExample() {
const [value, setValue] = useState("disabled");
const handleChange = useCallback(
(_checked, newValue) => setValue(newValue),
[]
);
const Container = ({ children }) => (
<div style={{ width: "200px" }}>{children}</div>
);
return (
<Container> // keyboard navigation doesn't work properly with this
{/* <div style={{ width: "200px"}}> */} // keyboard navigation works fine
<Stack vertical>
<RadioButton
label="Accounts are disabled"
helpText="Customers will only be able to check out as guests."
checked={value === "disabled"}
id="disabled"
name="accounts"
onChange={handleChange}
/>
<RadioButton
label="Accounts are optional"
helpText="Customers will be able to check out with a customer account or as a guest."
id="optional"
name="accounts"
checked={value === "optional"}
onChange={handleChange}
/>
</Stack>
{/* </div> */}
</Container>
);
}
export default RadioButtonExample;
The reason for this to happen is because your Container component is part of the RadioButtonExample component. Every time a state or prop gets updated, the Container component gets created as a new component. To prevent this, take your Container component outside of the RadioButtonExample like this:
import { Stack, RadioButton } from "#shopify/polaris";
import { useState, useCallback } from "react";
const Container = ({ children }) => (
<div style={{ width: "200px" }}>{children}</div>
);
function RadioButtonExample() {
const [value, setValue] = useState("disabled");
const handleChange = useCallback(
(_checked, newValue) => setValue(newValue),
[]
);
return (
<Container>
<Stack vertical>
<RadioButton
label="Accounts are disabled"
helpText="Customers will only be able to check out as guests."
checked={value === "disabled"}
id="disabled"
name="accounts"
onChange={handleChange}
/>
<RadioButton
label="Accounts are optional"
helpText="Customers will be able to check out with a customer account or as a guest."
id="optional"
name="accounts"
checked={value === "optional"}
onChange={handleChange}
/>
</Stack>
</Container>
);
}
export default RadioButtonExample;
A different solution would be to wrap the container in a useMemo to make sure it stays the same. This would give you something like this:
const Container = useMemo(() => ({ children }) => (
<div style={{ width: "200px" }}>{children}</div>
), []);
Both will work, but I would suggest going for the first solution.

how i get querySelectorAll html elements in react?

I am using react with functional component. I also use react-bootstrap , I attached the react-bootstrap carousel , everything is fine with react-bootstrap, for some functionality, I want to get three divs in react , divs has same classes. I know if I use simple html css js then I easily do that,
querySelectorAll(".indicator-btn")
but i want this on react. How can I do that? please suggest me a best practices for that.
This is a my component , in the last i have three divs which i want for Dom.
import React, { useState } from "react";
import Cardcarousel from "./Cardcarousel";
import { Carousel } from "react-bootstrap";
import Brand1img from "../images/brand1.png";
import Airbnbimg from "../images/Airbnb.png";
import Microsoft from "../images/microsoft.png";
import { Row, Col } from "react-bootstrap";
function Carouselx() {
const [index, setIndex] = useState(0);
const handleSelect = (selectedIndex, e) => {
console.log(selectedIndex, "e=>", e);
setIndex(selectedIndex);
};
const isSlide = (slide) => {
console.log("slide no => " + slide);
console.log(this);
};
return (
<>
<div className="px-5 py-3">
<Carousel activeIndex={index} onSelect={handleSelect} onSlide={isSlide}>
<Carousel.Item>
<Row>
<Col>
<Cardcarousel
name="Technology UI/UX"
companylogo={Brand1img}
type="Full time job"
/>
</Col>
<Col>
<Cardcarousel
name="Technology Backend developer"
companylogo={Airbnbimg}
type="Part time job"
/>
</Col>
<Col>
<Cardcarousel
name="Technology Backend developer"
companylogo={Microsoft}
type="Internee"
/>
</Col>
</Row>
</Carousel.Item>
<Carousel.Item>
<Row>
<Col>
<Cardcarousel
name="Technology UI/UX"
companylogo={Brand1img}
type="Full time job"
/>
</Col>
<Col>
<Cardcarousel
name="Technology Backend developer"
companylogo={Airbnbimg}
type="Part time job"
/>
</Col>
<Col>
<Cardcarousel
name="Technology front end developer"
companylogo={Microsoft}
type="Internee"
/>
</Col>
</Row>
</Carousel.Item>
</Carousel>
<div className="carusel-indecator-container">
<div className="indicator-btn indicator-btn1"></div>
<div className="indicator-btn indicator-btn2"></div>
<div className="indicator-btn indicator-btn3"></div>
</div>
</div>
</>
);
}
export default Carouselx;
You could use useState to get its refs (also useRef but there'll be a lot more inconveniences potentially).
const [btn1, setBtn1] = useState();
const [btn2, setBtn2] = useState();
const [btn3, setBtn3] = useState();
...
<div ref={setBtn1} className="indicator-btn indicator-btn1"></div>
<div ref={setBtn2} className="indicator-btn indicator-btn2"></div>
<div ref={setBtn3} className="indicator-btn indicator-btn3"></div>
At some point (after 1st rendering) you'll have btn1, btn2, btn3 as button elements. If you want to use it, assumingly imperatively, you could useEffect
useEffect(() => {
if (!btn1 || !btn2 || !btn3) return;
// do something with these refs i.e. make them jquery! $(btn1)
}, [btn1, btn2, btn3]);

More than needed React components re-rendering when typing in input

I am taking input from a search input field using searchInput and setSearchInput useState hook and after I press submit button, I call fetchSearchData function providing it the input text and setCompanies hook, where companies are updated with the fetched list of companies from the API.
Then companies are passed to another component CompanyList where a map function is called there.
The problem is whenever I type in the search field, the CompanyList component is re-rendered although I did not press submit. I understand that setSearchInput will re-render SearchBar component whenever I type in it, but I don't get why CompanyList re-renders.
Search page source code:
const Search = () => {
const [companies, setCompanies]=useState([]); //List of companies returned from searching
const [searchInput, setSearchInput] = useState(""); //Search field input
//Update search text whenever the user types in
const onSearchChange = (e) => {
setSearchInput(e.target.value)
}
//use the API providing it the search input, and
//setCompanies hook to update list of companies
const onSearchSubmit = (e) => {
e.preventDefault()
fetchSearchData(searchInput, setCompanies)
}
return (
<div>
<Container>
<Row className={"searchFilterBar"}>
<Col sm={6} md={8} className={"searchBar"}>
<SearchBar onSubmit={onSearchSubmit} onChange={onSearchChange} value={searchInput} />
</Col>
<Col sm={6} md={4} className={"filterBar"}>
</Col>
</Row>
<CompanyList companies={companies} ></CompanyList>
<Row>
</Row>
</Container>
</div>
)
}
export default Search;
SearchBar component source code:
const SearchBar = ({value,onSubmit, onChange}) => {
return (
<Form
className="search-form"
onSubmit={onSubmit}
>
<div className="input-group">
<span className="input-group-text rubik-font">
<i className="icon ion-search"></i>
</span>
<input
className="form-control rubik-font"
type="text"
placeholder="Search for companies that start with..."
onChange={onChange}
value={value}
/>
<Button className="btn btn-light rubik-font" type="submit">Search </Button>
</div>
</Form>
)
}
CompanyList component source code:
function MapDataToCompanyList(response) {
console.log(response); //Logging occurs here
if(!response || response===undefined || response.length===0)
{
return (<ErrorBoundary message={noCompaniesError.message}></ErrorBoundary>)
}
return response.map((company) => {
return (
<Col key={company._id} xs={12} md={6} lg={4} className="mt-2">
<CompanyCard
id={company._id}
logo={company.logo}
title={company.name}
logoBackground={company.logoBackground}
progLangs={company.progLangs}
backend={company.backend}
frontend={company.frontend}
url={company.url}
>
</CompanyCard>
</Col>
)
})
}
const CompanyList = (props) => {
const {companies} = props
return (
<div>
<Container className="mt-3">
<Row>
{
MapDataToCompanyList(companies)
}
</Row>
</Container>
</div>
)
}
export default CompanyList;
FetchSearchData function source code:
export const fetchSearchData = (query, cb)=>{
const uri = process.env.NODE_ENV === 'development' ?
`http://localhost:3000/api/companies/name/${query}` :
``;
axios.get(uri, {
timeout: MAX_TIMEOUT
})
.then((response)=>{
cb(response.data.data)
})
.catch((error)=>{
console.log(error)
})
}
As seen above, empty list of companies is logged when the page first loads, then I typed three characters and the it logged three time which means the map function called three times.
Even then if I pressed submit and retrieved list of companies normally, whenever I type it will keep printing the array of companies that was fetched.
Sorry if I missed something, I am still new to React.
When you call setSearchInput(e.target.value), Search component will re-render cause its state has changed. Search component re-renders means every tag nested in it will re-render (except the ones passed via children). That is the normal behaviour of React. If you want to avoid that, you would wanna use React.memo for CompanyList. Or you could use useRef to bind the input like so:
const Search = () => {
const [companies, setCompanies] = useState([]); //List of companies returned from searching
const inputRef = React.useRef(null);
//use the API providing it the search input, and
//setCompanies hook to update list of companies
const onSearchSubmit = (e) => {
e.preventDefault();
fetchSearchData(inputRef.current.value, setCompanies);
inputRef.current.value = "";
};
return (
<div>
<Container>
<Row className={"searchFilterBar"}>
<Col sm={6} md={8} className={"searchBar"}>
<SearchBar inputRef={inputRef} onSubmit={onSearchSubmit} />
</Col>
<Col sm={6} md={4} className={"filterBar"}></Col>
</Row>
<CompanyList companies={companies}></CompanyList>
<Row></Row>
</Container>
</div>
);
};
export default Search;
const SearchBar = ({ onSubmit, inputRef }) => {
return (
<Form className="search-form" onSubmit={onSubmit}>
<div className="input-group">
<span className="input-group-text rubik-font">
<i className="icon ion-search"></i>
</span>
<input
ref={inputRef}
className="form-control rubik-font"
type="text"
placeholder="Search for companies that start with..."
/>
<Button className="btn btn-light rubik-font" type="submit">
Search
</Button>
</div>
</Form>
);
};
I don't get why CompanyList re-renders.
Because it's nested in your Search component, and it's not React.memo'd (or a PureComponent).
Yes, the component is updated, but that doesn't mean it necessarily causes a DOM reconciliation.
In any case, React is completely at liberty of calling your component function as many times as it likes (and indeed, in Strict Mode it tends to call them twice per update to make sure you're not doing silly things), so you should look at side effects (such as console logging) in your component function (which you shouldn't have in the first place) as performance guidelines.
You do not need to maintain a state for input field. You can use useRef and pass it to input like below.
<input
ref={inputRef}
className="form-control rubik-font"
type="text"
placeholder="Search for companies that start with..."
/>
And you can get get value inside onSearchSubmit using inputRef.current.value
This will not re-render you component on input change.

Create a new row every three columns

I am trying to create a virtual shop and I want to make every row of products have four items on a large screen, three in medium, and two in smell.
My problem is that I can’t come up with a way to make it that every four items I iterate the item list a new row will be created.
(I am getting the data from an API I created with Flask, the getting the data part works.)
Here is my code:
import React, { useState, useEffect } from "react";
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
function ShopItems () {
const [items, setItems] = useState([]);
useEffect(() => {
fetch('/api/items').then(res => res.json()).then(data => {
setItems(data.items);
});
}, []);
return(
<Container fluid>
<Row xs={6} md={4} lg={3}>
{
items.map((item) => (
<Col key={item.id}>{item.name}</Col>))
}
</Row>
</Container>
)
}
export default ShopItems;
You are using props incorrectly.
<Container fluid>
<Row>
<Col xs={6} md={4} lg={3}></Col>
</Row>
</Container>

How to show/hide component in parent component via a button in a child component when using a map of buttons?

A bit of a mouthful question...
I've looked over dozen of similar questions, but they either have to do with components in the same file, or with singular buttons.
Desired Effect:
I'm trying to display repository commits once a user clicks on a card of a specific repo. The commits are hidden until they press a button. If the user clicks the button when the commits are showing, it should hide the commits. Continuous clicking of a repo card should show/hide/show/hide/etc. the appropriate commit cards.
function Home() {
const title = "Projects"
const message = "Projects fetched from Github using their GQL API."
const [clicked, setClicked] = useState(false)
const [repoInfo, setRepoInfo] = useState({ name: "Website", owner: "FernandoH-G" })
return (
<Container>
<Jumbo title={title} message={message} />
<CardDeck>
<RepoCards clicked={clicked} setClicked={setClicked} setRepoInfo={setRepoInfo} />
</CardDeck>
<br/>
<CardDeck>
{clicked && <CommitCards repoInfo={repoInfo} />}
</CardDeck>
</Container>
);
}
function RepoCards(props) {
const { loading, error, data } = useQuery(GET_PINNED_REPOS);
const [radioValue, setRadioValue] = useState("");
if (loading) return (
<Loading
message="Fetching pinned repositories..."
color="secondary" />
)
if (error) return (
<Loading
message="Error fetching pinned repositories."
color="danger" />
)
const pinEdges = data.user.pinnedItems.edges
return (
pinEdges.map((pin, idx) => (
<Card
key={pin.node.name}
className="text-center"
border="light">
<Card.Body>
<Card.Header as="h5"> {pin.node.name}</Card.Header>
<Card.Link href={pin.node.url}>
<Card.Img variant="top" src={chooseIMG(pin.node.name)} />
</Card.Link>
<Card.Text> {pin.node.description}</Card.Text>
</Card.Body>
<ButtonGroup toggle>
<ToggleButton
key={pin.node.name}
type="radio"
variant="outline-secondary"
name="Button Radio"
value={pin.node.name}
checked={radioValue === pin.node.name}
onChange={(e) => setRadioValue(e.currentTarget.value)}
onClick={() => {
console.log(`${idx} card clicked. click value: ${props.clicked}`)
props.setRepoInfo(
{
name: pin.node.name,
owner: pin.node.owner.login
})
props.setClicked(!clicked) // This should work,right?
// if (radioValue === pin.node.name) {
// // props.setClicked(prevVal => !prevVal)
// props.setClicked(false)
// } else {props.setClicked(true)}
}}>
Last Update:{' '}
{parseDate(pin.node.pushedAt)}
</ToggleButton>
</ButtonGroup>
</Card>
))
)
}
I'm including the CommitCards component code even though I don't think it's necessary.
const CommitCards = (props) => {
const name = props.repoInfo.name
const owner = props.repoInfo.owner
const { loading, error, data } = useQuery(GET_REPO_COMMITS, {
variables: { name, owner },
});
if (loading) return (
<Loading message={`Fetching ${name} commits...`} color="secondary" />
);
if (error) return (
<Loading message={`Error fetching ${name} commits.`} color="danger" />
);
const commits = data.repository.defaultBranchRef.target.history.edges
return commits.map(com => (
<Card
key={com.node.url}
bg={"dark"}
style={{ color: "white" }}
border="info">
<Card.Body>
<Card.Link href={com.node.url}>
Commit Date:{'\n'}
{parseDate(com.node.committedDate)}
</Card.Link>
<Card.Text>
{parseText(com.node.message)}
</Card.Text>
</Card.Body>
</Card>
))
}
After looking at the console, and recording the state of clicked, it may be a rendering issue. I say this because it seems to be rendering everything twice, thus messing with the value of clicked.
I don't know. I've been on this for a few days now. Below is a screenshot of the consoles output after I click each button. Commits fail to even show; not close to having the effect I desire.
Huge development
Following the suggestion of #deckele, I was working on a code sandbox when I noticed that my initial code posted...actually works, sorta. I can definitely optimize the posted code. I wasn't able to get react-bootstrap to work in code sandbox, but that turned out for the better.
It turns out that a <ToggleButton> reads clicking the label, the text portion, as a click on the console, twice. However, you have to hit the very small actual input area. You can check out the label registering as a click in the code sandbox.
Unfortunately, the input area is covered by the label, which is essentially the whole button.
Since this question took a turn, I will be closing up this one, and probably opening up a new react-bootstrap one.
There are probably more ways to get this working.
I think you can get away without using clicked state in Home - if I'm not missing something I'm not sure why you would need it
You could:
1. In Home
keep only repoInfo and get rid of clicked
initiate repoInfo with null or the value you want to be loaded on mount
pass repoInfo and setRepoInfo down to RepoCards
change the clicked check with reportInfo
function Home() {
const title = 'Projects';
const message = 'Projects fetched from Github using their GQL API.';
const [repoInfo, setRepoInfo] = useState({
name: 'Website',
owner: 'FernandoH-G',
}); // if you initiate repoInfo with `null` you won't have a repo selected in mount
return (
<Container>
<Jumbo title={title} message={message} />
<CardDeck>
<RepoCards
setRepoInfo={setRepoInfo}
reportInfo={repoInfo}
/>
</CardDeck>
<br />
<CardDeck>{repoInfo && <CommitCards repoInfo={repoInfo} />}</CardDeck>
</Container>
);
}
2. In RepoCards
get rid of radioValue - you won't use it; you can determine the checked value using repoInfo
remove onClick from ToggleButton and use just onChange changed to something like
<ToggleButton
key={pin.node.name}
type="radio"
variant="outline-secondary"
name="Button Radio"
value={pin.node.name}
checked={props.reportInfo && props.reportInfo.name === pin.node.name}
onChange={() => {
const newReportInfo =
props.reportInfo && props.reportInfo.name === pin.node.name
? null
: {
name: pin.node.name,
owner: pin.node.owner.login,
};
props.setRepoInfo(newReportInfo);
}}
>
Last Update: {parseDate(pin.node.pushedAt)}
</ToggleButton>;

Categories

Resources