How to conditionally reset state count with react hooks - javascript

After the user submits my form, the handleSubmit function runs and counts the number of li elements with class names containing "show" in my div showing all search results. It adds these counts all up with setStoreCount(prevCount => prevCount + 1) and I update my FAQ {searchCount} UI showing the number of search results with
useEffect(() => {
setSearchCount(storeCount) // collect all storeCount
}, [storeCount])
The problem: Say that the first search gets 5 results.
Then the UI would show FAQ 5. If you search the same thing again, you still get 5 results and would expect the UI to still show FAQ 5 but because the storeCount cannot reset to 0 it shows FAQ 10 (5 + 5) and then FAQ 15 (5 + 5 + 5) and so on.
Because my useEffect conditionally runs whenever storeCount changes, I cannot reset it to 0 because then I would always show FAQ 0.
How do I show the number of search results while refreshing the count on every search in the code below?
import React, { useState, useRef, useEffect } from 'react'
import ContentsWrap from '../../components/contents-wrap'
import FrequentList from './tabs/frequent'
import EstimateList from './tabs/estimate'
import ReturnList from './tabs/return'
import EtcList from './tabs/etc'
import SubsidiaryList from './tabs/subsidiary'
import ServiceList from './tabs/service'
import InList from './tabs/in'
import OutList from './tabs/out'
import styles from './index.module.scss'
export default function FAQ () {
const [tab, setTab] = useState('frequent')
const [storeText, setStoreText] = useState('') // stores searchbar text as user types
const [searchText, setSearchText] = useState('') // sends searchbar text to List component props on form submit
const [storeCount, setStoreCount] = useState(0) // stores count of each li element className that includes 'show'
const [searchCount, setSearchCount] = useState(0) // shows search count in UI
const searchContainer = useRef(null)
const handleSubmit = e => {
e.preventDefault()
setSearchText(storeText)
setTab('all')
setTimeout(() => {
// gets all <ul> children of <div class = "faq-list-wrap">
for (let i = 0; i < searchContainer.current.children.length; i++) {
// gets all <li> children of each <ul>
for (let j = 0; j < searchContainer.current.children[i].children.length; j++) {
// checks if each li class name has 'show'
if (searchContainer.current.children[i].children[j].className.includes('show')) {
console.log('count added')
setStoreCount(prevCount => prevCount + 1)
}
}
}
}, 100) // setTimeOut needed to target searchContainer after search and not before
}
const handleChange = newId => {
setTab(newId)
setStoreText('') // clear input value on tab click
setSearchText('') // show all search items on tab click
}
useEffect(() => {
setSearchCount(storeCount) // collect all storeCount
}, [storeCount])
return (
<ContentsWrap>
<div className="content-wrap content-width">
{/* <!-- S: faq-wrap --> */}
<div className="faq-wrap">
<div className="faq-title-wrap"><h2 className="title">FAQ {searchCount}</h2></div>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Search"
className={styles.searchBox}
value={storeText}
onChange={e => {
setStoreText(e.target.value)
}}
/>
</form>
{/* <!-- S: faq-list-wrap --> */}
<div className="faq-list-wrap" ref={searchContainer} >
{tab === 'all' && (
<>
<FrequentList searchText={searchText} />
<EstimateList searchText={searchText} />
<ReturnList searchText={searchText} />
<EtcList searchText={searchText} />
<SubsidiaryList searchText={searchText} />
<ServiceList searchText={searchText} />
<InList searchText={searchText} />
<OutList searchText={searchText} />
</>
)}
{tab === 'frequent' && (
<FrequentList searchText={searchText}/>
)}
{tab === 'estimate' && (
<EstimateList searchText={searchText}/>
)}
{tab === 'return' && (
<ReturnList searchText={searchText} />
)}
{tab === 'subsidiary' && (
<SubsidiaryList searchText={searchText} />
)}
{tab === 'service' && (
<ServiceList searchText={searchText} />
)}
{tab === 'in' && (
<InList searchText={searchText} />
)}
{tab === 'out' && (
<OutList searchText={searchText} />
)}
{tab === 'etc' && (
<EtcList searchText={searchText} />
)}
</div>
{/* <!-- E: faq-list-wrap --> */}
</div>
{/* <!-- E: faq-wrap --> */}
</div>
</ContentsWrap>
)
}

The problem here is in your setStoreCount logic.
In this line in your for loop:
if (searchContainer.current.children[i].children[j].className.includes('show')) {
console.log('count added')
setStoreCount(prevCount => prevCount + 1)
}
It adds the 1 to the prevCount meaning if the prevCount is 5 (which is the number of results on the first search, it adds 1 to that and so on.
What you can do is reset the storeCount before you add the number of elements found. like so:
setTimeout(() => {
setStoreCount(0)
// gets all <ul> children of <div class = "faq-list-wrap">
for (let i = 0; i < searchContainer.current.children.length; i++) {
// gets all <li> children of each <ul>
for (let j = 0; j < searchContainer.current.children[i].children.length; j++) {
// checks if each li class name has 'show'
if (searchContainer.current.children[i].children[j].className.includes('show')) {
console.log('count added')
setStoreCount(prevCount => prevCount + 1)
}
}
}
}, 100) // setTimeOut needed to target searchContainer after search and not before
But this might make it like, it show FAQ 0 then the rest of the count after it loops. So I suggest count it first before you set the count, like so.
setTimeout(() => {
let count = 0
// gets all <ul> children of <div class = "faq-list-wrap">
for (let i = 0; i < searchContainer.current.children.length; i++) {
// gets all <li> children of each <ul>
for (let j = 0; j < searchContainer.current.children[i].children.length; j++) {
// checks if each li class name has 'show'
if (searchContainer.current.children[i].children[j].className.includes('show')) {
console.log('count added')
count++
}
}
}
setStoreCount(count)
}, 100) // setTimeOut needed to target searchContainer after search and not before

Your loop is just incrementing the storeCount perpetually. You should start a counter at zero at the start of the loop the loop and then assign the final value of the counter into your storeCount using setStoreCount.

Related

Third condition in ternery operator doesn't render when condition is met

Trying to figure out where I am going wrong with this ternery.
It isn't rendering the last element (blurredscreen) if authStatus !== "authenticated".
return (
<>
<div key={"key-" + id}>
{isOpen && (
<div>
{authStatus === "authenticated" &&
(header.x && header.x.t === "abc" ? (
<div
onClick={(e) => {
e.stopPropagation();
}}
>
<ComponentOne />
</div>
) : header.x && header.x.t === "def" ? (
<div
onClick={(e) => {
e.stopPropagation();
}}
>
<ComponentTwo />
</div>
) : ( //this is the part that doesn't display when condition is met
authStatus !== "authenticated" &&
!header.x && (
<div>
<img
src={blurredScreen}
/>
<button
onClick={handleSignInClick}
>
<span style={{ textAlign: "left" }}>Login </span>
</button>
</div>
)
))}
</div>
)}
</div>
</>
);
Overuse of tenery operator in JSX can lead to really confusing code and makes it hard to debug. The posters question is a good example, there are only 3 conditional routes and the code looks very confusing, as you can imagine this only gets more complicated with 4,5+.. etc.
The good news, you can still use if else inside React, just assign your JSX to a local variable and render this.
Below is a simple example that flips between 3 states, and as you can see is much easier to reason with.
const {useEffect, useState, Fragment} = React;
function Demo() {
const [s, setS] = useState(0);
useEffect(() => {
const t = setInterval(() => setS(c => c +1), 1000);
return () => { clearInterval(t) }
}, []);
let render = null;
let d = s % 3;
if (d === 0) {
render = <div>This is 0</div>
} else if (d ===1) {
render = <div>This is One</div>
} else {
render = <div>Everything else</div>
}
//Fragment used here, because SO Snippet
//do not transpile <>....</>
return <Fragment>{render}</Fragment>
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Demo/>);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
Also notice I set let render = null;, this is deliberate, null is a valid JSX type that renders nothing. So if for some reason no code paths are met, React will render nothing instead of throwing an error. Of course in the above example that's not going to happen, but it's good habit to get into. If your on React 18, you can now leave out the null. https://blog.saeloun.com/2022/04/14/react-18-allows-components-to-render-undfined.html

State updates with the old and the new values at the same time

I have been trying to update initUsers values on the DOM so the initial state values it works just fine but when I update initUsers it renders different values from the expected ones.
So basically entriesNum get a number as an event and based on it changes the order of initUsers state value from something like this:
enter image j here
to this :
enter image description here
const [iteratedUsers, setIteratedUsers] = useState([...users]);
const [Newobj, setNewobj] = useState([]);
const [isNewObj, setIsNewObj] = useState(false);
const [initUsers, setUsers] = useState(users);`
const entriesNum = (event) => {
const newi = Math.ceil(5 / event.target.value);
for (let i = 0; i < newi; i++) {
if (iteratedUsers.length >= event.target.value) {
Newobj.push(
iteratedUsers.splice(0, event.target.value).reduce((obj, key, i) => {
obj[i] = key;
return obj;
}, {})
);
} else {
Newobj.push(
iteratedUsers.splice(0).reduce((obj, key, i) => {
obj[i] = key;
return obj;
}, {})
);
}
}
setNewobj([]);
setIsNewObj(true);
setUsers(Newobj);
setIteratedUsers(users);
};
Because I have two forms of initUsers I hade to set two ways of destructuring like this:
{isNewObj ?
initUsers.map((objUser ) => (
< >
{Object.keys(objUser).map((numo) => (
<div
key={numo}
className="contacts-info border-solid border-2 border-black table-row w-full "
>
<input type={"checkbox"} className={`${shortcut}`} />
<div className={`${shortcut}`}>
{objUser[numo].index}
</div>
<div className={`${shortcut}`}>
{objUser[numo].email}
</div>
<div className={`${shortcut}`}>
{objUser[numo].access}
</div>
</div>
))}
</ >
)) :
initUsers.map(({ index, handle, phone, email, access }) => (
<div
key={id}
className="contacts-info border-solid border-2 border-black table-row w-full "
>
<input type={"checkbox"} className={`${shortcut}`} />
<div className={`${shortcut}`}>{index}</div>
<div className={`${shortcut}`}>{handle}</div>
<div className={`${shortcut}`}>{phone}</div>
<div className={`${shortcut}`}>{email}</div>
<div className={`${shortcut}`}>{access}</div>
</div>
))
}
the second condition destructur the initUsers when it is not nested and shows the following result:
enter image description here
And the first one destructure it in its nested form and shows the following result:
enter image description here
So instead of getting 5 rows the result of destructuring the nested initUsers I get 9.
You cant update the state by this way Newobj.push that's doesn't work with ReactJs state.
You can use setNewobj(your new data)
And
setNewobj((prev) => [...prev,{}])
Here prev are the old data you have.
react state is immutable, you can't modify it directly.
instead of Newobj.push use setNewobj([...Newobj]) , your operation to modify Newobj
after setting Newobj you should first setUsers(Newobj) then setNewobj([]);

conditionally rendering className in react jsx

var array = [
['2','35'],
['80','30'],
['300','25']
]
so this is a simplified version of the array, what i am getting from an api call. Each children array's 1st value is quantity and 2nd value is price. below is my simplified state
this.state = {quantity:''}
Inside jsx what i am trying to do is conditionally render a classname called selected based upon quantity value of the state. whenever the state quantity changes. the selected class also should change accordingly.
below is my jsx
{array.map((price, index, arr) => {
if (index < arr.length -1) {
if (parseInt(arr[index+1][0]) > parseInt(this.state.quantity) && parseInt(this.state.quantity) >= parseInt(price[0])){
return (
<div className='price-box selected'>
<h3 className='price'>Tk {price[1]}</h3>
<p className='quantity'>{price[0]} or more</p>
</div>
);
} else {
return (
<div className='price-box'>
<h3 className='price'>Tk {price[1]}</h3>
<p className='quantity'>{price[0]} or more</p>
</div>
);
}
} else {
if (parseInt(this.state.quantity) >= parseInt(price[0])) {
return (
<div className='price-box selected'>
<h3 className='price'>Tk {price[1]}</h3>
<p className='quantity'>{price[0]} or more</p>
</div>
);
} else {
return (
<div className='price-box'>
<h3 className='price'>Tk {price[1]}</h3>
<p className='quantity'>{price[0]} or more</p>
</div>
);
}
}
})}
Here everything is working fine (apart from for quantity 0 and 1 all of the conditions are evaluating to false as expected.so not a single div is assigned selected class).I am 100% sure there is a shorter and better approach.
Name your data points and construct a test which satisfies all the requirements for selected. Then assign the class name if selected is true using a template literal.
{array.map((price, index, arr) => {
const stateQ = parseInt(this.state.quantity);
const dataQs = arr.map((p, i) => i === 0 ? 0 : parseInt(p[0]));
const selectedIndex = dataQs.findIndex((q, i, arr) => {
return stateQ >= q && stateQ < (arr[i+1] || stateQ + 1);
});
const selected = selectedIndex === index;
return (
<div className={`price-box ${selected && 'selected'}`}>
<h3 className='price'>Tk {price[1]}</h3>
<p className='quantity'>{price[0]} or more</p>
</div>
);
})}
Maybe something like this is what you're looking for ?
Working example on Codesandbox
class Quantity extends React.Component {
constructor(props) {
super(props);
this.state = {
quantity: "2"
};
}
render() {
const array = [["2", "35"], ["80", "30"], ["300", "25"], ["2"], ["", "3"]]; // With some errors
return (
<div>
<h1>Hello</h1>
{array.map((row) => {
const condition = this.state.quantity === row[0]; // Create your condition
if (row && row[0] && row[1])
return (
<div className={`price-box ${condition && "selected"}`}> // Called template literal, you can simply include selected if your condition is true
<h3>Quantity {row[0]}</h3>
<p>Price {row[1]}</p>
</div>
);
else return <div>Data error</div>;
})}
</div>
);
}
}

JavaScript. React. Redux. Async. Loop does not complete before rendering of DOM

I'm working on a Messagelist component in which I am pulling message data from an external API. Every message contains a UserId in which I'll use to query my own API for more user information. I put the list of foundUsers in my redux store and have connected my component. Before rendering to the DOM, my objective is to loop through each individual message and replace the userid with the name of its corresponding user.
The problem I'm having is that the loop does not appear to finish before rendering the component, giving me some strange behavior.
Half a second later
Here is my code:
render(){
let users = this.props.chatkit.roomUsers
let userIds = users.map((user) => {
return user.id
})
let roomMessages = this.props.messages
let messages = []
for(var i = 0; i < roomMessages.length; i++){
//create a new field to give each message a unique id
roomMessages[i].unique = roomMessages[i].senderId
//create a new field to give each message the avatar from that user
let matchingUserIndex = userIds.indexOf(roomMessages[i].senderId)
if(matchingUserIndex >= 0){
roomMessages[i].avatar = users[matchingUserIndex].avatar
}
//review messages
let previous = {}
if(i > 0){
previous = roomMessages[i - 1]
}
let currentMessage = roomMessages[i]
//check if consecutive messages are made by the same user
if(currentMessage.senderId === previous.senderId && currentMessage.unique === previous.unique){
//display user name and avatar as an empty string
messages.push({...currentMessage, unique: "", avatar: ""})
} else{
//display the user name
messages.push({...currentMessage})
}
}
//transform the unique field to contain the name of the user
let updatedMessages = []
for(var j = 0; j < messages.length; j++){
let matchingIdIndex = userIds.indexOf(messages[j].unique)
if(matchingIdIndex >= 0 && messages[j].unique !== ""){
messages[j].unique = users[matchingIdIndex].name
updatedMessages.push(messages[j])
} else {
updatedMessages.push(messages[j])
}
}
let currentChatkitUser = this.props.chatkit.chatUser.id
return(
<div>
{this.props.room && (
<div
style={{overflow: "scroll", overflowX: "hidden", maxHeight: "70vh"}}
ref={this.messageList}
>
<ul style={{listStyle: "none"}} className="p-4 mb-0">
{updatedMessages.map((message, index) => {
return (
<li
className="mb-1"
key={index}>
<div>
{message.unique && (
<span
className="d-block font-weight-bold mt-3"
style={{color: "#000323"}}
>
<img
src={message.avatar}
style={{width: "2.5rem"}}
className="rounded-circle mr-2"
/>
{message.unique}
</span>
)}
<span
className={message.senderId === currentChatkitUser ?
"muted-blue text-light rounded d-inline-block ml-5" : "bg-secondary text-light rounded d-inline-block ml-5"
}
style={{padding:".25rem .5rem"}}
>
{message.text}
</span>
</div>
</li>
)
})}
</ul>
<TypingIndicator usersWhoAreTyping={this.props.usersWhoAreTyping}/>
<div
style={{float: "left", clear: "both"}}
ref={this.messagesEnd}>
</div>
</div>
)}
</div>
)
}
use state property(e.g. loading = true) at initial which wil return the rendering some loading DOM. and define a function performing all the loop activity and execute it in componentwillmount and when you finish looping set loading property of state to false by which you can make your DOM to render your list of user in render method. ( not sure if componentwillmount is deprecated ).

How to show results of a map in two or more columns using react

I think I have a simple question, but I can't get a solution to do this with react, I would like show results in two columns like:
item 1 | item 4
item 2 | item 5
item 3 | item 6
I tried verify if array lenght is 0 or new start column, but I can't draw a start div element without draw the end div element
I would like to do something like this:
render() {
const secondColumnStart = this.props.result.length / 2;
return <div className="row">
{this.props.result.map((item, i) =>
{ (i == 0 || i == secondColumnStart) && <div className="col-md-6"> }
item.value
{ (i == 0 || i == secondColumnStart) && </div> })}
</div>;
}
Simply map items as you usually do from one array. With that, use the CSS property "columns" to display them as described in the question above.
.container {
columns: 2 auto;
}
Assuming two column's, having col-md-6 class for row splitting.
create stateless component myRow
const myRow = ({index})=>{(<div className="col-md-6">{index}</div>)}
create array for each cols
const col1 = [],col2 = [];
this.props.result.forEach((item, i) => {
if(i%===0){
col1.push(myRow);
}else{
col2.push(myRow);
}
}
return the React element in render.
return <div className="row">
{col1}{col2}
</div>;
If you always want exactly two columns, then one option is to call map twice. Once for each half of the array:
const secondColumnStart = Math.floor(this.props.result.length / 2);
return (
<div className="row">
<div className="col-md-6">
{this.props.result.slice(0,secondColumnStart).map(item => item.value)}
</div>
<div className="col-md-6">
{this.props.result.slice(secondColumnStart).map(item => item.value)}
</div>
</div>
);
Will there always be 2 columns, regardless of how many items you have? If there are 5 items, should it display as col A -> 1,2,3. col B -> 4,5?
Use CSS to put the two columns side by side.
var halfwayPoint = Math.ceiling(this.props.result.length / 2)
var columnA = this.props.result.splice(0, halfwayPoint)
var columnB = this.props.result.splice(halfwayPoint)
render () {
return (
<div className='columnA'>
{columnA.map((item, i) => {
return (
<div>{item.value}</div>
)
})
}
</div>
<div className='columnB'>
{columnB.map((item, i) => {
return (
<div>{item.value}</div>
)
})
}
</div>
)
}
You can use the following code.
const Thingy = props => {
const gridCols = [[],[]];
const result = [10,20,30,40,50,60,70];
result.forEach( ( data,i )=>{
const comp = (
<button value={data}>{data+" "}</button>
);
const colNumber = i % 2;
gridCols[colNumber].push( comp );
} );
return (
<div className="container">
<div className="row">
<div className="col-sm">
{gridCols[0]}
</div>
<div className="col-sm">
{gridCols[1]}
</div>
</div>
</div>
)
};
// Render it
ReactDOM.render(
<Thingy title="I'm the thingy" />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I also faced a similar problem where I need to show the results of an array into three columns in card.
For that I have grouped the array elements into groups as below.
arr = ['a','b','c','d',e','f'] --------> arr = [['a','b','c'],['d','e','f']]
let modified_collection=[]
if (collection.length>0) {
modified_collection = collection.reduce( (rows, key, index) =>{
return (index % 3 === 0 ? rows.push([key])
: rows[rows.length-1].push(key)) && rows;
}, []);
}
After grouping I have map the elements in the modified array as below.
modified_collection.map((row) =>
<Row>
{row.map(col => (<Col>
<Card
hoverable
style={{ width: 240, height : 480 }}
cover={<img alt="example" src={col.collection.image_url} />}
>
<Meta title={col.collection.title} description={col.collection.description} />
</Card>
</Col>))}
</Row>
)
The simplest method with few lines of code for your question is
list.map((list, index)=>{
return index % 2 === 0 ?
<Col xs={6}>
{list}
</Col>
:
<Col xs={6}>
{list}
</Col>
})

Categories

Resources