I am working on a reusable filter component that dynamically creates a list of checkboxes within a dropdown, and includes an "all selected" toggle (similar to google flights). Everything is working as desired, but there is huge latency when clicking a checkbox, and especially when using the select all toggle. It is very similar to the issue described here (almost identical even), but I have tried my best to implement the solutions described therein to no avail. I tried applying useCallback on all the functions that are passed, and gotten rid of all warnings from 'why-did-you-render' but none of it resolved the issue. I'm hoping somebody here can help tell me what I'm missing. See relevant code snippets below. Thanks!
This is the parent component that manages the data:
import React, { useState, useEffect } from "react";
import ControlPanel from '../Components/ControlPanel';
const TopLevelComponent = () => {
const [data, setData] = useState();
const [selectedFilters, setSelectedFilters] = useState([]);
useEffect(() => {
getData();
}, [])
const getData = async() => {
...
}
return (
<div>
<ControlPanel
filterOptions={ data?.filterSet }
filterSelections={ selectedFilters }
filterHandler={ setSelectedFilters }
/>
{ /* other stuff irrelevant here */ }
</div>
)
}
export default TopLevelComponent;
This is a reusable control panel component that holds the filters and other settings:
import React from "react";
import { Filter } from './Inputs';
const ControlPanel = ({ filterOptions, filterSelections, filterHandler }) => {
return (
<>
{ /* other controls not relevant here */ }
<div className="control-panel__filters">
<Filter
filterName="Filter A"
options={ filterOptions }
handler={ filterHandler }
selections={ filterSelections }
selectAllLabel="Select all filter A"
/>
</div>
{ /* more irrelevant stuff */ }
</>
)
}
export default ControlPanel;
and finally, this is where the filters and the components that make them up are defined:
import React, { useState } from "react";
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem, Label } from 'reactstrap';
import Toggle from 'react-toggle';
import chevronDown from '../icons/chevron-down-dark.svg';
import chevronUp from '../icons/chevron-top.svg';
import closeIcon from '../icons/close.svg';
export const Filter = ({ options, handler, selections, filterName, selectAllLabel }) => {
const [dropdownOpen, setDropdownOpen] = useState(false);
const toggle = () => setDropdownOpen(prevState => !prevState);
return (
<Dropdown isOpen={ dropdownOpen } toggle={ toggle } >
<DropdownToggle>
{ filterName }
<img src={ dropdownOpen ? chevronUp : chevronDown } />
</DropdownToggle>
<DropdownMenu>
<FilterInterface
toggle={ toggle }
filterName={ filterName }
options={ options }
selections={ selections }
handler={ handler }
selectAllLabel={ selectAllLabel }
/>
</DropdownMenu>
</Dropdown>
);
}
const FilterInterface = ({ toggle, filterName, options, selections, handler, selectAllLabel }) => {
const [allSelected, setAllSelected] = useState(false);
const toggleAllSelected = () => {
if (options.length !== selections.length) {
const newSelections = options.map(option => option.id);
handler(newSelections);
setAllSelected(true);
} else {
handler([]);
setAllSelected(false);
}
}
const handleCheck = id => {
let newArray;
if (selections.includes(id)) {
newArray = selections.filter(selection => {
return selection !== id;
});
setAllSelected(false);
} else {
newArray = [...selections, id];
}
handler(newArray);
if (newArray.length === options.length) setAllSelected(true);
}
return (
<>
<DropdownItem header>
{ filterName }
<img src={ closeIcon } onClick={ toggle } />
</DropdownItem>
<DropdownItem header>
{selectAllLabel}
<Toggle
checked={ allSelected }
icons={ false }
onChange={ toggleAllSelected }
/>
</DropdownItem>
{
options?.map(option => (
<DropdownItem header
key={ option.id }
value={ option.name }
className="filterCheckbox"
>
<Label onClick={ () => handleCheck(option.id) }>
<Checkbox checked={ selections.includes(option.id) } readOnly />
<span>{ option.name }</span>
</Label>
</DropdownItem>
))
}
</>
)
}
The thing that finally seemed to do the trick that isn't discussed in the linked article was passing custom comparison functions to the memoized components so they would not update when the dropdown was closed.
Related
I am trying to make a flashcard web app for language learning and/or rote learning. I have managed to show the first element of the array which contains the data that I'm fetching from the backend but I can't switch from the first element to the subsequent elements.
Here is my code in React:
// Decklist component that displays the flashcard
import { React, useEffect, useState, useContext } from "react";
import Card from "./Card";
import cardContext from "../store/cardContext";
const axios = require("axios");
export default function Decklist() {
//State for data fetched from db
const [data, setData] = useState([]);
//State for array element to be displayed from the "data" state
const [position, setPosition] = useState(0);
//function to change the array element to be displayed after user reads card
const setVisibility = () => {
setPosition(position++);
};
//function to change the difficulty of a card
const difficultyHandler = (difficulty, id) => {
console.log(difficulty);
setData(
data.map((ele) => {
if (ele.ID === id) {
return { ...ele, type: difficulty };
}
return ele;
})
);
};
//useEffect for fetching data from db
useEffect(() => {
axios
.get("/api/cards")
.then((res) => {
if (res.data) {
console.log(res.data);
setData(res.data.sort(() => (Math.random() > 0.5 ? 1 : -1)));
}
})
.catch((err) => {
console.log(err);
});
}, []);
return (
<cardContext.Provider
value={{ cardData: data, setDifficulty: difficultyHandler }}
>
{data.length && (
<Card
position={position}
// dataIndex={index}
visible={setVisibility}
id={data[position].ID}
front={data[position].Front}
back={data[position].Back}
/>
)}
</cardContext.Provider>
);
}
//Card component
import { React, useState, useEffect } from "react";
import Options from "./Options";
export default function Card(props) {
//State for showing or hiding the answer
const [reverse, setReverse] = useState(false);
const [display, setDisplay] = useState(true);
//function for showing the answer
const reversalHandler = () => {
setReverse(true);
};
return (
<div>
{reverse ? (
<div className="card">
{props.front} {props.back}
<button
onClick={() => {
props.visible();
}}
>
Next Card
</button>
</div>
) : (
<div className="card">{props.front}</div>
)}
<Options
visible={props.visible}
reverse={reversalHandler}
id={props.id}
/>
</div>
);
}
//Options Component
import { React, useContext, useState } from "react";
import cardContext from "../store/cardContext";
export default function Options(props) {
const ctx = useContext(cardContext);
const [display, setDisplay] = useState(true);
return (
<>
<div className={display ? "" : "inactive"}>
<button
onClick={() => {
setDisplay(false);
props.reverse();
ctx.setDifficulty("easy", props.id);
}}
>
Easy
</button>
<button
onClick={() => {
setDisplay(false);
props.reverse();
ctx.setDifficulty("medium", props.id);
}}
>
Medium
</button>
<button
onClick={() => {
setDisplay(false);
props.reverse();
ctx.setDifficulty("hard", props.id);
}}
>
Hard
</button>
</div>
</>
);
}
The setVisibility function in the Decklist component is working fine and setting the position state properly. However, I don't know how to re-render the Card component so that it acts on the position state that has changed.
One way to force a re-render of a component is to set its state to itself
onClick={() => {
props.visible();
setReverse(reverse);
}}
However this probably isn't your issue as components will automatically re-render when their state changes or a parent re-renders. This means that for some reason the Card component isn't actually changing the parent component.
I cannot select the options being displayed from MUI's autocomplete component. The reason I think is that I am using renderOption. I want to display the image along the title in the options of my component and without using renderOption, I have not been able to do that. But, doing so (using renderOption), I cannot select any option.
import React, { useState } from 'react';
import { fetchSearchAnimeEndpoint } from '../../redux';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import TextField from '#mui/material/TextField';
import Autocomplete from '#mui/material/Autocomplete';
import CircularProgress from '#mui/material/CircularProgress';
import './searchbar.css';
const SearchBar = (props: any) => {
const [openPopper, setOpenPopper] = useState(false);
const [inputValue, setInputValue] = useState('');
const { anime } = props;
const handleKeyPress = debounce(
(event: React.FormEvent<HTMLInputElement>) => {
const value = (event.target as HTMLInputElement).value;
props.fetchSearchAnime(value);
},
700
);
return (
<div>
{/*
Cannot select the options being displayed either by clicking or using keyboard
Want to display the title on the textfield by selecting desired option */}
<Autocomplete
id="combo-box-demo"
options={anime.results.data ? anime.results.data : []}
style={{ width: 300, marginTop: '2rem' }}
isOptionEqualToValue={(option, value) => option.title === value.title}
getOptionLabel={(option: any) => option.title}
renderOption={(option: any, optionAnime) => {
return (
<h4 key={optionAnime.mal_id} className="search-container">
<img
className="search-image"
alt="anime"
src={optionAnime.image_url}
/>
{optionAnime.title}
</h4>
);
}}
renderInput={(params) => (
<TextField
onInput={(e) => {
if ((e.target as HTMLInputElement).value.length >= 3) {
setOpenPopper(true);
} else {
setOpenPopper(false);
}
return handleKeyPress(e);
}}
{...params}
label="Anime"
/>
)}
open={openPopper}
onClose={() => {
setOpenPopper(false);
}}
/>
</div>
);
};
const mapStateToProps = (state: any) => {
return {
anime: state,
};
};
const mapDispatchToProps = (dispatch: any) => {
return {
fetchSearchAnime: (name: string) =>
dispatch(fetchSearchAnimeEndpoint(name)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(SearchBar);
I am getting the options and just cannot select them. What I want is to select them by clicking on them or using the keyboard and get the title value on the textfield
Hey so they changed the renderInput function as it now takes props and options
so for this example
<TextField
onInput={(e) => {
if ((e.target as HTMLInputElement).value.length >= 3) {
setOpenPopper(true);
} else {
setOpenPopper(false);
}
return handleKeyPress(e);
}}
{...props}
label="Anime"
/>
)}
open={openPopper}
onClose={() => {
setOpenPopper(false);
}}
/>
for more info on the problem, theres an example on github:
https://github.com/mui/material-ui/issues/29943
Maybe the question is a little bit confusing because I'm confused. The problem I have listed categories in the database I fetched it and create a post. Now I'm trying to edit the post. The categories are in checkbox format if check it adds the setCategories state if uncheck it will remove from the state. I have fetched the post and saved categories for that particular post. I've shown them checked. Now I'm trying to change the categories I've added. I'm successful to add more but cannot remove it as well am unable to uncheck the checkbox. Please check this code...
I'm highlighted the onChange part with dashes
here is code
import React, { useEffect, useState } from 'react';
import { Alert, Button, Card, Container, Form } from 'react-bootstrap';
import ReactMarkdown from 'react-markdown';
import { useDispatch, useSelector } from 'react-redux';
import { toast, ToastContainer } from 'react-toastify';
import { listCategory } from '../actions/categoryActions';
import { listPostDetails, updatePost } from '../actions/postActions';
const EditPost = ({ history, match }) => {
const postId = match.params.id;
const [categories, setCategories] = useState([]);
const dispatch = useDispatch();
const userLogin = useSelector((state) => state.userLogin);
const { userInfo } = userLogin;
const categoryList = useSelector((state) => state.categoryList);
const { categories: cateList } = categoryList;
useEffect(() => {
if (!userInfo) {
history.push('/login');
}
if (!post || post._id !== postId) {
dispatch(listPostDetails(postId));
} else {
setCategories(post.categories);
console.log(categories);
}
dispatch(listCategory());
}, [dispatch, history, userInfo, post, postId, categories]);
const resetHandler = () => {
setTitle('');
setImg('');
setCategories('');
setDesc('');
};
const submitHandler = (e) => {
e.preventDefault();
dispatch(updatePost(postId, title, desc, img, categories));
resetHandler();
history.push('/my_posts');
};
return (
<div className=" createPost mt-4 py-4">
<ToastContainer />
<Container>
<h2>EDIT POST</h2>
<Form onSubmit={submitHandler}>
<Form.Group controlId="category" className="mb-2">
<Form.Label>Select Categories</Form.Label>
<br />
{cateList?.map((cate) => (
<Form.Check
inline
key={cate._id}
type="checkbox"
label={cate.name}
checked={categories.includes(cate.name)}
------------------------------------------------------------------------------------------
onChange={(e) => {
if (e.target.checked) {
setCategories([categories.push(cate.name)]);
} else {
setCategories(
categories?.filter((cat) => cat !== cate.name)
);
}
}}
-------------------------------------------------------------------------------------------
/>
))}
</Form.Group>
<Button
type="submit"
variant="success"
style={{ letterSpacing: '2px', fontWeight: 'bold' }}>
UPDATE
</Button>
</Form>
</Container>
</div>
);
};
export default EditPost;
I think the problem is on useEffect method you are console.log(categories) it keeps on refreshing the state and not allowing you to add or remove items. first remove the console.log(categories) and also categories dependencies from useEffect and use this setCategories([...categories, cate.name]); instead of this setCategories([categories.push(cate.name)]);. You shouldn't change categories directly
You shouldn't change categories directly. So, instead of
setCategories([categories.push(cate.name)]);
try
setCategories([...categories, cate.name]);
So I have a Context created with reducer. In reducer I have some logic, that in theory should work. I have Show Component that is iterating the data from data.js and has a button.I also have a windows Component that is iterating the data. Anyway the problem is that when I click on button in Show Component it should remove the item/id of data.js in Windows Component and in Show Component, but when I click on it nothing happens. I would be very grateful if someone could help me. Kind regards
App.js
const App =()=>{
const[isShowlOpen, setIsShowOpen]=React.useState(false)
const Show = useRef(null)
function openShow(){
setIsShowOpen(true)
}
function closeShowl(){
setIsShowOpen(false)
}
const handleShow =(e)=>{
if(show.current&& !showl.current.contains(e.target)){
closeShow()
}
}
useEffect(()=>{
document.addEventListener('click',handleShow)
return () =>{
document.removeEventListener('click', handleShow)
}
},[])
return (
<div>
<div ref={show}>
<img className='taskbar__iconsRight' onClick={() =>
setIsShowOpen(!isShowOpen)}
src="https://winaero.com/blog/wp-content/uploads/2017/07/Control-
-icon.png"/>
{isShowOpen ? <Show closeShow={closeShow} />: null}
</div>
)
}
```Context```
import React, { useState, useContext, useReducer, useEffect } from 'react'
import {windowsIcons} from './data'
import reducer from './reducer'
const AppContext = React.createContext()
const initialState = {
icons: windowsIcons
}
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
const remove = (id) => {
dispatch({ type: 'REMOVE', payload: id })
}
return (
<AppContext.Provider
value={{
...state,
remove,
}}
>
{children}
</AppContext.Provider>
)
}
export const useGlobalContext = () => {
return useContext(AppContext)
}
export { AppContext, AppProvider }
reducer.js
const reducer = (state, action) => {
if (action.type === 'REMOVE') {
return {
...state,
icons: state.icons.filter((windowsIcons) => windowsIcons.id !== action.payload),
}
}
}
export default reducer
``data.js```
export const windowsIcons =[
{
id:15,
url:"something/",
name:"yes",
img:"/images/icons/crud.png",
},
{
id:16,
url:"something/",
name:"nine",
img:"/images/icons/stermm.png",
},
{
id:17,
url:"domething/",
name:"ten",
img:"/images/icons/ll.png",
},
{
id:18,
url:"whatever",
name:"twenty",
img:"/images/icons/icons848.png",
},
{
id:19,
url:"hello",
name:"yeaa",
img:"/images/icons/icons8-96.png",
},
]
``` Show Component```
import React from 'react'
import { useGlobalContext } from '../../context'
import WindowsIcons from '../../WindowsIcons/WindowsIcons'
const Show = () => {
const { remove, } = useGlobalContext()
return (
<div className='control'>
{windowsIcons.map((unin)=>{
const { name, img, id} = unin
return (
<li className='control' key ={id}>
<div className='img__text'>
<img className='control__Img' src={img} />
<h4 className='control__name'>{name}</h4>
</div>
<button className='unin__button' onClick={() => remove(id)} >remove</button>
</li> )
</div>
)
}
export default Show
import React from 'react'
import {windowsIcons} from "../data"
import './WindowsIcons.css'
const WindowsIcons = ({id, url, img, name}) => {
return (
<>
{windowsIcons.map((icons)=>{
const {id, name , img ,url} =icons
return(
<div className='windows__icon' >
<li className='windows__list' key={id}>
<a href={url}>
<img className='windows__image' src={img}/>
<h4 className='windows__text'>{name}</h4>
</a>
</li>
</div>
)
})}
</>
)
}
Issue
In the reducer you are setting the initial state to your data list.
This is all correct.
However, then in your Show component you are directly importing windowsIcons and looping over it to render. So you are no longer looping over the state the reducer is handling. If the state changes, you won't see it.
Solution
In your Show component instead loop over the state that you have in the reducer:
const { remove, icons } = useGlobalContext()
{icons.map((unin) => {
// Render stuff
}
Now if you click remove it will modify the internal state and the icons variable will get updated.
Codesandbox working example
I am trying to make a set of questions through multiple pages, so I need the selected answer from page 1 and page 2 to be passed to page 3 because page 3 is like the confirmation page which will show all the selected answer from the past 2 pages.
The interface is successfully shown, well, easy but it seems like there is no data passed at all, oh, and the types of questions are radio and checkbox, so it's kinda hard for me because these 2 types are something new for me (if textarea or normal input, is easy).
This is the mainpage.jsx
// MainForm.jsx
import React, { Component } from 'react';
import AircondQuantity from './AircondQuantity';
import ServicePackage from './ServicePackage';
import Confirmation from './Confirmation';
import Success from './Success';
class MainForm extends Component {
state = {
step: 1,
oneAircond: ''
}
nextStep = () => {
const { step } = this.state
this.setState({
step : step + 1
})
}
prevStep = () => {
const { step } = this.state
this.setState({
step : step - 1
})
}
handleChange = input => event => {
this.setState({ [input] : event.target.value })
}
render(){
const {step} = this.state;
const { oneAircond } = this.state;
const values = { oneAircond };
switch(step) {
case 1:
return <AircondQuantity
nextStep={this.nextStep}
handleChange = {this.handleChange}
values={values}
/>
case 2:
return <ServicePackage
nextStep={this.nextStep}
prevStep={this.prevStep}
handleChange = {this.handleChange}
values={values}
/>
case 3:
return <Confirmation
nextStep={this.nextStep}
prevStep={this.prevStep}
values={values}
/>
case 4:
return <Success />
}
}
}
export default MainForm;
this is the first page, AircondQuantity.jsx
// AircondQuantity.jsx
import React, { Component } from 'react';
import { Form, Button, FormRadio, Radio } from 'semantic-ui-react';
class AircondQuantity extends Component{
saveAndContinue = (e) => {
e.preventDefault()
this.props.nextStep()
}
render(){
const { values } = this.props;
return(
<Form >
<h1 className="ui centered"> How many aircond units do you wish to service? </h1>
<Form.Field>
<Radio
label='1'
name='oneAircond'
value='oneAircond'
//checked={this.state.value === this.state.value}
onChange={this.props.handleChange('oneAircond')}
defaultValue={values.oneAircond}
/>
</Form.Field>
<Button onClick={this.saveAndContinue}> Next </Button>
</Form>
)
}
}
export default AircondQuantity;
this is the next page, ServicePackage.jsx
// ServicePackage.jsx
import React, { Component } from 'react';
import { Form, Button, Checkbox } from 'semantic-ui-react';
import { throws } from 'assert';
class ServicePackage extends Component{
saveAndContinue = (e) => {
e.preventDefault();
this.props.nextStep();
}
back = (e) => {
e.preventDefault();
this.props.prevStep();
}
render(){
const { values } = this.props
return(
<Form color='blue' >
<h1 className="ui centered"> Choose your package </h1>
<Form.Field>
<Checkbox
label={<label> Chemical Cleaning </label>} />
</Form.Field>
<Form.Field>
<Checkbox
label={<label> Deep Cleaning </label>} />
</Form.Field>
<Button onClick={this.back}> Previous </Button>
<Button onClick={this.saveAndContinue}> Next </Button>
</Form>
)
}
}
export default ServicePackage;
this is the confirmation.jsx page, the page that will show all the selected options
// Confirmation.jsx
import React, { Component } from 'react';
import { Button, List } from 'semantic-ui-react';
import AircondQuantity from './AircondQuantity';
class Confirmation extends Component{
saveAndContinue = (e) => {
e.preventDefault();
this.props.nextStep();
}
back = (e) => {
e.preventDefault();
this.props.prevStep();
}
render(){
const {values: { oneAircond }} = this.props;
return(
<div>
<h1 className="ui centered"> Confirm your Details </h1>
<p> Click Confirm if the following details have been correctly entered </p>
<List>
<List.Item>
<List.Content> Aircond Quantity: {oneAircond}</List.Content>
</List.Item>
</List>
<Button onClick={this.back}>Back</Button>
<Button onClick={this.saveAndContinue}>Confirm</Button>
</div>
)
}
}
export default Confirmation;
I am very new in using React and I know that I have some mistakes in transferring values or variables but I can't detect it, coz I am a newbie, so em, can you help me? thank you.
This doesn't look right to me:
onChange={this.props.handleChange('oneAircond')}
Firstly, it's going to be called instantly, before onChange is actually called, to fix that do this:
onChange={() => this.props.handleChange('oneAircond')}
However you'll also need to pass the change event from the radio button, try this:
onChange={(event) => this.props.handleChange('oneAircond')(event)}
The handleChange function below is a function that returns another function, the first one taking the 'oneAircond' string (input) and returning a function that is expecting the event from the radio button to be passed which is what you're missing
handleChange = input => event => {
this.setState({ [input] : event.target.value })
}
Ok, first i convert solution into hooks:
// MainForm.jsx
import React, { Component, useState, useEffect } from 'react';
import AircondQuantity from './AircondQuantity';
import ServicePackage from './ServicePackage';
import Confirmation from './Confirmation';
// import Success from './Success';
let MainForm = (props) => {
const [step, setStep] = useState(1)
const [input, setInput] = useState([])
const [oneAircond, setoneAircond] = useState('')
const nextStep = () => {
setStep(step + 1)
}
const prevStep = () => {
setStep(step - 1)
}
const handleChange = input => event => {
setInput({ [input] : event.target.value})
console.log(input)
}
const values = { oneAircond };
return(
<React.Fragment>
{(() => {
switch(step) {
case 1:
return <AircondQuantity
nextStep={nextStep}
handleChange = {handleChange}
values={values}
/>
case 2:
return <ServicePackage
nextStep={nextStep}
prevStep={prevStep}
handleChange = {handleChange}
values={values}
/>
case 3:
return <Confirmation
nextStep={nextStep}
prevStep={prevStep}
values={values}
/>
case 4:
// return <Success />
}
})()}
</React.Fragment>
)
}
export default MainForm;
Next I transform AirCondQuantity.jsx, and create new component Radio.jsx, which holds Radio structure (it is a component into which we inject data directly)
So here is:
// AircondQuantity.jsx
import React, { Component, useState } from 'react';
import { Form, Button, FormRadio, } from 'semantic-ui-react';
import Radio from './Radio';
let AircondQuantity = (props) => {
const [radio, setRadio] = useState();
const radioSelect = (name, e) => {
localStorage.setItem('FirstStep', name);
setRadio({ selectedRadio: name })
console.log(radio)
}
const saveAndContinue = (e) =>{
e.preventDefault()
props.nextStep()
}
return(
<Form >
<h1 className="ui centered"> How many aircond units do you wish to service? </h1>
<Form.Field>
<Radio
handleRadioSelect={radioSelect}
selectedRadio={radio}
name ="oneAircond"/>
<Radio
handleRadioSelect={radioSelect}
selectedRadio={radio}
name ="twoAircond"/>
</Form.Field>
<Button onClick={saveAndContinue}> Next </Button>
</Form>
)
}
export default AircondQuantity;
I added function, which setting the item to LocalStorage + send it in radio state
Additional here is a Radio component:
import React, { Component, useState } from 'react';
const Radio = (props) => {
return (
<div className = "RadioButton"
onClick={() => props.handleRadioSelect(props.name)} >
<div>
<input id={props.id} value={props.value} type="radio" checked={props.selectedRadio === props.name} />
{props.name}
</div>
</div>
);
}
export default Radio;
The solution works, despite one thing - it doesn't check the box, I have no idea how to fix it.