useState not causing rerender when objects array changes *hooks* - reactjs

I have an object, which has a number of keys, and at each key, there is an array of objects. Each object contains an image file that I need to show to preview the image that the user has just uploaded. But it's showing the image that was uploaded the previous time (not the one that's been just uploaded)
Apparently useState doesn't immediately cause a rerender unless it sees a change in the array or object How do I update states onchange in an array of object in React Hooks
I followed the above (and a number of similar suggestions from StackOverflow) and made a copy of the object and a copy of the array at the key but it still doesn't work
Any idea why?
const Form = props => {
let [images, setImages] = useState({
bannerPicture: [],
projectPictures: [],
projectsPictures: [],
thumbnailPictures: []
})
const showTempImage = (file, tempFileObj, key) => {
const imagesCopy = {...images}
// add this image to the end of the array at the given key
imagesCopy[key] = [...imagesCopy[key], tempFileObj]
setImages({...imagesCopy})
....
}
I just want the image to show immediately after the user uploads it

Do it from your async function.
I'm assuming you'll only show the image after the upload is complete.
const mockUploadApi = () => {
return new Promise((resolve,reject) => {
setTimeout(() => {
resolve("uploadedImage");
},1000);
});
};
const App = () => {
const [state,setState] = React.useState({
status: "IDLE",
image: ""
});
const uploadImage = () => {
setState((prevState) => ({
...prevState,
status: "UPLOADING"
}));
// THIS SHOULD USE await BUT THE SNIPPET DOES NOT ALLOW IT
// SO I'M USING .then()
mockUploadApi().then((uploadedImage) => {
setState({
status: "IDLE",
image: uploadedImage
});
});
};
return(
<div>
<div>
status: {state.status}
</div>
<div>
image: {state.image || "no image"}
</div>
<div>
<button onClick={uploadImage}>
Upload
</button>
</div>
</div>
);
};
ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>

Related

useState in parent component not keep state, when parent component's callback function is called from child component

Update
This maybe a Formik bug, and I have switched to https://react-hook-form.com, as Formik has not been updated for a while.
https://github.com/jaredpalmer/formik/issues/3716
Context
I'm using React, Formik, and google-map-react to allow store owner edit their store address with google map place autocomplete.
I have three components:
EditStoreInfoPage is the page component, which contains EditStoreInfoForm.
EditStoreInfoForm is the form component, which contains FormikAddressField. I uses Formik here.
FormikAddressField is the one form field that supports google place autocomplete.
Store information will be fetched from backend in EditStoreInfoPage, and passed down to EditStoreInfoForm and FormikAddressField. Whenever a new address is typed in FormikAddressField, it calls a callback function handleStoreLocationUpdate passed down from EditStoreInfoPage.
Issue
Render the page without any issue. I see that formValues are populated corrected with the data fetched from backend.
However, once I finished typing the address, the form get cleared except the store address is still there.
From the console output of above screenshot, I can see that function handleStoreLocationUpdate get called, however, console.log(formValues); in function handleStoreLocationUpdate of EditStoreInfoPage contains empty value for store fields. I was expecting that the formValues here still kept the value fetched from backend, not sure why these values get wiped out as I use React useState.
Any idea what went wrong?
Code
EditStoreInfoPage
This is the React component that first call backend API to get the store information based on storeIdentifier. formValues will be populated with these information, as you can see that setFormValues is being called. formValues is passed down to child component EditStoreInfoForm as props.
type EditStoreInfoPageProps = {
storeIdentifier: string;
};
const EditStoreInfoPage = (props: EditStoreInfoPageProps) => {
let navigate = useNavigate();
const [formValues, setFormValues] = React.useState<StoreAttributes>({
storeName: "",
storeLocation: "",
storeLocationLongitude: 0,
storeLocationLatitude: 0,
});
// Get store info.
React.useEffect(() => {
const user: CognitoUser | null = getCurrentBusinessAccountUser();
if (!user) {
Toast("Store Not Found!", "Failed to get store information!", "danger");
} else {
const storeIdentifier: string = user?.getUsername();
getStoreInfo(storeIdentifier)
.then((response) => {
setFormValues({
storeName: response?.storeName || "",
storeLocation: response?.storeLocation || "",
storeLocationLatitude: response?.storeLocationLatitude!,
storeLocationLongitude: response?.storeLocationLongitude!,
});
})
.catch((error) =>
Toast(
"Store Not Found!",
"Failed to get store information!",
"danger"
)
);
}
}, []);
const handleStoreLocationUpdate = (newStoreLocation: string) => {
const geocoder = new window.google.maps.Geocoder();
console.log("handleStoreLocationUpdate");
console.log(newStoreLocation);
console.log(formValues);
const geocodeRequest = { address: newStoreLocation };
const geocodeCallback = (
results: google.maps.GeocoderResult[] | null,
status: google.maps.GeocoderStatus
) => {
if (status === "OK") {
if (results && results[0]) {
const formValuesClone: StoreAttributes = structuredClone(formValues);
formValuesClone.storeLocation = newStoreLocation;
formValuesClone.storeLocationLatitude =
results[0].geometry.location.lat();
formValuesClone.storeLocationLongitude =
results[0].geometry.location.lng();
setFormValues(formValuesClone);
} else {
Toast("Not valid address!", "Please input a valid address", "danger");
}
} else {
Toast("Not valid address!", "Please input a valid address", "danger");
}
};
geocoder.geocode(geocodeRequest, geocodeCallback);
};
const handleSubmit = (data: StoreAttributes) => {
updateStore(props.storeIdentifier, JSON.stringify(data, null, 2))
.then((response) => {
if (response.status == 200) {
Toast(
"Updated!",
"The store information has been updated. Redirect to store page...",
"success"
);
navigate("/stores/" + props.storeIdentifier);
} else {
Toast(
"Updated failed!",
"Failed to update store information.",
"danger"
);
}
})
.catch((error) => {
Toast("Updated failed!!", error.message, "danger");
});
};
const handleUpdate = (data: StoreAttributes) => {
// make a deep clone here, as formValues here is an object.
console.log("handleUpdate");
const copy = structuredClone(data);
setFormValues(copy);
};
return (
<EditStoreInfoForm
formValues={formValues}
handleStoreLocationUpdate={handleStoreLocationUpdate}
handleUpdate={handleUpdate}
handleSubmit={handleSubmit}
/>
);
};
export default EditStoreInfoPage;
EditStoreInfoForm
EditStoreInfoForm is the form component. I use Formik here. It renders the form with props.formValues. It contains a child component FormikAddressField which will be used to support google place auto complete.
export type EditStoreInfoFormProps = {
formValues: StoreAttributes;
handleStoreLocationUpdate: any;
handleUpdate: any;
handleSubmit: any;
};
const EditStoreInfoForm = (props: EditStoreInfoFormProps) => {
console.log("EditStoreInfoForm");
const onBlur = () => {
console.log(props.formValues);
}
return (
<div className="flex justify-center items-center">
<Formik.Formik
initialValues={props.formValues}
enableReinitialize={true}
validationSchema={validationSchema}
validateOnChange={false}
validateOnBlur={false}
onSubmit={(values) => {
props.handleSubmit(values);
}}
>
{({ }) => (
<Formik.Form className="w-1/3">
<div className="form-group">
<div>
<FormikTextField
label="Store Name"
name="storeName"
placeholder={props.formValues?.storeName}
/>
</div>
<div className="form-group">
<FormikAddressField
label="Store Location"
name="storeLocation"
onAddressUpdate={props.handleStoreLocationUpdate}
placeholder={props.formValues?.storeLocation}
/>
</div>
<div className="w-full h-60">
{/* <GoogleMapLocationPin latitude={10} longitude={10} text="store"/> */}
<StoresGoogleMapLocation
googleMapCenter={{
lat: props.formValues.storeLocationLatitude,
lng: props.formValues.storeLocationLongitude,
}}
storeAddress={props.formValues?.storeLocation}
storeLocationLongitude={
props.formValues?.storeLocationLongitude
}
storeLocationLatitude={props.formValues?.storeLocationLatitude}
/>
</div>
<div className="form-group">
<button type="submit" className="form-button m-2 w-20 h-10">
Update
</button>
</div>
</Formik.Form>
)}
</Formik.Formik>
</div>
);
};
export default EditStoreInfoForm;
FormikAddressField
FormikAddressField is the field for autocomplete. See https://developers.google.com/maps/documentation/javascript/place-autocomplete to know what it is.
const FormikAddressField = ({ label, onAddressUpdate, ...props }: any) => {
const [field, meta] = useField(props);
const loader = new Loader({
apiKey: process.env.REACT_APP_GOOGLE_MAP_API_KEY!,
libraries: ["places", "geometry"],
});
const locationInputId = "locationInputId";
let searchInput: HTMLInputElement;
const autoCompleteInstanceRef = React.useRef<any>(null);
React.useEffect(() => {
loader.load().then(() => {
let searchInput = document.getElementById(
locationInputId
) as HTMLInputElement;
//console.log(searchInput);
autoCompleteInstanceRef.current = new google.maps.places.Autocomplete(
searchInput!,
{
// restrict your search to a specific type of resultmap
//types: ["address"],
// restrict your search to a specific country, or an array of countries
// componentRestrictions: { country: ['gb', 'us'] },
}
);
autoCompleteInstanceRef.current.addListener(
"place_changed",
onPlaceChanged
);
});
// returned function will be called on component unmount
return () => {
google.maps.event.clearInstanceListeners(searchInput!);
};
}, []);
const onPlaceChanged = () => {
const place: google.maps.places.PlaceResult =
autoCompleteInstanceRef.current.getPlace();
if (!place) return;
onAddressUpdate(place.formatted_address);
};
return (
<>
<label htmlFor={props.id || props.name} className="form-label">
{label}
</label>
<Field
id={locationInputId}
className="text-md w-full h-full m-0 p-0"
type="text"
{...field}
{...props}
/>
{meta.touched && meta.error ? (
<div className="error">{meta.error}</div>
) : null}
</>
);
};
export default FormikAddressField;
CodeSandbox
Here is a simplified version: https://nv1m89.csb.app/
The EditStoreInfoPage is above the EditStoreInfoForm. The formikValues in EditStoreInfoPage appears to be a copy, which is not updated every time the actual real-time formik values in EditStoreInfoForm are changed. Your real problem here is that you shouldn't have the clone in the first place.
Just pass the real store values up to the handler:
<FormikAddressField
label="Store Location"
name="storeLocation"
onAddressUpdate={(newAddress) => props.handleStoreLocationUpdate(newAddress, formValues)}
placeholder={props.formValues?.storeLocation}
/>
Now change:
const handleStoreLocationUpdate = (newStoreLocation: string) => {
To:
const handleStoreLocationUpdate = (newStoreLocation: string, formValues: StoreAttributes) => {
And use that argument.
As mentioned there are other issues here. Really you should refactor to get rid of this completely:
const [formValues, setFormValues] = React.useState<StoreAttributes>({
storeName: "",
storeLocation: "",
storeLocationLongitude: 0,
storeLocationLatitude: 0,
});
You'd do it by making the actual form state accessible to that component. Probably by changing to the useFormik pattern and loading that hook in the parent.

React useState object not letting me access it's properties

I'm making a chat app and I'm trying to create dynamic chat pages based on room page names within firestore. My chat component updates the chatbox title depending on what it finds off of firestore. I have determined that the query I'm using does in fact pull the data correctly through console.log.
function Chat() {
const { roomId } = useParams();
const [roomDetails, setRoomDetails] = useState();
useEffect(() => {
const roomQuery = query(
collection(db, "rooms"),
where("name", "==", roomId)
);
if (roomId) {
const unsubscribe = onSnapshot(roomQuery, (snapshot) => {
setRoomDetails(snapshot.docs.map((doc) => ({ ...doc.data() })));
});
return () => {
unsubscribe();
};
}
}, [roomId]);
console.log(roomDetails);
return (
<div className="chat">
<div className="chat__header">
<div className="chat__headerLeft">
<h4 className="chat__channelName">
<strong>#{roomDetails?.name}</strong>
<StarBorderOutlinedIcon />
</h4>
</div>
<div className="chat__headerRight">
<p>
<InfoOutlinedIcon /> Details
</p>
</div>
</div>
</div>
);
}
What doesn't work is the bottom section
<h4 className="chat__channelName">
<strong>#{roomDetails?.name}</strong>
<StarBorderOutlinedIcon />
</h4>
Within my browser the chat title will be blank and I presume that it's because it isn't reading the data or the data isn't defined yet. Here is what the useState looks like in console.
console.log output
[{…}]0:
{name: 'general'}
length: 1
[[Prototype]]: Array(0)
Here's what Firestore looks like
I found the solution I changed the way the Data was updated into the useState
useEffect(() => {
const roomQuery = query(
collection(db, "rooms"),
where("name", "==", roomId)
);
if (roomId) {
const unsubscribe = onSnapshot(roomQuery, (snapshot) => {
setRoomDetails(snapshot.docs.at(0).data());
});
return () => {
unsubscribe();
};
}
}, [roomId]);
The way the data is rendered is the same. Hope this helps some future people.

Loading spinner not showing in React Component

I am creating a React.js app which got 2 components - The main one is a container for the 2nd and is responsible for retrieving the information from a web api and then pass it to the child component which is responsible for displaying the info in a list of items. The displaying component is supposed to present a loading spinner while waiting for the data items from the parent component.
The problem is that when the app is loaded, I first get an empty list of items and then all of a sudden all the info is loaded to the list, without the spinner ever showing. I get a filter first in one of the useEffects and based on that info, I am bringing the items themselves.
The parent is doing something like this:
useEffect(() =>
{
async function getNames()
{
setIsLoading(true);
const names = await WebAPI.getNames();
setAllNames(names);
setSelectedName(names[0]);
setIsLoading(false);
};
getNames();
} ,[]);
useEffect(() =>
{
async function getItems()
{
setIsLoading(true);
const items= await WebAPI.getItems(selectedName);
setAllItems(items);
setIsLoading(false);
};
getTenants();
},[selectedName]);
.
.
.
return (
<DisplayItems items={allItems} isLoading={isLoading} />
);
And the child components is doing something like this:
let spinner = props.isLoading ? <Spinner className="SpinnerStyle" /> : null; //please assume no issues in Spinner component
let items = props.items;
return (
{spinner}
{items}
)
I'm guessing that the problem is that the setEffect is asynchronous which is why the component is first loaded with isLoading false and then the entire action of setEffect is invoked before actually changing the state? Since I do assume here that I first set the isLoading and then there's a rerender and then we continue to the rest of the code on useEffect. I'm not sure how to do it correctly
The problem was with the asynchronicity when using mulitple useEffect. What I did to solve the issue was adding another spinner for the filters values I mentioned, and then the useEffect responsible for retrieving the values set is loading for that specific spinner, while the other for retrieving the items themselves set the isLoading for the main spinner of the items.
instead of doing it like you are I would slightly tweak it:
remove setIsLoading(true); from below
useEffect(() =>
{
async function getNames()
{
setIsLoading(true); //REMOVE THIS LINE
const names = await WebAPI.getNames();
setAllNames(names);
setSelectedName(names[0]);
setIsLoading(false);
};
getNames();
} ,[]);
and have isLoading set to true in your initial state. that way, it's always going to show loading until you explicitly tell it not to. i.e. when you have got your data
also change the rendering to this:
let items = props.items;
return isLoading ? (
<Spinner className="SpinnerStyle" />
) : <div> {items} </div>
this is full example with loading :
const fakeApi = (name) =>
new Promise((resolve)=> {
setTimeout(() => {
resolve([{ name: "Mike", id: 1 }, { name: "James", id: 2 }].filter(item=>item.name===name));
}, 3000);
})
const getName =()=> new Promise((resolve)=> {
setTimeout(() => {
resolve("Mike");
}, 3000);
})
const Parent = () => {
const [name, setName] = React.useState();
const [data, setData] = React.useState();
const [loading, setLoading] = React.useState(false);
const fetchData =(name) =>{
if(!loading) setLoading(true);
fakeApi(name).then(res=>
setData(res)
)
}
const fetchName = ()=>{
setLoading(true);
getName().then(res=> setName(res))
}
React.useEffect(() => {
fetchName();
}, []);
React.useEffect(() => {
if(name)fetchData(name);
}, [name]);
React.useEffect(() => {
if(data && loading) setLoading(false)
}, [data]);
return (
<div>
{loading
? "Loading..."
: data && data.map((d)=>(<Child key={d.id} {...d} />))}
</div>
);
};
const Child = ({ name,id }) =>(<div>{name} {id}</div>)
ReactDOM.render(<Parent/>,document.getElementById("root"))
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="root"></div>

How to perform search on data that is stored in context api in reactjs?

i am doing one task in that i have json data and I am using context api to pass data in different component. but i am facing problem in how to perform search operation when user enter something input field in search box, how can i get filtered data from context api ?
A big problem with making request based on user input is that you don't know in what order they resolve. If a user types a and then another a the app would make a request with a and then aa. If the first request takes 5 seconds and the second request takes 1 second then the search result of aa comes in first and the ui is set with this result. Then the result of a would come in and the ui will be set with that result. Problem now is that the user typed aa but result for a is showing.
See the hidden snippet below where if you type aa quickly then the result for a comes in after aa and those results will be shown:
const data = ['aa', 'ab', 'bb'];
const later = (value, time) =>
new Promise((resolve) =>
setTimeout(() => resolve(value), time)
);
const getData = (query) =>
later(
data.filter((d) => d.includes(query)),
query.length === 1 ? 500 : 100
);
const App = () => {
const [state, setState] = React.useState({
loading: true,
filter: '',
});
React.useEffect(() => {
setState((s) => ({ ...s, loading: true }));
getData(state.filter).then(
(data) =>
console.log(
'setting result based on filter:',
state.filter
) ||
setState((s) => ({ ...s, data, loading: false }))
);
}, [state.filter]);
const changeFilter = React.useCallback((e) => {
const filter = e.target.value;
setState((s) => ({
...s,
filter,
}));
}, []);
return (
<div>
<div>
<input type="text" onChange={changeFilter} />
</div>
<pre>{JSON.stringify(state, undefined, 2)}</pre>
</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>
It logs the following:
setting result based on filter:
setting result based on filter: aa
setting result based on filter: a
First log is when the component mounted and search is empty, second is when you typed aa and the search resolved and the third is when you typed a (need to type a before you can type aa) and the search resolved. Now the UI ends up showing results for a but UI showing you are searching for a
What you want to do is only resolve the api promise (and set the UI) when it was the last request, see snippet below:
const data = ['aa', 'ab', 'bb'];
const later = (value, time) =>
new Promise((resolve) =>
setTimeout(() => resolve(value), time)
);
const REPLACED = 'REPLACED';
const last = (fn) => {
const current = {};
return (...args) => {
const latest = {};
current.latest = latest;
return Promise.resolve(fn(...args)).then((resolve) =>
current.latest === latest
? resolve
: Promise.reject(REPLACED)
);
};
};
const getData = last((query) =>
later(
data.filter((d) => d.includes(query)),
query.length === 1 ? 500 : 100
)
);
const App = () => {
const [state, setState] = React.useState({
loading: true,
filter: '',
});
React.useEffect(() => {
setState((s) => ({ ...s, loading: true }));
getData(state.filter).then(
(data) =>
console.log(
'setting result based on filter:',
state.filter
) ||
setState((s) => ({ ...s, data, loading: false })),
(error) =>
console.log(
`error: ${error} for filter: ${state.filter}`
)
);
}, [state.filter]);
const changeFilter = React.useCallback((e) => {
const filter = e.target.value;
setState((s) => ({
...s,
filter,
}));
}, []);
return (
<div>
<div>
<input type="text" onChange={changeFilter} />
</div>
<pre>{JSON.stringify(state, undefined, 2)}</pre>
</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>
Now it will log:
setting result based on filter:
setting result based on filter: aa
error: REPLACED for filter: a
Almost the same as the first snippet but instead of the slow request resolving it is now rejecting with the error REPLACED. This is because when search for a comes in it was already replaced with a newer request for aa.
Here is a full example with the fake api call getData in React.Context
const data = ['aa', 'ab', 'bb'];
const later = (value, time) =>
new Promise((resolve) =>
setTimeout(() => resolve(value), time)
);
const REPLACED = 'REPLACED';
const last = (fn) => {
const current = {};
return (...args) => {
const latest = {};
current.latest = latest;
return Promise.resolve(fn(...args)).then((resolve) =>
current.latest === latest
? resolve
: Promise.reject(REPLACED)
);
};
};
const getData = last((query) =>
later(
data.filter((d) => d.includes(query)),
query.length === 1 ? 500 : 100
)
);
const Data = React.createContext();
const DataProvider = ({ children }) => {
const [state, setState] = React.useState({
loading: true,
filter: '',
});
React.useEffect(() => {
setState((s) => ({ ...s, loading: true }));
getData(state.filter).then(
(data) =>
console.log(
'setting result based on filter:',
state.filter
) ||
setState((s) => ({ ...s, data, loading: false })),
(error) =>
console.log(
`error: ${error} for filter: ${state.filter}`
)
);
}, [state.filter]);
const changeFilter = React.useCallback((e) => {
const filter = e.target.value;
setState((s) => ({
...s,
filter,
}));
}, []);
return (
<Data.Provider value={{ state, changeFilter }}>
{children}
</Data.Provider>
);
};
const App = () => {
const { state, changeFilter } = React.useContext(Data);
return (
<div>
<div>
<input type="text" onChange={changeFilter} />
</div>
<pre>{JSON.stringify(state, undefined, 2)}</pre>
</div>
);
};
ReactDOM.render(
<DataProvider>
<App />
</DataProvider>,
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>
If you want to wait until the user is done typing, look into debouncing https://www.npmjs.com/package/lodash.debounce
If you want to kill in-flight requests when the user types something new before the first request comes back, look into attaching an abort controller to each request, and calling the abort method before each new request.

React Hooks Remove item from array

In my react hooks component I am rendering data from an array of objects.
const lineInfo = [
{
id: '001',
line: 's-1125026',
arrival: '7:30',
departure: '8:00',
authorization: 'User 1',
},
{
id: '002',
line: 's-1125027',
arrival: '8:01',
departure: '8:50',
authorization: 'User 1',
},
In the .map() I'm returning data using this data:
<div>
{lineInfo.map((line) => (
<Row key={`line_${line.id}`}>
// remaining code removed for length
The list returns fine, so now I am trying to remove a row from the list.
Remove func
const [list, setList] = useState(lineInfo);
function handleRemove(id) {
console.log('id' id);
const newList = list.filter((line) => line.id !== id);
setList(newList);
}
Remove Button
<Button
className={style.close_button}
onClick={() => handleRemove(line.id)}
>
<img src={close} alt="trash" />
</Button>
</Row>
The problem I am running into is that in my console log, is that only the line.id is being removed from the array instead of the whole row of data.
How do I remove all the data belonging to a particular id?
Even though the console log shows that the text is removed, why is the text that is displayed in my row not removed from the view?
I'm not too familiar with hooks and have only been able to find examples of my particular problem with class components. Thanks in advance.
You should display the defined state with the hook. in this case the list , and not the lineInfo.
<div>
{list.map((line) => (
<Row key={`line_${line.id}`}>
// remaining code removed for length
You should not render lineInfo, render the list from local state instead:
const { useState, useCallback } = React;
const lineInfo = [
{
id: '001',
line: 's-1125026',
arrival: '7:30',
departure: '8:00',
authorization: 'User 1',
},
{
id: '002',
line: 's-1125027',
arrival: '8:01',
departure: '8:50',
authorization: 'User 1',
},
];
//make row pure component using React.memo
const Row = React.memo(function Row({ item, remove }) {
return (
<div>
<pre>{JSON.stringify(item, undefined, 2)}</pre>
<button onClick={() => remove(item.id)}>
remove
</button>
</div>
);
});
const App = () => {
const [list, setList] = useState(lineInfo);
//make remove a callback that is only created
// on App mount using useCallback with no dependencies
const remove = useCallback(
(removeId) =>
//pass callback to setList so list is not a dependency
// of this callback
setList((list) =>
list.filter(({ id }) => id !== removeId)
),
[]
);
return (
<ul>
{list.map((line) => (
<Row
key={`line_${line.id}`}
item={line}
remove={remove}
/>
))}
</ul>
);
};
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>

Resources