I'm using material-table to display a table, with table data being retrieved by calling an api and storing in a state hook. I also have a hidden modal that is opened via material-table's add button full of input fields, where each input field has an associate state variable in the overall component, to be used to add new rows.
My issue is when I type into an input field in the modal, there is a noticeable delay between typing and the change rendering - especially if I mash the buttons.
It appears that Material-Table re-renders itself or checks if anything in the table changes with every change to an input and is the cause of the lag.
The issue can be reduced to the below code structure (it seems to occur if input is associated with a state variable).
const component = () => {
const [data, setData] = useState('');
const [var, setVar] = useState('');
useEffect(// Call API and set data, []);
return (
<div>
<MaterialTable></MaterialTable>
<input value={var} onChange={x => setVar(x.target.value)}></input>
</div>)
}
I'm wondering if there is a solution to my lag issue or is material-table designed this way?
Edit. Included sandbox. Typing really fast in the input has a noticeable lag.
https://codesandbox.io/s/material-table-yjbpr?file=/src/App.js
Edit 2. Updated sandbox with CEich's solution. There appears to be a noticeable lag if you hold backspace/hold a key.
https://codesandbox.io/s/material-table-2-8g98l?file=/src/App.js
Try using useCallback
import React, { useState, useCallback } from "react";
import MaterialTable from "material-table";
import "./styles.css";
export default function App() {
const [name, setName] = useState("");
const handleChange = useCallback((e) => {setName(e.target.value)}, [setName])
const table = (
<MaterialTable
columns={[
{ title: "First Name", field: "Name" },
{ title: "Surname", field: "Surname" }
]}
data={[{ Name: "John", Surname: "Doe" }]}
title="Title"
/>
);
return (
<div className="App">
<h1>Hello {name}</h1>
<div>
<label>Type here fast -> </label>
<input value={name} onChange={handleChange} />
</div>
{table}
</div>
);
}
By putting the callback code directly in the jsx, you were causing the component to re-render every time the value changed.
useCallback keeps your handler the same unless something in the provided array changes. (Here, that's only the setName method). Two different functions are never considered equal, even if they have the same function body. So, when comparing your callback function, react would cause your component to re-render on every keystroke. For a smaller component, this probably might not make a difference. But for a more complex component, the issue is more obvious
Related
I have two components in my project.
One is Aapp.jsx
One is Child.jsx
When I called the state set function in Child 1; it is supposed to see mentioning child 1 in the console, but now it is showing child 3. It is weird and why is that?
I guess the reason is "key" in prop, so I added "key" there too. But the problem is still there.
Here is the code:
App:
import React,{useState,useEffect} from 'react';
import {Child} from './Child.jsx'
export function App(props) {
[message,setMessage]=useState('');
[showChild1,setShowChild1]=useState(true);
[showChild2,setShowChild2]=useState(true);
[showChild3,setShowChild3]=useState(true);
[child1data,setChild1data] = useState('child1');
[child2data,setChild2data] = useState('child2');
[child3data,setChild3data] = useState('child3');
useEffect(() => {
console.log('parent was rendered')
})
return (
<div className='App'>
<button onClick={()=>setShowChild1(!showChild1)}>Show child1</button>
{showChild1 && <Child key='1' data={child1data}/>}
<br/>
<br/>
<button onClick={()=>setShowChild2(!showChild2)}>Show child2</button>
{showChild2 && <Child key='2'data={child2data}/>}
<br/>
<br/>
<button onClick={()=>setShowChild3(!showChild3)}>Show child3</button>
<br/>
{showChild3 && <Child key='3' data={child3data}/>}
</div>
);
}
// Log to console
console.log('Hello console')
Child:
import React, {useState, useEffect} from 'react';
export const Child = (props) => {
const {data} = props;
[message,setMessage]=useState('');
useEffect(()=>{
console.log(data)
console.log(message)
})
return <>
<h1>This is {data}</h1>
<input onChange={((e)=>setMessage(e.target.value))}></input>
</>
}
For better illustrate, here is my code
https://playcode.io/940717
This is a tricky one but fortunately comes with a very simple fix.
TL;DR
In Child.jsx change this:
[message, setMessage] = useState('');
to this:
const [message, setMessage] = useState('');
The longer answer
When declaring variables without let, const, or var (in a non-strict environment) Javascript will create an implicit global variable. What this means is that your message and setMessage variables point to the last value they were assigned. I.e. The global variable message will be assigned to the result of the useState call in the last Child component that you render, which in your case is Child 3.
So, when you modify message, Child 3 detects that change and runs the effect for Child 3, not the Child component where the change was actually made.
You can see this in action by changing the Child component to this and examining the output in the console:
[message, setMessage] = useState("");
useEffect(() => {
console.log(data); // Will always log "child 3"
console.log(message);
});
return (
<>
<h1>This is {data}</h1>
<input
onChange={(e) => {
console.log(data); // Will log "child n" where n is the child you expect
setMessage(e.target.value);
}}
></input>
</>
);
You might also be curious as to why, if all of your Child components are referencing the same message variable, you're still able to change the inputs individually. This is because you're input elements are uncontrolled and their state is being managed by the browser natively.
You can see this in action by adding the following to the input element in your Child component:
<input value={message} onChange={e => setMessage(e.target.value)}></input>
What to do in the future
You should always, always, always declare your variables with let or const as it will save you headaches like this in the future. In fact, many modern transpilers such as Babel will throw an error if you don't.
When using uncontrolled inputs you should consider asking yourself why you need to do so. More often than not, a controlled input is what you actually want and in the cases where it's not, consider leaving a comment as to why it's not.
You forgot const in many places:
const [message, setMessage] = useState('');
Without the const keyword message and setMessage (and other stuff) leak to the window global object.
Working example
when "Search" is clicked, next page is loaded by the below code.
import React from "react";
import { Link } from "react-router-dom";
function name(){
return(
<div className="search">
<input type="text"placeholder="city name"></input>
<input type="text"placeholder="number of people"></input>
<p><Link to="/nextpage">Search</Link</p>
</div>
)
}
I want to take data of these input fields to fetch api using that data to make cards on next page.
How to do it?
Here's the general idea of how you could accomplish this. You need to store the form input (in your case, the city and number of people) in application state. You can do that using the useState hook. Now this state can be managed by your first component (the one which renders the input fields) and accessed by the second component (the one that will display the values).
Because the values need to be accessed by both components, you should store the state in the parent component (see lifting state up). In my example, I used App which handles routing and renders FirstPage and SecondPage. The state values and methods to change it are passed as props.
Here's how you initialise the state in the App component:
const [city, setCity] = useState(null);
const [peopleCount, setPeopleCount] = useState(null);
city is the value, setCity is a function which enables you to modify the state. We will call setCity when the user makes a change in the input, like this:
export const FirstPage = ({ city, setCity, peopleCount, setPeopleCount }) => {
...
const handleCityChange = (e) => {
setCity(e.target.value);
};
...
<input
type="text"
placeholder="city name"
value={city}
onChange={handleCityChange}
/>
When a change in the input is made, the app will call the setCity function which will update the state in the parent component. The parent component can then update the SecondPage component with the value. We can do it simply by passing the value as a prop:
<NextPage city={city} peopleCount={peopleCount} />
Now you can do whatever you want with the value in your SecondPage component. Here's the full code for it, where it just displays both values:
export const NextPage = ({ city, peopleCount }) => {
return (
<div>
<p>city: {city}</p>
<p># of people: {peopleCount}</p>
</div>
);
};
And here's the full working example: https://stackblitz.com/edit/react-uccegh?file=src/App.js
Note that we don't have any field validation, and we have to manually write the change handlers for each input. In a real app you should avoid doing all this by yourself and instead use a library to help you build forms, such as Formik.
I have a huge form and sumbit is triggered from outside the form
<App>
<Form/>
<Button/>
</App>
The problem is that I need to have current form fields object in button component. I've tried to pass state through multiple layers by passing setState function:
const [formFields, setFormFields] = useState(null);
<App>
<Form setData={setFormFields}/>
<Button data={formFields}/>
</App>
And also to use redux dispatch(on form field changes) and useSelector in button component to get current data. But both methods seems to really slow down the application when I'm writing some text in input fields.
What would be the best solution to optimize it?
really slow down the application when I'm writing some text in input fields.
This is because you're tying the form's data to the rendering. It would be fine to store the whole form through Redux as well (or wherever else you want), as long as you're not creating a dependency between the component's rendering lifecycle and those mutations in the form field's values.
Does the whole form need to update on each individual components' events? Not really. However, since the whole form is gathered in one big object, and that object's reference is changed after an update, then the whole component tree gets re-rendered.
To reduce the need for re-rendering, you can synchronize the data (note: the data, not the validations, etc...) outside of React, and only fetch it when you submit.
import React, { useState } from "react";
const externalFormData = {};
function Form() {
return (
<input
onChange={function(ev) {
externalFormData.input = ev.target.value;
}}
/>
);
}
export default function App() {
const [formData, setFormData] = useState();
return (
<div>
<Form />
<input
type={"button"}
onClick={function() {
setFormData(externalFormData);
}}
value="Submit form"
/>
<p>{`Submitted form is: ${JSON.stringify(formData)}`}</p>
</div>
);
}
If that's your choice, I suggest looking for some existing form library which handles that for you (including validation and etc).
Another idea would be decoupling this "big object" you mentioned into individual objects which gets updated separately, thus only triggering re-renders on the components affected by the value.
I've built several modals as React functional components. They were shown/hidden via an isModalOpen boolean property in the modal's associated Context. This has worked great.
Now, for various reasons, a colleague needs me to refactor this code and instead control the visibility of the modal at one level higher. Here's some sample code:
import React, { useState } from 'react';
import Button from 'react-bootstrap/Button';
import { UsersProvider } from '../../../contexts/UsersContext';
import AddUsers from './AddUsers';
const AddUsersLauncher = () => {
const [showModal, setShowModal] = useState(false);
return (
<div>
<UsersProvider>
<Button onClick={() => setShowModal(true)}>Add Users</Button>
{showModal && <AddUsers />}
</UsersProvider>
</div>
);
};
export default AddUsersLauncher;
This all works great initially. A button is rendered and when that button is pressed then the modal is shown.
The problem lies with how to hide it. Before I was just setting isModalOpen to false in the reducer.
When I had a quick conversation with my colleague earlier today, he said that the code above would work and I wouldn't have to pass anything into AddUsers. I'm thinking though that I need to pass the setShowModal function into the component as it could then be called to hide the modal.
But I'm open to the possibility that I'm not seeing a much simpler way to do this. Might there be?
To call something on unmount you can use useEffect. Whatever you return in the useEffect, that will be called on unmount. For example, in your case
const AddUsersLauncher = () => {
const [showModal, setShowModal] = useState(false);
useEffect(() => {
return () => {
// Your code you want to run on unmount.
};
}, []);
return (
<div>
<UsersProvider>
<Button onClick={() => setShowModal(true)}>Add Users</Button>
{showModal && <AddUsers />}
</UsersProvider>
</div>
);
};
Second argument of the useEffect accepts an array, which diff the value of elements to check whether to call useEffect again. Here, I passed empty array [], so, it will call useEffect only once.
If you have passed something else, lets say, showModal in the array, then whenever showModal value will change, useEffect will call, and will call the returned function if specified.
If you want to leave showModal as state variable in AddUsersLauncher and change it from within AddUsers, then yes, you have to pass the reference of setShowModal to AddUsers. State management in React can become messy in two-way data flows, so I would advise you to have a look at Redux for storing and changing state shared by multiple components
I currently use
<TextField onChange={e => this.change(e, items)}
This gets fired at every single letter I put into the TextField, consequently, text I type gets filled in slow motion. I was thinking it'd be better if this request goes out once the user types everything and focuses away. What kind of event I can use in this scenario with React & Material UI TextField ?
Having a debounced version of your function is useful when any DOM event is attached. It reduces the number of calls to this function to the bare minimum and thereby improve the performance.
So, you can do this:
import _ from 'lodash';
constructor(props) {
super(props)
this.onChangeDebounce = _.debounce(e => this.change(e, items), 300);
}
render() {
...
onChange={e => this.onChangeDebounce(e)}
...
}
In this case, I am passing to debounce only two parameters:
The function to be rate-limited
The rate limit in milliseconds, ie., the time to wait before the function is fired.
Or you can use onBlur event, that is available for any DOM element. The onBlur event happens whenever an input loses focus on. In other words: when you remove your cursor from within the input, it loses "focus", or becomes "blurry".
The caveat is that it doesn't have an associated event, so, to reach what you want, you can update the state with the field value and on onBlur, retrieve this value from state.
Here you have a fiddle doing this.
After one and a half year, here is a contemporary approach mostly for functional components:
Keep the value of the field as a React state.
Set it onChange
Persist (or do whatever expensive process) onBlur
So, a component containing this text field would, in a way, look like similar to this:
import React, { useState } from 'react'
import { TextField } from '#material-ui/core'
const MyFunctionalComponent = () => {
const [textFieldValue, setTextFieldValue] = useState('')
// ...
return (
<React.Fragment>
{/** ... */}
<TextField
value={textFieldValue}
onChange={(e) => setTextFieldValue(e.target.value)}
onBlur={() => {
console.log(`I am blurred and ready to process ${textFieldValue}`)
}}
/>
{/** ... */}
</React.Fragment>
)
}