How to debounce search function in this React Component? - javascript

I have a component which gets a list of employees as a prop. I've also created an input element for filtering the list by string.
I moved filtering logic into a function which expects a list of data and a search value so it could return filtered list.
I want to add lodash debounce to search input, so whenever user types something, it would wait 1 second and filter the list out.
import React from 'react';
import _ from "lodash"
import { IEmployee } from '../../../store/EmployeesStore/reducer'
import AddEmployee from '../AddEmployee/AddEmployee';
import EmployeeItem from './EmployeeItem/EmployeeItem';
import { TextField } from '#material-ui/core';
import InputAdornment from '#material-ui/core/InputAdornment';
import SearchIcon from '#material-ui/icons/Search';
export interface EmployeeProps {
employees: IEmployee[];
}
class EmployeeList extends React.Component<EmployeeProps> {
state = {
searchValue: ''
};
//function which returns filtered list
filterList = (employeesList: IEmployee[], searchValue: string) => {
return employeesList.filter((item: any) => {
const fullName = `${item.firstName}${item.lastName}`.toLowerCase();
const reversedFullName = `${item.lastName}${item.firstName}`.toLowerCase();
const trimmedSearchValue = searchValue
.replace(/\s+/g, '')
.toLowerCase();
return (
fullName.includes(trimmedSearchValue) ||
reversedFullName.includes(trimmedSearchValue)
);
});
};
render() {
// saving filtered list data in filteredList variable
let filteredList = this.filterList(this.props.employees, this.state.searchValue)
return (
<>
<AddEmployee />
<TextField
style={{ marginLeft: '20px' }}
size="medium"
id="input-with-icon-textfield"
variant="outlined"
value={this.state.searchValue}
onChange={(e) => this.setState({ searchValue: e.target.value })}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<SearchIcon />
</InputAdornment>
),
}}
InputLabelProps={{
shrink: true,
}}
/>
<div>
<ul
style={{
margin: '0px',
padding: '0px',
listStyle: 'none',
display: 'flex',
flexWrap: 'wrap',
}}
>
{filteredList.map((employee) => {
return <EmployeeItem key={employee.id} {...employee} />;
})}
</ul>
</div>
</>
);
}
}
export default EmployeeList;
Where should I add the _.debounce function and how?

Only showing the relevant changes :-
constructor (props) {
super(props)
this.state = {
searchValue: ''
};
this.debouncedHandler = _.debounce(this.handleChange.bind(this),1000);
}
handleChange = (e) => {
this.setState({ searchValue: e.target.value })};
}
<TextField
style={{ marginLeft: '20px' }}
size="medium"
id="input-with-icon-textfield"
variant="outlined"
value={this.state.searchValue}
onChange={this.debouncedHandler}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<SearchIcon />
</InputAdornment>
),
}}
InputLabelProps={{
shrink: true,
}}
/>
Explanation : We are calling the debouncedHandler repeatedly via onChange so we need to ensure that handleChange only gets triggered once that burst of 1000ms is over and no call to debouncedHandler happened within this duration. If another call to debouncedHandler happens within that burst interval a new burst interval starts.
From your component's perspective, we are delaying the execution of our main logic which is inside handleChange by 1000ms everytime unless the user doesn't enter any other character in the TextField component within that 1000ms, once that 1000ms are over, the setState will get triggered to state the state ---> meaning a re-render -----> meaning new filteredList and it's display to the user.

You should not be calling your filterList function in return statement, instead of that it must be called on onChange of TextField.
Something like this:
handleChange = (e) => {
this.setState({ searchValue: e.target.value })};
const debouncedCall = _.debounce(() => this.filterList(this.props.employees, e.target.value), 1000);
debouncedCall();
}
//Rest of your code
render() {
<TextField
onChange={(e) => handleChange(e)}
...other attributes
/>
}

Related

React - Secondary Prop Value - Node - Material UI

I've been working on simplifying my code and am curious how I would approach passing a secondary value using props and fetching data from the back end. I'm using material UI's Autocomplete and the PERN stack. Everything is working, except I want to change "region_name" to be a prop. So I can change the values dynamically within event details.js. when I'm fetching other data.
I currently have this component setup.
Production.js
import Autocomplete from "#mui/material/Autocomplete";
import Box from "#mui/material/Box";
import Stack from "#mui/material/Stack";
import TextField from "#mui/material/TextField";
export default function CustomAutoComplete(props) {
return (
<Stack sx={{ m: 1 }}>
<Autocomplete
sx={{ ml: 2, mr: 2 }}
size="small"
id="combo-box-demo"
freeSolo
inputValue={props.inputValue}
onInputChange={(event, newValue) => {
props.set(newValue);
}}
getOptionLabel={(data) => `${data.region_name}`}
options={props.data}
isOptionEqualToValue={(option, value) =>
option.region_name === value.region_name
}
renderOption={(props, data) => (
<Box component="li" {...props} key={data.id}>
{data.region_name}
</Box>
)}
renderInput={(params) => <TextField {...params} label="Region" />}
/>
</Stack>
);
}
Then importing it into a separate file EventDetails.js fetching the data and storing it in LocalStorage, which I'll move to useState eventually.
import CustomAutoComplete from "../../atoms/AutoComplete";
import FormGroup from "#mui/material/FormGroup";
import { Fragment, useState, useEffect } from "react";
import { useLocalStorage } from "../../utils/LocalStorage.js";
const EventDetails = () => {
const [region, setRegion] = useLocalStorage("region", "");
const [getRegion, setGetRegion] = useState([]);
// fetching backend data
useEffect(() => {
fetch("/authentication/region")
.then((response) => response.json())
.then((getRegion) => setGetRegion(getRegion));
}, []);
return (
<Fragment>
<FormGroup sx={{ p: 2, m: 1 }}>
<CustomAutoComplete
inputValue={region}
set={setRegion}
data={getRegion}
key={getRegion.id}
name={region_name} // <--feeble attempt
label="Region"
/>
</FormGroup>
</Fragment>
);
};
I had someone help me with finding a solution just had to create a prop variable like
export default function CustomAutoComplete(props) {
const { labelValue } = props;
return (renderOption={(props, data) => (
<Box component="li" {...props} key={data.id}>
{data[labelKey]}
</Box>
)})
then in the parent component
<CustomAutoComplete labelValue="region_name" />

How to pass the search query to table filter between different JS files?

I have a datagrid table in which I'm getting my database data from an API call and I have written the table code in one file. I also have a search functionality where you can search for a particular record inside the table, but this search code is in another file. I am having difficulty of passing my state variable containing the search parameter from my search file to the table file. I have separated all my components in different pages since it'd be easier to structure them using a grid in my App.js. How do I get my search query to my table file next?
My search code:
export default function SearchInput() {
const [searchTerm, setSearchTerm] = React.useState('');
return (
<Grid item xs={3}>
<Box mt={1.6}
component="form"
sx={{
'& > :not(style)': { m: 1, width: '20ch', backgroundColor: "white", borderRadius: 1},
}}
noValidate
autoComplete="off"
>
<TextField
placeholder="Search Customer ID"
variant="outlined"
size="small"
sx={{input: {textAlign: "left"}}}
onChange={(event) => {
setSearchTerm(event.target.value);
console.log(searchTerm);
}}
/>
</Box>
</Grid>
);
}
My table code:
export default function DataTable() {
const [pageSize, setPageSize] = React.useState(10);
const [data, setData] = React.useState([]);
useEffect(async () => {
setData(await getData());
}, [])
return (
<div style={{ width: '100%' }}>
<DataGrid
rows={data}
columns={columns}
checkboxSelection={true}
autoHeight={true}
density='compact'
rowHeight='40'
headerHeight={80}
disableColumnMenu={true}
disableSelectionOnClick={true}
sx={datagridSx}
pageSize={pageSize}
onPageSizeChange={(newPageSize) => setPageSize(newPageSize)}
rowsPerPageOptions={[5, 10, 15]}
pagination
/>
</div>
);
}
App.js
function App() {
return (
<div className="App">
<Container maxWidth="false" disableGutters="true">
<Grid container spacing={0}>
<ABClogo />
<HHHlogo />
</Grid>
<Grid container spacing={0}>
<LeftButtonGroup />
<SearchInput />
<RightButtonGroup />
</Grid>
<Grid container spacing={0}>
<DataTable />
<TableFooter />
</Grid>
</Container>
</div>
);
}
Here is a minimal example using createContext(), and useReducer() to lift up state and share it between components, similar to what you are after, but as jsNoob says, there are multiple options. This is one I'm comfortable with.
The concept is explained here: https://reactjs.org/docs/context.html
Essentially you can create 'global' state at any point in your component tree and using Provider / Consumer components, share that state and functionality with child components.
//Main.js
import React, { createContext, useContext, useReducer } from 'react';
const MainContext = createContext();
export const useMainContext => {
return useContext(MainContext);
}
const mainReducer = (state, action) => {
switch(action.type){
case: 'SOMETHING':{
return({...state, something: action.data});
}
default:
return state;
}
}
export const Main = () => {
const [mainState, mainDispatch] = useReducer(mainReducer, {something: false});
const stateOfMain = { mainState, mainDispatch };
return(
<MainContext.Provider value={stateOfMain}>
<MainContext.Consumer>
{() => (
<div>
<Nothing />
<Whatever />
</div>
)}
</MainContext.Consumer>
</MainContext.Provider>
)
}
Then you can have the other components use either or both of the main state and dispatch.
//Nothing.js
import {mainContext} from './Main.js'
const Nothing = () => {
const { mainState, mainDispatch } = useMainContext();
return(
<button onClick={() => {mainDispatch({type: 'SOMETHING', data: !mainState.something})}}></button>
)
}
Clicking the button in the above file, should change the display of the below file
//Whatever.js
import {mainContext} from './Main.js'
const Whatever = () => {
const { mainState } = useMainContext();
return(
<div>{mainState.something}</div>
);
}

MUI Custom Text Field loses focus on state change

I'm using MUI library to create my React Js app.
Here I'm using the controlled Text Field component to create a simple search UI.
But there is something strange. The Text Field component loses focus after its value is changed.
This is my first time facing this problem. I never faced this problem before.
How this could happen? And what is the solution.
Here is the code and the playground: https://codesandbox.io/s/mui-autocomplete-lost-focus-oenoo?
Note: if I remove the breakpoint type from the code, the Text Field component still loses focus after its value is changed.
It's because you're defining a component inside another component, so that component definition is recreated every time the component renders (and your component renders every time the user types into the input).
Two solutions:
Don't make it a separate component.
Instead of:
const MyComponent = () => {
const MyInput = () => <div><input /></div>; // you get the idea
return (
<div>
<MyInput />
</div>
);
};
Do:
const MyComponent = () => {
return (
<div>
<div><input /></div> {/* you get the idea */}
</div>
);
};
Define the component outside its parent component:
const MyInput = ({value, onChange}) => (
<div>
<input value={value} onChange={onChange} />
</div>
);
const MyComponent = () => {
const [value, setValue] = useState('');
return (
<div>
<MyInput
value={value}
onChange={event => setValue(event.target.value)}
/>
</div>
);
};
For MUI V5
Moved your custom-styled code outside the component
For example:
import React from 'react';
import { useTheme, TextField, styled } from '#mui/material';
import { SearchOutlined } from '#mui/icons-material';
interface SearchInputProps { placeholder?: string; onChange: (event: React.ChangeEvent<HTMLInputElement>) => void; value: string; dataTest?: string; }
const StyledSearchInput = styled(TextField)(({ theme }: any) => { return {
'& .MuiOutlinedInput-root': {
borderRadius: '0.625rem',
fontSize: '1rem',
'& fieldset': {
borderColor: `${theme.palette.text.secondary}`
},
'&.Mui-focused fieldset': {
borderColor: `${theme.palette.primary}`
}
} }; });
const SearchInput: React.FC<SearchInputProps> = ({ placeholder = 'Search...', onChange, value, dataTest, ...props }) => { const theme = useTheme();
return (
<StyledSearchInput
{...props}
onChange={onChange}
placeholder={placeholder}
variant="outlined"
value={value}
inputProps={{ 'data-testid': dataTest ? dataTest : 'search-input' }}
InputProps={{
startAdornment: (
<SearchOutlined
sx={{ color: theme.palette.text.secondary, height: '1.5rem', width: '1.5rem' }}
/>
)
}}
/> ); };
export default SearchInput;
Be careful about your components' keys, if you set dynamic value as a key prop, it will also cause focus lost. Here is an example
{people.map(person => (
<div key={person.name}>
<Input type="text" value={person.name} onChange={// function which manipulates person name} />
</div>
))}

How to clear TextField of Materal ui

I have created a form with material ui, and I would like to clear it. I have searched for answers/solutions across Stackoverflow but all of them are not working for me.
I have tried by using setstate but it not result to clear the form.
I want to know how to do this using onclick event of the button.
import React, { Component } from "react";
import {
Grid,
TextField,
AppBar,
Typography,
Toolbar,
Button
} from "#material-ui/core";
export class MyInput extends Component {
constructor(props) {
super(props);
this.state = {
first_name: "",
last_name: ""
};
}
handle_input = (name, value) => {
let cur_state = this.state;
cur_state[name] = value;
this.setState(cur_state);
};
render() {
return (
<Grid container justify="center">
<Grid item md={4}>
<TextField
defaultValue={this.state.first_name}
variant="outlined"
label="First Name"
onKeyUp={(e) => {
this.handle_input("first_name", e.target.value);
}}
/>
</Grid>
<Grid item md={4}>
<TextField
defaultValue={this.state.last_name}
variant="outlined"
label="Last Name"
onKeyUp={(e) => {
this.handle_input("last_name", e
.target.value);
}}
/>
</Grid>
<Grid item md={4}>
<div
style={{
width: "100%",
padding: "10px",
display: "flex",
justifyContent: "space-between"
}}
>
<Button color="primary" variant="contained">
save
</Button>
<Button
color="secondary"
variant="contained"
onClick={() => {
this.setState({
first_name: "",
last_name: ""
});
}}
>
cancel
</Button>
</div>
</Grid>
</Grid>
);
}
}
export default MyInput;
Inside the TextField set a value,
<TextField
value={this.state.first_name}
defaultValue={this.state.first_name}
variant="outlined"
label="First Name"
onKeyUp={(e) => {
this.handle_input("first_name", e.target.value);
}}
/>
Then when you want to clear it, just use setState
this.setState({ first_name: '' })
Edit:
In fact, you can delete defaultValue as the default is an empty string anyways, which is what you're using.
<TextField
value={this.state.first_name}
variant="outlined"
label="First Name"
onKeyUp={(e) => {
this.handle_input("first_name", e.target.value);
}}
/>
It may be worth reading the react docs, basically you're wanting to override the form's internal state, which you do by using the value prop
I dont know if this will help as I am pretty new to react myself but you could try making a functional component instead of a class to help reduce the amount of code you need, i would do something like this
import React, {useState} from "react";
export default function MyInput(props) {
const [myState, setMyState] = useState(props);
function clearState() {
setMyState.first_name("")
setMyState.last_name("")
}
return (
//Your grid code here
)
//set your button on click to this
onClick = {() => {clearState()}}
Look into writing components like this its quite nice. Again this will need tweaking but may be a point in the right direction.

Textfields not tracking text

I have a forum setup using react-redux and material ui. All of the text fields seem to not track the change of state this is suppose to track..
So when a user attempts to type in the text field nothing happens.
onChange={e =>
onTextFieldChange("workoutCompleted", e.target.value)
}
here is the rest of the forum code im sure its a real simple fix here.
import React from "react"
import { connect } from "react-redux"
import TextField from "#material-ui/core/TextField"
import Button from "#material-ui/core/Button"
import CustomSnackBar from "../Components/customSnackBar"
import { withStyles } from "#material-ui/core/styles"
import { NEW_WORKOUT_FORM_UPDATED } from "../constants"
import { createNewWorkout } from "../action-creators/events"
const styles = theme => ({
input: {
width: "50%",
marginLeft: 16,
marginTop: 16,
marginBottom: 10,
color: "red"
},
button: {
color: "secondary"
}
})
class NewWorkout extends React.Component {
componentDidMount() {
console.log("componentDidMount!!!")
}
render() {
console.log("RENDER!!!")
const { workout, duration, eventDateTime } = this.props.event
const {
isError,
isSaving,
errorMsg,
classes,
onTextFieldChange,
createWorkout,
history
} = this.props
return (
<div style={{ paddingTop: 56 }}>
<form autoComplete="off" onSubmit={createWorkout(history)}>
<TextField
label="Workout"
value={workout}
onChange={e =>
onTextFieldChange("workoutCompleted", e.target.value)
}
margin="normal"
required
className={classes.input}
/>
<TextField
label="Duration"
value={duration}
onChange={e => onTextFieldChange("Duration", e.target.value)}
margin="normal"
required
className={classes.input}
multiline
/>
<TextField
label="Date"
value={eventDateTime}
clickable="false"
margin="normal"
required
className={classes.input}
/>
<div style={{ paddingTop: 50 }}>
<Button
className={classes.button}
color="red400"
variant="contained"
type="submit"
aria-label="add"
>
SUBMIT
</Button>
</div>
</form>
{isError && <CustomSnackBar message={errorMsg} snackType="error" />}
{isSaving && <CustomSnackBar message="Saving..." snackType="info" />}
</div>
)
}
}
const mapStateToProps = state => {
console.log("What is state?", state)
return {
event: state.newWorkout.data,
isError: state.newWorkout.isError,
isSaving: state.newWorkout.isSaving,
errorMsg: state.newWorkout.errorMsg
}
}
const mapActionsToProps = dispatch => {
return {
onTextFieldChange: (key, value) => {
dispatch({ type: NEW_WORKOUT_FORM_UPDATED, payload: { [key]: value } })
},
createWorkout: history => e => {
e.preventDefault()
dispatch(createNewWorkout(history))
}
}
}
const connector = connect(
mapStateToProps,
mapActionsToProps
)
export default connector(withStyles(styles)(NewWorkout))
onChange={e =>
onTextFieldChange("workoutCompleted", e.target.value)
}
should be
onChange= e => {
onTextFieldChange("workoutCompleted", e.target.value)
}
no?
Thanks for the help #UXDart
you are changing "workoutCompleted" but then checking the property
inside event "workout"... anyway IMO use state for that, and send to
redux when submit the form

Categories