React update sibling component - javascript

I'm confused about react hot updating components.
I've got something like this:
const SingleEvent = ({ event }) => (
<>{event.status}</>
)
const EventDetails = ({ event, updateEvent }) => (
<button
onClick={async () => {
const data = await getAPIResponse(); // { status: 'open' }
updateEvent(event.id, data)
}
>
Update
</button>
)
const List = ({ events, updateEvent, selectedEvent }) => {
if (selectedEvent) {
return <EventDetails event={selectedEvent} updateEvent={updateEvent} />
}
return (
<>
{events.map(event => <SingleEvent event={event}/>)}
</>
)
}
const Page = ({ initialEvents }) => {
const [events, setEvents] = useState(initialEvents || []);
const [selectedEvent, setSelectedEvent] = useState(null);
const updateEvent = (eventId, data) => {
setEvents(prevState => {
const eventIndex = prevState.findIndex(
element => element._id === eventId,
);
if (eventIndex === -1) {
return prevState;
}
prevState[eventIndex] = {
...prevState[eventIndex],
...data,
};
return prevState;
});
};
return <List events={events} updateEvent={updateEvent} selectedEvent={selectedEvent} />
}
In the <EventDetails /> component I'm updating one of the events (basically changing it's status). If API works fine, when I close the details (set the selectedEvent to null) everything is changing as it should. If I close the the details before getting the API response - nothing changed.
I've checked the updateEvent function, and it's performing the update, but the UI is not refreshed.
To be clear:
I Open the <EventDetails /> component, I'm pressing the button to update the event. The API should change its status. When I close the EventDetails I'm getting a List of <SingleEvent /> components. Every one of them displays the event.status.
If I close the EventDetails before getting response, status in SingleEvent is not updating. If I wait for the response everything works ok.

Since the component is unmounted before the data is fetched, its no longer able to update the response.
You can instead provide a function as props which performs the API requst and updates the status
const EventDetails = ({ handleClick }) => (
<button
onClick={handleClick}
>
Update
</button>
)
const List = ({ events, updateEvent, selectedEvent }) => {
const handleClick = async () => {
const data = await getAPIResponse(); // { status: 'open' }
updateEvent(event.id, data)
}
if (selectedEvent) {
return <EventDetails event={selectedEvent} updateEvent={updateEvent} handleClick={handleClick}/>
}
return (
<>
{events.map(event => <SingleEvent event={event}/>)}
</>
)
}

The problem was with the update method. Operating directly on prevState is not a good idea. After I changed the updateEvent function, everything works fine.
setEvents(prevState =>
prevState.map(event => {
if (event._id === eventId) {
return {
...event,
...updatedEvent,
};
}
return event;
})
);

Related

Why does my toast notification not re-render in React?

I am trying to create my own "vanilla-React" toast notification and I did manage to make it work however I cannot wrap my head around why one of the solutions that I tried is still not working.
So here we go, onFormSubmit() I want to run the code to get the notification. I excluded a bunch of the code to enhance readability:
const [notifications, setNotifications] = useState<string[]>([]);
const onFormSubmit = (ev: FormEvent<HTMLFormElement>) => {
ev.preventDefault();
const newNotifications = notifications;
newNotifications.push("success");
console.log(newNotifications);
setNotifications(newNotifications);
};
return (
<>
{notifications.map((state, index) => {
console.log(index);
return (
<ToastNotification state={state} instance={index} key={index} />
);
})}
</>
</section>
);
Inside the Toast I have the following:
const ToastNotification = ({
state,
instance,
}:
{
state: string;
instance: number;
}) => {
const [showComponent, setShowComponent] = useState<boolean>(true);
const [notificationState, setNotificationState] = useState(
notificationStates.empty
);
console.log("here");
const doNotShow = () => {
setShowComponent(false);
};
useEffect(() => {
const keys = Object.keys(notificationStates);
const index = keys.findIndex((key) => state === key);
if (index !== -1) {
const prop = keys[index] as "danger" | "success";
setNotificationState(notificationStates[prop]);
}
console.log(state);
}, [state, instance]);
return (
<div className={`notification ${!showComponent && "display-none"}`}>
<div
className={`notification-content ${notificationState.notificationClass}`}
>
<p className="notification-content_text"> {notificationState.text} </p>
<div className="notification-content_close">
<CloseIcon color={notificationState.closeColor} onClick={doNotShow} />
</div>
</div>
</div>
);
};
Now for the specific question - I cannot understand why onFormSubmit() I just get a log with the array of strings and nothing happens - it does not even run once - the props get updated with every instance and that should trigger a render, the notifications are held into a state and even more so, should update.
What is wrong with my code?

React: props are undefined, even though they should be

my props in the children class is supposed to be an array of Event objects.
I am checking beforehand, if the array is empty in App.js like this:
function App() {
class Event {
constructor(id, title, date){
this.id = id;
this.title = title;
this.date = date;
}
}
const [events, setEvents] = useState([])
const [ids, setIds] = useState([])
const [safedIds, setSafedIds] = ([])
const [eventsPrep, setEventsPrep] = useState([Event])
useEffect(() => {
fetch('https://someAPI.com')
.then(response => response.json())
.then(
res => {setEvents(res);
console.log(res);
})
.catch(err => console.log(err))
.then(handleIncomingData())
//.then(console.log("was here"))
}, [])
function handleIncomingData () {
if(events.length > 0) {
events.forEach(event => {
ids.push(event["_id"]);
let date = new Date(event["date"]);
eventsPrep.push(new Event(event["_id"], event["title"], date.toDateString()))
})
}
}
return (
<>
<Navbar/>
{eventsPrep.length > 0 ? <Home events={eventsPrep}/> : <></>}
</>
);
}
export default App;
but whenever I try to reach the props in the child component it is considered undefined.
My child component:
import React from 'react'
import SingleEvent from '../../event/SingleEvent'
export const Home = (props) => {
console.log(props.events)
return (
<>
{props?.events
? props.events.forEach((event) => {
console.log('was here 2');
return <SingleEvent title={event.title} start={event.date} />;
})
: 'no upcomming events'}
</>
);
}
Even if I only pass a string down, it is still undefined.
Thanks for help!
In your useEffect() you update events via setEvents(res) and call handleIncomingData() after that.
In handleIncomingData() you use events, but it will still hold the value from previous render / from the initialization as setEvents(res) will not change events immidiately. You can console.log(events) inside that function to investigate this.
Instead you can pass res into handleIncomingData() and use it instead of events inside that function. So in you useEffect you would have:
.then(response => response.json())
.then(
res => {
setEvents(res);
handleIncomingData(res);
})
In addition in handleIncomingData() use setEventsPrep(...) instead of eventsPrep.push(), as mentioned in the comment.
You are returning 2 return statments in the component.
Only need to return 1 return statement in component
export const Home = (props) => {
return (
<>
{props?.events && Object.values(props.events).length > 0
? props.events.forEach((event) => {
console.log('was here 2');
return <SingleEvent title={event.title} start={event.date} />;
})
: 'no upcomming events'}
</>
);
};
Also to check whether eventsPrep has a length > 0 then try it this way if eventsPrep is an object
eventsPrep && Object.values(eventsPrep).length > 0

How can I prevent states from resetting after a http request?

when I send the server an HTTP request ( patch request in the checkbox onChange function) and update the state other states will be deleted until I reload the page and they will be back
so how can I update the states without losing the others?
I'm not totally sure but I think the problem is where I'm updating the state with the response I get from the server I think I'm not updating the state and I'm just adding the new response to it and replacing the others
here's my code
const Form = () => {
const [todos, setTodos] = React.useState([]);
const debounce = (func, timeout = 350) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
};
const saveInput = (e, id) => {
const x = !e.target.checked;
console.log(x);
axios
.patch(`http://127.0.0.1:8000/todo/todos/${id}/`, {
completed: x,
})
.then(
(response) => {
console.log(response.data);
setTodos([response.data]);
},
(error) => {
console.log(error);
}
);
};
const processChange = debounce((e, id) => saveInput(e, id));
useEffect(() => {
axios.get("http://127.0.0.1:8000/todo/todos/").then((response) => {
setTodos(response.data);
});
}, []);
return (
<form>
<h1>Todo list</h1>
<button>Add</button>
<div>
{todos.map((todo) => (
<ul key={todo.id}>
<li>{todo.title}</li>
<li>{todo.description}</li>
<button onClick={() => deleteHandler(todo.id)}>delete</button>
<input
type="checkbox"
placeholder="completed"
onChange={(e) => processChange(e, todo.id)}
checked={todo.completed}
/>
</ul>
))}
</div>
</form>
);
};
export default Form;
Inside your saveInput function you are resetting your todos with the new response data. Thus you are losing the data from your component initialization. What you need to do is to destructure your existing data and add them with your new payload.
You can either do this:
setTodos([...todos, response.data]);
Or this:
setTodos((prevState) => ([...prevState, response.data]))
The second option is the best practice as this returns your state correctly.
Hope this helps.
Try This.
const saveInput = (e, id) => {
const x = !e.target.checked;
console.log(x);
const newTodos = [...todos];
const filteredTodos = newTodos.filter(todo => todo.id !== id);
axios
.patch(`http://127.0.0.1:8000/todo/todos/${id}/`, {
completed: x,
})
.then(
(response) => {
console.log(response.data);
filteredTodos.push(response.data);
setTodos(filteredTodos);
},
(error) => {
console.log(error);
}
);
};
So Morteza's answer worked but it had a tiny problem which was when i updated the state the order of the state ( array ) would change because of .push() method which pushes the item to the last index
here is the solution which works fine and won't change the orders:
setTodos(val => val.map(item => item.id === response.data.id ? (response.data) : item));
It's getting the todos current state, mapping and checking if each of its item.id is equal to the item.id from the server and if it is, then replaced and updated with a response.data, if not then nothing changes.

Make React JS await for a async func to complete before running

I'm trying to make react not load until after an axios get requests finishes. I'm pretty rough on react all around, so sorry in advance.
I'm getting an array of objects
const { dogBreedsTest } = useApplicationData()
And I need it to be the default value of one of my states
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest);
However, I'm getting an error that my value is coming up as null on the first iteration of my app starting. How can I ensure that my value has completed my request before my app tries to use it?
Here is how I am getting the data for useApplicationData()
const [dogBreedsTest, setDogBreeds] = useState(null);
const getDogBreeds = async () => {
try{
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
}
useEffect(() => {
getDogBreeds()
}, []);
return {
dogBreedsTest,
setDogBreeds
}
And I am importing into my app and using:
import useApplicationData from "./hooks/useApplicationData";
const { dogBreedsTest } = useApplicationData()
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest[0]);
const [breedList1, updateBreedList1] = useState(dogBreedsTest[0])
function handleOnDragEnd(result) {
if (!result.destination) return;
const items = Array.from(dogBreeds);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
for (const [index, item] of items.entries()) {
item['rank'] = index + 1
}
updateDogBreeds(dogBreedsTest[0]);
updateBreedList1(dogBreedsTest[0])
}
return (
<div className="flex-container">
<div className="App-header">
<h1>Dog Breeds 1</h1>
<DragDropContext onDragEnd={handleOnDragEnd}>
<Droppable droppableId="characters">
{(provided) => (
<ul className="dogBreeds" {...provided.droppableProps} ref={provided.innerRef}>
{breedList1?.map(({id, name, rank}, index) => {
return (
<Draggable key={id} draggableId={id} index={index}>
{(provided) => (
<li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<p>
#{rank}: { name }
</p>
</li>
)}
</Draggable>
);
})}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
</div>
)
error: TypeError: Cannot read property 'map' of null
(I am mapping the data later in the program)
const getDogBreeds = async () => {
try {
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
}
useEffect(() => {
getDogBreeds() // -> you are not awaiting this
}, []);
Do this instead
useEffect(() => {
axios.get('https://dog.ceo/api/breeds/list/all')
.then(res => {
const newDogList = generateDogsArray(res.data['message']);
const generatedDogs = selectedDogs(newDogList);
setDogBreeds(generatedDogs);
})
.catch(err => console.log(err));
}, []);
I know this looks awful, but I don't think you should use async/await inside useEffect
Use this in your application
useEffect will update whenever dogBreedsTest is changed. In order to make it work, start with null values and update them to the correct initial values once your async operation is finished.
const { dogBreedsTest } = useApplicationData();
const [dogBreeds, updateDogBreeds] = useState(null);
const [breedList1, updateBreedList1] = useState(null);
useEffect(() => {
updateDogBreeds(dogBreedsTest[0]);
updateBreedList1(dogBreedsTest[0]);
}, [dogBreedsTest]);
The problem is, that react first render and then run useEffect(), so if you don't want to render nothing before the axios, you need to tell to react, that the first render is null.
Where is your map function, to see the code? to show you it?.
I suppose that your data first is null. So you can use something like.
if(!data) return null
2nd Option:
In your map try this:
{breedList1 === null
? null
: breedList1.map(({id, name, rank}, index) => (
<Draggable
key={id} draggableId={id} index={index}>
{(provided) => (
<li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<p>
#{rank}: { name }
</p>
</li>
)}
</Draggable> ))}
You have null, because your axios is async and react try to render before any effect. So if you say to react that the list is null, react will render and load the data from the api in the second time.
Option 1 use the optional chaining operator
dogBreedsTest?.map()
Option 2 check in the return if dogBreedsTest is an array
retrun (<>
{Array.isArray(dogBreedsTest) && dogBreedsTest.map()}
</>)
Option 3 return early
if (!Array.isArray(dogBreedsTest)) return null
retrun (<>
{dogBreedsTest.map()}
</>)
Option 4 set initial state
const [dogBreedsTest, setDogBreeds] = useState([]);
You could also add a loading state and add a loading spinner or something like that:
const [dogBreedsTest, setDogBreeds] = useState(null);
const [loading, setLoading] = useState(true)
const getDogBreeds = async () => {
setLoading(true)
try{
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
setLoading(false)
}
useEffect(() => {
getDogBreeds()
}, []);
return {
dogBreedsTest,
loading,
setDogBreeds
}
Edit
Try to use a useEffect hook to update the states when dogBreedsTest got set.
const { dogBreedsTest } = useApplicationData()
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest?.[0] ?? []);
const [breedList1, updateBreedList1] = useState(dogBreedsTest?.[0] ?? [])
useEffect(() => {
updateDogBreeds(dogBreedsTest?.[0] ?? [])
updateBreedList1(dogBreedsTest?.[0] ?? [])
}, [dogBreedsTest])

Couldn't correctly initialize state in parent component from children states

I have two React components, namely, Form and SimpleCheckbox.
SimpleCheckbox uses some of the Material UI components but I believe they are irrelevant to my question.
In the Form, useEffect calls api.getCategoryNames() which resolves to an array of categories, e.g, ['Information', 'Investigation', 'Transaction', 'Pain'].
My goal is to access checkboxes' states(checked or not) in the parent component(Form). I have taken the approach suggested in this question.(See the verified answer)
Interestingly, when I log the checks it gives(after api call resolves):
{Pain: false}
What I expect is:
{
Information: false,
Investigation: false,
Transaction: false,
Pain: false,
}
Further More, checks state updates correctly when I click into checkboxes. For example, let's say I have checked Information and Investigation boxes, check becomes the following:
{
Pain: false,
Information: true,
Investigation: true,
}
Here is the components:
const Form = () => {
const [checks, setChecks] = useState({});
const [categories, setCategories] = useState([]);
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
}
useEffect(() => {
api
.getCategoryNames()
.then((_categories) => {
setCategories(_categories);
})
.catch((error) => {
console.log(error);
});
}, []);
return (
{categories.map(category => {
<SimpleCheckbox
label={category}
onCheck={handleCheckChange}
key={category}
id={category}
/>
}
)
}
const SimpleCheckbox = ({ onCheck, label, id }) => {
const [check, setCheck] = useState(false);
const handleChange = (event) => {
setCheck(event.target.checked);
};
useEffect(() => {
onCheck(check, id);
}, [check]);
return (
<FormControl>
<FormControlLabel
control={
<Checkbox checked={check} onChange={handleChange} color="primary" />
}
label={label}
/>
</FormControl>
);
}
What I was missing was using functional updates in setChecks. Hooks API Reference says that: If the new state is computed using the previous state, you can pass a function to setState.
So after changing:
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
}
to
const handleCheckChange = (isChecked, category) => {
setChecks(prevChecks => { ...prevChecks, [category]: isChecked });
}
It has started to work as I expected.
It looks like you're controlling state twice, at the form level and at the checkbox component level.
I eliminated one of those states and change handlers. In addition, I set checks to have an initialState so that you don't get an uncontrolled to controlled input warning
import React, { useState, useEffect } from "react";
import { FormControl, FormControlLabel, Checkbox } from "#material-ui/core";
import "./styles.css";
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Form />
</div>
);
}
const Form = () => {
const [checks, setChecks] = useState({
Information: false,
Investigation: false,
Transaction: false,
Pain: false
});
const [categories, setCategories] = useState([]);
console.log("checks", checks);
console.log("categories", categories);
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
};
useEffect(() => {
// api
// .getCategoryNames()
// .then(_categories => {
// setCategories(_categories);
// })
// .catch(error => {
// console.log(error);
// });
setCategories(["Information", "Investigation", "Transaction", "Pain"]);
}, []);
return (
<>
{categories.map(category => (
<SimpleCheckbox
label={category}
onCheck={handleCheckChange}
key={category}
id={category}
check={checks[category]}
/>
))}
</>
);
};
const SimpleCheckbox = ({ onCheck, label, check }) => {
return (
<FormControl>
<FormControlLabel
control={
<Checkbox
checked={check}
onChange={() => onCheck(!check, label)}
color="primary"
/>
}
label={label}
/>
</FormControl>
);
};
If you expect checks to by dynamically served by an api you can write a fetchHandler that awaits the results of the api and updates both slices of state
const fetchChecks = async () => {
let categoriesFromAPI = ["Information", "Investigation", "Transaction", "Pain"] // api result needs await
setCategories(categoriesFromAPI);
let initialChecks = categoriesFromAPI.reduce((acc, cur) => {
acc[cur] = false
return acc
}, {})
setChecks(initialChecks)
}
useEffect(() => {
fetchChecks()
}, []);
I hardcoded the categoriesFromApi variable, make sure you add await in front of your api call statement.
let categoriesFromApi = await axios.get(url)
Lastly, set your initial slice of state to an empty object
const [checks, setChecks] = useState({});

Categories