Input event
public handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ emptyFields: false, error: false, loading: false });
this.setState({ product: { ...this.state.product, [e.target.name]: e.target.value } });
}
Map test
<tbody>
{this.props.products.map((prod: IProduct) =>{
console.log('remap ???')
return (<tr key={prod.id}>
<td>{prod.id}</td>
<td>{prod.name}</td>
<td>{prod.price}</td>
</tr>)
}
)}
</tbody>
When I change the input, this map is made again as many times as I change the input.
When you change the state, react will call the render method again.This is expected.
Break out parts of your html in seperate components and make the components pure. This will prevent needless re render of DOM. However; at the moment it won't re render dom because virtual DOM compare of React will optimize. You will get in trouble if each row gets props that are recreated every time the parent renders, like not using useCallback for the delete callback:
//use React.memo to create a pure component
const ProductRow = React.memo(function ProductRow({
product: { id, name },
onDelete,
}) {
console.log('generating jsx for product:', id);
return (
<tr>
<td>{id}</td>
<td>{name}</td>
<td>
<button onClick={() => onDelete(id)}>X</button>
</td>
</tr>
);
});
//React.memo is pure component, only re renders if
// props (=products or onDelete) change
const Products = React.memo(function Products({
products,
onDelete,
}) {
return (
<table>
<tbody>
<tr>
<th>id</th>
<th>name</th>
</tr>
{products.map((product) => (
<ProductRow
key={product.id}
product={product}
onDelete={onDelete}
/>
))}
</tbody>
</table>
);
});
const id = ((id) => () => ++id)(0); //create id
const AddProduct = React.memo(function AddProduct({
onAdd,
}) {
const [name, setName] = React.useState('');
//no use to use useCallback, this function re creates
// when name changes
const save = () => {
onAdd(name);
setName('');
};
return (
<div>
<label>
name:
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
<button onClick={save}>save</button>
</div>
);
});
const App = () => {
//initial products state
const [products, setProducts] = React.useState(() => [
{ id: id(), name: 'first product' },
{ id: id(), name: 'second product' },
]);
//use useCallback to create an add function on mount
// this function is not re created causing no needless
// re renders for AddProduct
const onAdd = React.useCallback(
(name) =>
setProducts((products) =>
products.concat({
id: id(),
name,
})
),
[]
);
//delete function only created on mount
const onDelete = React.useCallback(
(id) =>
setProducts((products) =>
products.filter((product) => product.id !== id)
),
[]
);
return (
<div>
<AddProduct onAdd={onAdd} />
<Products products={products} onDelete={onDelete} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Related
I'm working on react table project, and can be searched and filtered by name and email both. The problem is Whenever I input a value it gets the previous state first, not the updated real-time value. Why is it and how to solve that? Thank You!
const URL = "https://jsonplaceholder.typicode.com/users";
function App() {
const [data, setData] = useState([]);
const [searchInput, setSearchInput] = useState("");
const [filteredResults, setFilteredResults] = useState([]);
const getData = async () => {
const response = await axios.get(URL);
setData(response.data);
};
useEffect(() => {
getData()
}, []);
Filtering By Name
const nameFilter = (e) => {
if (e.target.value !== "") {
setSearchInput(e.target.value);
const results = data.filter((user) => {
return user.name.toLowerCase().includes(searchInput.toLowerCase());
});
setFilteredResults(results);
} else {
setFilteredResults(data);
}
};
Filtering by Email
const emailFilter = (e) => {
if (e.target.value !== "") {
setSearchInput(e.target.value);
const results = data.filter((user) => {
return user.email.toLowerCase().includes(searchInput.toLowerCase());
});
setFilteredResults(results);
} else {
setFilteredResults(data);
}
};
Table Header
const renderHeader = () => {
let headerElement = ["id", "name", "email", "Company Name", "Zipcode"];
return headerElement.map((key, index) => {
return <th key={index}>{key.toUpperCase()}</th>;
});
};
Table Body
const renderBody = () => {
return (
<>{
searchInput ? (
filteredResults.map(({ id, name, email, company, address }) => {
return (
<tr key={id}>
<td>{id}</td>
<td>{name}</td>
<td>{email}</td>
<td>{company.name}</td>
<td>{address.zipcode}</td>
</tr>
);
})
) :
data.map(({ id, name, email, company, address }) => {
return (
<tr key={id}>
<td>{id}</td>
<td>{name}</td>
<td>{email}</td>
<td>{company.name}</td>
<td>{address.zipcode}</td>
</tr>
);
})}
</>
);
};
return (
<>
<h1 id="title">React Table</h1>
{!data.length ? (
<h2>Loading...</h2>
) : (
<div>
<span>
<input
type="search"
onChange={nameFilter}
className="input"
placeholder="Search by Name"
/>
<input
type="search"
onChange={emailFilter}
className="input"
placeholder="Search by Email"
/>
</span>
<table id="employee">
<thead>
<tr>{renderHeader()}</tr>
</thead>
<tbody>{renderBody()}</tbody>
</table>
</div>
)}
</>
);
}
export default App;
This is because you set your filter state and after that immediately read its value (where the state isn't updated yet). To possible fixes:
Use event.target.value as value to filter with
Don't use a state value for the filtered results and instead use a "normal" variable (which gets recalculated every render, which occurs after every state change) and the value of this is the combined filtered result of both filters applied. Additionally you need a state to store the current input for the email and the name filter.
That's because the setState hook is asynchronous event
For that you have two solution you can use useEffect hook or use the callback syntax of useState
setFilteredResults(filteredResults=> [...filteredResults , results]);
Also as suggestion you use e.target.value directly in the includes method
I am learning React. I want to display a map on which markers are dynamically shown and hidden. This works. I use Maplibre GL, which is a fork of Mapbox GL and the handling is the same.
Unfortunately, however, when the markers are changed, the map is reloaded because a dependency is specified in the useEffect-Hook. Without it, however, the markers do not change. I cannot access the map (variable map) outside of useEffect.
What possibilities are there to prevent the map from being reloaded, but to adjust the markers dynamically.
This is my code:
import React from "react";
import maplibregl from "maplibre-gl";
const App = () => {
const pois = [
{
display_name: "backery a",
icon: "https://nominatim.openstreetmap.org/ui/mapicons//shopping_bakery.p.20.png",
lat: "50.4",
lon: "7.1",
importance: "0.111",
geojson: '{"type":"Point","coordinates":[7.1,50.4]}',
place_id: "1",
},
{
display_name: "backery b",
icon: "https://nominatim.openstreetmap.org/ui/mapicons//shopping_bakery.p.20.png",
lat: "51.4",
lon: "6.1",
importance: "0.211",
geojson: '{"type":"Point","coordinates":[6.1,51.4]}',
place_id: "2",
},
];
const [searchTerm, setSearchTerm] = React.useState(
localStorage.getItem("search") || "backery a"
);
React.useEffect(() => {
localStorage.setItem("search", searchTerm);
}, [searchTerm]);
const handleSearch = (event) => {
setSearchTerm(event.target.value);
};
const searchedpois = pois.filter((poi) =>
poi.display_name.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div>
<h1>React Maplibre Map</h1>
<Search searchValue={searchTerm} onSearch={handleSearch} />
<hr />
<List list={searchedpois} />
<hr />
<Map list={searchedpois} />
</div>
);
};
const Map = (props) => {
React.useEffect(() => {
const map = new maplibregl.Map({
container: "map",
style:
"https://api.maptiler.com/maps/streets/style.json?key=mykey",
center: [7.5, 50.1],
zoom: 4,
});
map.on("load", () => {
props.list.map((item) =>
new maplibregl.Marker().setLngLat([item.lon, item.lat]).addTo(map)
);
});
}, [props.list]);
return <div></div>;
};
const Search = ({ searchValue, onSearch }) => (
<div>
<label htmlFor="search">Suche: </label>
<input id="search" type="text" onChange={onSearch} value={searchValue} />
</div>
);
const List = (props) => (
<div>
<table>
<tbody>
{props.list.map((item) => (
<POI key={item.place_id} poi={item} />
))}
</tbody>
</table>
</div>
);
const POI = ({ poi }) => (
<tr key={poi.place_id}>
<td>{poi.display_name}</td>
<td>{poi.lat}</td>
<td>{poi.lon}</td>
<td>{poi.importance}</td>
<td>
<img alt="" src={poi.icon} />
</td>
</tr>
);
export default App;
I have created this dynamic input table, but I want to make it reusable and more dynamic. Here, the name and age are static. If I want to send the name, age, country, region, etc. as props, this code won't work. How can I do this?
Here is my code:
import React, { useState } from 'react';
const DynamicTable = () => {
const [fields, setFields] = useState([{ name: '', age: 0 }]);
const onChangeHandler = (e, index) => {
let name = '';
let age = 0;
if (e.target.name === 'name') {
name = e.target.value;
console.log(name, age);
} else {
age = e.target.value;
console.log(name, age);
}
// Set Field Value
const newFields = fields.map((item, i) => {
if (index === i) {
if (e.target.name === 'name') {
return { ...item, name };
} else {
return { ...item, age };
}
} else {
return item;
}
});
setFields(newFields);
console.log(newFields);
};
// For delete button
const deleteHandler = (index) => {
const newField = fields.filter((item, i) => i !== index);
setFields(newField);
};
return (
<div>
<div>
<button onClick={() => setFields([...fields, { name: '', age: 0 }])}>
Add
</button>
</div>
<table>
<tbody>
{fields.map((item, index) => (
<tr key={index}>
<td>
Name:
<input
type='text'
name='name'
onChange={(e) => onChangeHandler(e, index)}
/>
</td>
<td>
Age:
<input
type='text'
name='age'
onChange={(e) => onChangeHandler(e, index)}
/>
</td>
<td>
<button onClick={() => deleteHandler(index)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
<div>
<button onClick={() => console.log(fields)}>Submit</button>
</div>
{/* Test input value working correctly */}
<div>
{fields.map((item) => (
<p>
{item.name} - {item.age}
</p>
))}
</div>
{/* ---- */}
</div>
);
};
export default DynamicTable;
Note: It would be great if we can delete and add rows based on the checkbox. Also, applying CSS throw props for buttons or other elements would be awesome.
To make the fields dynamic and the component overall more generic:
Pass a fields prop object that defines the fields for a single row.
Create a utility that wraps a fields object with an id for easy identification (this comes in handy when mutating the array rows since using an array index won't work).
Adjust handlers to manage rows by id and the new state shape.
Code:
import React, { useState } from "react";
import { v4 as uuidV4 } from "uuid";
import "./styles.css";
const createFieldsObject = (fields) => ({
id: uuidV4(), // <-- generate unique Id, can use anything really
fields
});
const DynamicTable = ({ fields }) => {
const [fieldRows, setFieldRows] = useState([createFieldsObject(fields)]);
const onChangeHandler = (id) => (e) => {
const { name, value } = e.target;
setFieldRows((rows) =>
rows.map((row) =>
row.id === id
? {
...row,
fields: {
...row.fields,
[name]: value
}
}
: row
)
);
};
const addHandler = () =>
setFieldRows((rows) => [...rows, createFieldsObject(fields)]);
const deleteHandler = (id) => () =>
setFieldRows((rows) => rows.filter((row) => row.id !== id));
const getFieldRows = () => fieldRows.map(({ fields }) => fields);
const submitHandler = e => {
e.preventDefault();
console.log(getFieldRows());
}
return (
<form onSubmit={submitHandler}>
<div>
<button onClick={addHandler}>Add</button>
</div>
<table>
<tbody>
{fieldRows.map(({ id, fields }, index) => (
<tr key={id}>
{Object.entries(fields).map(([key, value]) => (
<td key={key}>
{key}:
<input
type="text"
name={key}
onChange={onChangeHandler(id)}
value={value}
/>
</td>
))}
<td>
<button onClick={deleteHandler(id)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
<div>
<button type="submit">Submit</button>
</div>
{/* Test input value working correctly */}
<div>
{fieldRows.map(({ id, fields }) => (
<p key={id}>{Object.values(fields).join(" - ")}</p>
))}
</div>
{/* ---- */}
</form>
);
};
Example usage:
<DynamicTable fields={{ Name: "", Age: 0 }} />
<DynamicTable fields={{ Name: "", Age: 0, Location: "" }} />
I have a list of products and I want to add a few more data like price and quantity. The problem is I lose input focus when I start typing because the entire list is being re-rendered. I have a simple Fiddle replicating that piece of code:
const App = () => {
// List of products
const [products, setProducts] = React.useState([
{
title: 'My Product',
},
{
title: 'My Product 2',
}
]);
// Simple debug to track changes
React.useEffect(() => {
console.log('PRODUCTS', products);
}, [products]);
const ProductForm = ({ data, index }) => {
const handleProductChange = (name, value) => {
const allProducts = [...products];
const selectedProduct = {...allProducts[index]};
allProducts[index] = {
...selectedProduct,
[name]: value
};
setProducts([ ...allProducts ]);
}
return (
<li>
<h2>{data.title}</h2>
<label>Price:</label>
<input type="text" value={products[index].price} onChange={(e) => handleProductChange('price', e.target.value)} />
</li>
);
}
return <ul>{products.map((item, index) => <ProductForm key={item.title} index={index} data={item} />)}</ul>;
}
ReactDOM.render(
<App />,
document.getElementById('container')
);
https://jsfiddle.net/lucasbittar/zh1d6y37/23/
I've tried a bunch of solution I found here but none of them really represents what I have.
Thanks!
Issue
You've defined ProductForm internally to App so it is recreated each render cycle, i.e. it's a completely new component each render.
Solution
Move ProductForm to be externally defined. This now has issue where the handleProductChange doesn't have access to App's functional scope products state. The solution here is to move handleProductChange back into App and update the function signature to consume the index. Use data.price as the input value, you can provide a fallback value or provide initial state for this property.
Suggestion: Name the input name="price" and simply consume it from the event object.
const ProductForm = ({ data, index, handleProductChange }) => {
return (
<li>
<h2>{data.title}</h2>
<label>Price:</label>
<input
name="price" // <-- name input field
type="text"
value={data.price || ''} // <-- use data.price or fallback value
onChange={(e) => handleProductChange(e, index)} // <-- pass event and index
/>
</li>
);
}
const App = () => {
// List of products
const [products, setProducts] = React.useState([
{
title: 'My Product',
},
{
title: 'My Product 2',
}
]);
// Simple debug to track changes
React.useEffect(() => {
console.log('PRODUCTS', products);
}, [products]);
// Update signature to also take index
const handleProductChange = (e, index) => {
const { name, value } = e.target; // <-- destructure name and value
const allProducts = [...products];
const selectedProduct = {...allProducts[index]};
allProducts[index] = {
...selectedProduct,
[name]: value
};
setProducts([ ...allProducts ]);
}
return (
<ul>
{products.map((item, index) => (
<ProductForm
key={item.title}
index={index}
data={item}
handleProductChange={handleProductChange} // <-- pass callback handler
/>)
)}
</ul>
);
}
ReactDOM.render(
<App />,
document.getElementById('container')
);
Working jsfiddle demo
One way to solve the issue would be to have a separate component for <ProductForm /> and pass the required props there.
const ProductForm = ({ data, index, products, setProducts }) => {
const handleProductChange = (name, value) => {
const allProducts = [...products];
const selectedProduct = {...allProducts[index]};
allProducts[index] = {
...selectedProduct,
[name]: value
};
setProducts([ ...allProducts ]);
}
return (
<li>
<h2>{data.title}</h2>
<label>Price:</label>
<input
type="text"
value={products[index].price || ''}
onChange={(e) => handleProductChange('price', e.target.value)}
/>
</li>
);
}
const App = () => {
const [products, setProducts] = React.useState([{title: 'My Product'},{title: 'My Product 2'}]);
return (
<ul>
{products.map((item, index) =>
<ProductForm
key={item.title}
index={index}
data={item}
products={products}
setProducts={setProducts}
/>
)}
</ul>
)
}
ReactDOM.render(
<App />,
document.getElementById('container')
);
Here is the working JSFiddle code link
export default function App() { return(<div><customButton/><customTable/></div>) }
export default function customButton() { return(<div><button>update</button></div>) }
Since the app is the parent component and button and table are child components. I know this is not the best practice but how do I update(re-render) table component from the button component?
The way to do this would be to have some shared state at the closest ancestor. In the example below, I create a value state variable in App and pass it to the customTable element. I have a setValue setter that is passed to customButton. When the button is clicked, the value is updated and passed to customTable, causing the table to re-render with that new value.
export default function App() {
const [value, setValue] = React.useState(0);
return(
<div>
<customButton setValue={setValue} />
<customTable value={value} />
</div>
)
}
export default function customButton({ setValue }) {
return(
<div>
<button onClick={() => setValue(v => v + 1)}>update</button>
</div>
)
}
Sample application using custom table and custom button.
function CustomButton(props) {
return <button onClick={props.addClick}>{props.children}</button>
}
function CustomTable({ table }) {
return (
<table>
<thead>
<tr>
<th>No.</th>
<th>Name</th>
</tr>
</thead>
<tbody>
{table.map((item, i) => (
<tr key={i.toString()}>
<td>{i.toString()}</td>
<td>{item.name}</td>
</tr>
))}
</tbody>
</table>
);
}
function App() {
const [input, setInput] = React.useState('')
const [table, setTable] = React.useState([
{ name: "John" },
{ name: "Bukky" },
{ name: "Behem" }
]);
const handleInput = e=>{
setInput(e.target.value)
}
const addClick = e => {
const update = [...table, { name: input }];
setTable(update);
setInput('')
};
return (
<div>
<input type="text" value={input} onChange={handleInput}/>
<CustomButton addClick={addClick}>Click to Add</CustomButton>
<CustomTable table={table} />
</div>
);
}