MUI Custom Text Field loses focus on state change - javascript

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>
))}

Related

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>
);
}

MaskedInput text is not correctly displayed when it received through the props

I have a a Material UI OutlinedInput on top of which I'm using MaskedInputfrom react-text-mask, when I originally put text into it and element is not in focus, the inputted text displays correctly, however when I close down the Dialog window with this component and reopen there's some strange behavior with text overplapping over placeholder text.
That looks like this:
When I originally input text:
Here's my code for this component:
const SERIES_MASK = [/\d/, /\d/, " ", /\d/, /\d/];
const useStyles = makeStyles({
inputLabel: {
margin: "-8px 0 0 14px",
},
});
const SeriesMask: FC<{}> = (props) => <MaskedInput {...props} mask={SERIES_MASK} />;
export const DocumentSeriesField: FC<{
name: string;
value: string;
label: string;
error?: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onBlur: (e: React.FocusEvent<HTMLInputElement>) => void;
}> = ({ name, value, label, error, onChange, onBlur }) => {
const classes = useStyles();
const id = useMemo(() => randomId("series"), []);
return (
<FormControl>
<InputLabel htmlFor={id} error={Boolean(error)} classes={{ root: classes.inputLabel }}>
{label}
</InputLabel>
<OutlinedInput id={id} name={name} value={value} label={label} inputComponent={SeriesMask} error={Boolean(error)} onChange={onChange} onBlur={onBlur} />
<FormHelperText error={Boolean(error)}>{error}</FormHelperText>
</FormControl>
);
};
Could you please tell me what could be a possible issue here and how could it be fixed?
I think you are missing reference of the Outlined Input in MaskedInput
Try to pass inputRef to MaskedInput, the label should be able to find the field is filled and go back up to the border instead of overlaying.
SeriesMask
interface SeriesMaskRefProps {
inputRef: (ref: HTMLInputElement | null) => void;
}
const SeriesMask: FC<{}> = (props: InputBaseComponentProps) => (
<MaskedInput
{...props}
ref={(ref: any) => {
(props as SeriesMaskRefProps).inputRef(ref ? ref.inputElement : null);
}} // pass the ref to allow input label to connect with maaksed input
mask={SERIES_MASK}
/>
);

How to debounce search function in this React Component?

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
/>
}

why setItem aren't letting 'edit button' work?

i am trying to put a button of edit for each item with the help of simple hook. i have used map to see the elements of an item.... but somehow it's not working...while pressing the button nothing is showing.....
Codesandbox link: https://codesandbox.io/s/condescending-worker-s0igh
app.js:
import React, { useState } from "react";
import TodoList from "./TodoFiles/TodoList";
const defaultItems = [
{ id: 1, title: "Write React Todo Project", completed: true },
{ id: 2, title: "Upload it to github", completed: false }
];
const App = () => {
const [items,setItems]=useState(defaultItems)
const editItem=({id,title})=>{
setItems(items.map(p=>p.id===id?{...p,title}:p))
}
return (
<div style={{ width: 400 }}>
<hr />
<TodoList
items={items}
editItem={editItem}/>
<hr />
</div>
);
};
export default App;
TodoList.js:
import React from "react";
const TodoItem = ({ title, completed, editItem }) => {
return (
<div style={{ width: 400, height: 25 }}>
<input type="checkbox" checked={completed} />
{title}
<button style={{ float: "right" }} onClick={() => editItem(title)}>
Edit
</button>
</div>
);
};
const TodoList = ({ items = [], index,editItem }) => {
return items.map((p, index) => (
<TodoItem
{...p}
key={p.id}
index={index}
editItem={editItem}
/>
));
};
export default TodoList;
i don't want to use useEffect or useReducer fro custom hook ... because i want to practice with basics. sorry for my frequent questions, i'm trying so hard to learn this reactjs and i dun want to give up... thanx in advance and if it is possible, will you put some explanations in a plain text, why it's not working.
You forgot to pass id to editItem function on button onClick in TodoItem component.
Also you shouldn't use old state in setState function like that (in editItem function). You should use an updater function argument. Thanks to that you always modify current state.
const editItem = ({id,title}) => {
setItems(oldItems => (olditems.map(p=>p.id===id?{...p,title}:p)))
}
import React from "react";
const TodoItem = ({id, title, completed, editItem }) => {
return (
<div style={{ width: 400, height: 25 }}>
<input type="checkbox" checked={completed} />
{title}
<button style={{ float: "right" }} onClick={() => editItem({id,title})}>
Edit
</button>
</div>
);
};
const TodoList = ({ items = [], index,editItem }) => {
return items.map((p, index) => (
<TodoItem
{...p}
key={p.id}
index={index}
editItem={editItem}
/>
));
};
export default TodoList;

React Input losing focus after keystroke

I'm building a an app in React/Redux and I have some input fields that lose focus after each key stroke. My code looks like this:
<General fields={props.general}
onChange={value => props.updateValue('general', value)} />
<FormWrapper>
<Network fields={props.network}
onChange={value => props.updateValue('network', value} />
</NetworkTableForm>
</FormWrapper>
General and Network just contain different sections of the form and FormWrapper can wrap a subsection of the form and provide some interesting visualisations. The problem is that all of the fields in General work fine but the fields in Network lose focus after each keystroke. I've already tried appending keys to a bunch of different elements such as General Network, FormWrapper and my components wrapping my input fields and the reason that I'm not using redux-form is because it doesn't work well with the UI framework that I'm using(Grommet). Any idea what could be causing this?
Edit: Adding more code as per request. Here's the full component:
const InfoForm = (props) => {
let FormWrapper = FormWrapperBuilder({
"configLists": props.configLists,
"showForm": props.showForm,
"loadConfiguration": props.loadConfiguration,
"updateIndex": props.updateIndex,
"metadata": props.metadata})
return (
<CustomForm onSubmit={() => console.log('test')}
loaded={props.loaded} heading={'System Details'}
header_data={props.header_data}
page_name={props.general.name} >
<General fields={props.general}
onChange={(field, value) => props.updateValue('general', field, value)} />
<FormWrapper labels={["Interface"]} fields={["interface"]} component={'network'}>
<Network fields={props.network}
onChange={(field, value) => props.updateValue('network', field, value)} />
</FormWrapper>
<User fields={props.user}
onChange={(field, value) => props.updateValue('user', field, value)} />
</CustomForm>
)
}
export default InfoForm
Here's an example of a form section component:
const Network = ({fields, onChange}) => (
<CustomFormComponent title='Network'>
<CustomTextInput value={fields.interface} label='Interface'
onChange={value => onChange('interface', value)} />
<CustomIPInput value={fields.subnet} label='Subnet'
onChange={value => onChange('subnet', value)} />
<CustomIPInput value={fields.network} label='Network'
onChange={value => onChange('network', value)} />
<CustomIPInput value={fields.gateway} label='Gateway'
onChange={value => onChange('gateway', value)} />
<CustomIPInput value={fields.nat_ip} label='NAT'
onChange={value => onChange('nat_ip', value)} />
</CustomFormComponent>
)
export default Network
Here's an example of one of the custom inputs:
const CustomTextInput = ({label, value, onChange}) => (
<FormField label={<Label size='small'>{label}</Label>}>
<Box pad={{'horizontal': 'medium'}}>
<TextInput placeholder='Please Enter' value={value}
onDOMChange={event => onChange(event.target.value)}
/>
</Box>
</FormField>
)
export default CustomTextInput
And here's the FormWrapperBuilder function:
const FormWrapperBuilder = (props) =>
({component, labels, fields, children=undefined}) =>
<VisualForm
configLists={props.configLists[component]}
showForm={visible => props.showForm(component, visible)}
loadConfiguration={index => props.loadConfiguration(component, index)}
updateIndex={index => props.updateIndex(component, index)}
metadata={props.metadata[component]}
labels={labels} fields={fields} >
{children}
</VisualForm>
export default FormWrapperBuilder
All of these are kept in separate files which may affect the rerendering of the page and it's the functions that are wrapped in the FormWrapper that lose focues after a state change. Also I've tried adding keys to each of these components without any luck. Also here's the functions in my container component:
const mapDispatchToProps = (dispatch) => {
return {
'updateValue': (component, field, value) => {
dispatch(updateValue(component, field, value))},
'showForm': (component, visible) => {
dispatch(showForm(component, visible))
},
'loadConfiguration': (component, index) => {
dispatch(loadConfiguration(component, index))
},
'pushConfiguration': (component) => {
dispatch(pushConfiguration(component))
},
'updateIndex': (component, index) => {
dispatch(updateIndex(component, index))
}
}
}
Edit 2: Modified my custom text input as such as per Hadas' answer. Also slathered it with keys and refs for good measure
export default class CustomTextInput extends React.Component {
constructor(props) {
super(props)
this.state = {value: ""}
}
render() {
return (
<FormField key={this.props.label} ref={this.props.label} label={<Label size='small'>{this.props.label}</Label>}>
<Box key={this.props.label} ref={this.props.label} pad={{'horizontal': 'medium'}}>
<TextInput placeholder='Please Enter' value={this.state.value} key={this.props.label} ref={this.props.label}
onDOMChange={event => { this.props.onChange(event.target.value); this.state.value = event.target.value }} />
</Box>
</FormField>
)
}
}
React is re-rendering the DOM on each key stroke. Try setting value to a variable attached to the component's state like this:
<TextInput
style={{height: 40, borderColor: 'gray', borderWidth: 1}}
onChangeText={(text) => this.setState({text})}
value={this.state.text}></TextInput

Categories