React child component does not rerender after parent component update state - javascript

I have a main parent component (DataTableComponent) which is a data table and have a large set of data. And in that data table I have edit buttons to each row. And then after user click on edit button I pass the respective data to a function then I push to another component (EditComponent) using react-router-dom useHistory hook and with that I pass the data. Functions look like this.
const redirectToEditPage = (data) => {
push({
pathname: '/edit',
state: {
data
}
})
}
And in the EditComponent, there's a form which sets the initial data which receive as props from the DataTableComponent. In a useEffect hook call I process some data (I create image File objects using image links and then set it to a setState which is initialized as an empty array) received from props. And along with that form in the EditComponent, there's also one other child component (ImagePicker) which render some images. And to this ImagePicker component I pass the processed image Files array as a prop. And in the first initial render of the EditComponent, ImagePicker component render the images that I passed as props nicely. But after clicking back button on web browser which goes back to the DataTableComponent and then selecting another table row and cliking edit button on the table which push to the EditComponent again, it render the other data on the form in the EditComponent as expected but it does not render any image files on the child component (ImagePicker)
EditComponent
const EditComponent = ({data}) => {
const [imageFiles, setImageFiles] = useState([])
const imageFilesInitialStateFunc = () => {
const tempArr = []
tempArr.push({sequence: 0, image: data.image?.mainImage})
const productAdditionalImages = data.image?.productAdditionalImageList
if (productAdditionalImages.length > 0) {
productAdditionalImages.map((image) => {
const obj = {sequence: image.sequence, image: image.additionalImage}
tempArr.push(obj)
})
}
return tempArr
}
const convertImageUrlToFiles = (files) => {
const tempFiles = []
files.map((file) => {
const imageUrl = constructMediaCDNUrl(`product_images/${file.image}`, 200)
toDataURL(imageUrl)
.then(dataUrl => {
// console.log('Base64 Url', dataUrl)
const fileData = dataURLtoFile(dataUrl, file.image);
// console.log("JavaScript File Object", fileData)
tempFiles.push(fileData)
})
})
setImageFiles(tempFiles)
}
useEffect(() => {
const initialImageFiles = imageFilesInitialStateFunc()
convertImageUrlToFiles(initialImageFiles)
return () => {
console.log('pressed back')
}
}, [])
return (
// form
<ImagePicker key={imageFiles} files={imageFiles} setFiles={setImageFiles}/>
)
}
ImagePicker Component
const ImagePicker = ({files, setFiles}) => {
return (
<div>
{files.length > 0 ? (
{files.map((file, index) => (
<img key={index} src={URL.createObjectURL(file)} alt={file.name} className="product-images img-thumbnail img-fluid rounded mb-1" />
))
) : <div>Loading...</div>
</div>
)
}
After the first render when user goes back to DataTableComponent by clicking back button and then selecting another table row and clicking edit button which goes to EditComponent, then all other data render as expected but only ImagePicker component is displayed as Loading...

In EditComponent, try modifying the useEffect as follows:
useEffect(() => {
const initialImageFiles = imageFilesInitialStateFunc()
convertImageUrlToFiles(initialImageFiles)
return () => {
console.log('pressed back')
}
}, [data, data.image])

Related

React State / DOM Not Updating When All Items Deleted

I'm building an app with React and Firebase Realtime Database. Objects are added to an array and sent to the database.
The arrays are updated in React and the result is sent to the database.
The functionality to remove items/objects from the list works fine when there are more than one (i.e. button clicked, database, DOM and state updated immediately).
However, whenever there's one item left and you click its delete button, it's deleted from the database but the state and React DOM aren't updated - you have to refresh the page for it to be removed.
I've tried using different methods to update the database in case it triggered a different response but that didn't work - any ideas would be greatly appreciated:
import React, {useState, useEffect} from 'react'
import { Button } from "react-bootstrap";
import Exercise from "./Exercise";
import AddNewWorkout from "./AddNewWorkout";
import { v4 as uuidv4 } from "uuid";
import WorkoutComponent from './WorkoutComponent';
import AddNewExercise from "./AddNewExercise"
import { database, set, ref, onValue, update } from "../firebase"
const Dashboard = ({user}) => {
const [selectedWorkout, setSelectedWorkout] = useState();
const [workouts, setWorkouts] = useState([])
const [creatingNewWorkout, setCreatingNewWorkout] = useState(false);
const [addingNewExercise, setAddingNewExercise] = useState(false)
function selectWorkout(number) {
const selection = [...workouts].filter(workout => number == workout.id);
setSelectedWorkout(selection[0])
}
function toggleNewWorkoutStatus(e) {
e.preventDefault()
setCreatingNewWorkout(creatingNewWorkout => !creatingNewWorkout)
}
function toggleNewExerciseStatus() {
setAddingNewExercise(addingNewExercise => !addingNewExercise)
}
function writeData() {
const newWorkouts = [...workouts]
const workoutTitle = document.getElementById("workoutTitle").value || new Date(Date.now()).toString()
const workoutDate = document.getElementById("workoutDate").value;
newWorkouts.push({
id: uuidv4(),
title: workoutTitle,
date: workoutDate,
exercises: []
})
set(ref(database, `${user.uid}/workouts/`), newWorkouts )
}
function addWorkoutToListDB(e) {
e.preventDefault();
writeData(user.uid)
}
function removeWorkoutFromList(id) {
const newWorkouts = [...workouts].filter(workout => id !== workout.id);
update(ref(database, `${user.uid}`), {"workouts": newWorkouts} )
}
function addExerciseToWorkout(e) {
e.preventDefault();
if (selectedWorkout === undefined) {
alert("No workout selected")
return
}
const newWorkouts = [...workouts]
const exerciseID = uuidv4();
const exerciseName = document.getElementById("exerciseName").value
const exerciseSets = document.getElementById("exerciseSets").value
const exerciseReps = document.getElementById("exerciseReps").value
const exerciseWeight = document.getElementById("exerciseWeight").value
const exercisetTarget = document.getElementById("exercisetTarget").checked
const exerciseNotes = document.getElementById("exerciseNotes").value;
const newExercise = {
id: exerciseID,
name: exerciseName,
sets: exerciseSets,
reps: exerciseReps,
weight: `${exerciseWeight}kg`,
target: exercisetTarget,
notes: exerciseNotes,
}
for (let key of newWorkouts) {
if (key.id === selectedWorkout.id) {
if (key.exercises) {
key.exercises.push(newExercise)
} else {
key.exercises = [newExercise]
}
}
}
update(ref(database, `${user.uid}`), {"workouts": newWorkouts} )
}
function removeExerciseFromWorkout(id) {
const newWorkouts = [...workouts];
for (let workout of newWorkouts) {
if(selectedWorkout.id === workout.id) {
if (!workout.exercises) {return}
workout.exercises = workout.exercises.filter(exercise => exercise.id !== id)
}
}
const newSelectedWorkout = {...selectedWorkout}
newSelectedWorkout.exercises = newSelectedWorkout.exercises.filter(exercise => exercise.id !== id)
setSelectedWorkout(newSelectedWorkout)
update(ref(database, `${user.uid}`), {"workouts": newWorkouts} )
}
useEffect(() => {
function getWorkoutData() {
const dbRef = ref(database, `${user.uid}`);
onValue(dbRef, snapshot => {
if (snapshot.val()) {
console.log(snapshot.val().workouts)
setWorkouts(workouts => workouts = snapshot.val().workouts)
}
}
)
}
getWorkoutData()
},[])
return (
<div>
{creatingNewWorkout && <AddNewWorkout addWorkoutToListDB={addWorkoutToListDB} toggleNewWorkoutStatus={toggleNewWorkoutStatus} /> }
<div id="workoutDiv">
<h2>Workouts</h2><p>{selectedWorkout ? selectedWorkout.title : "No workout selected"}</p>
<Button type="button" onClick={toggleNewWorkoutStatus} className="btn btn-primary">Add New Workout</Button>
{workouts && workouts.map(workout => <WorkoutComponent key={workout.id} removeWorkoutFromList={removeWorkoutFromList} selectWorkout={selectWorkout} workout={workout}/> )}
</div>
<div>
<h2>Exercise</h2>
{addingNewExercise && <AddNewExercise selectedWorkout={selectedWorkout} addExerciseToWorkout={addExerciseToWorkout} toggleNewExerciseStatus={toggleNewExerciseStatus}/> }
<Button type="button" onClick={toggleNewExerciseStatus} className="btn btn-primary">Add New Exercise</Button>
{selectedWorkout && selectedWorkout.exercises && selectedWorkout.exercises.map(exercise => <Exercise removeExerciseFromWorkout={removeExerciseFromWorkout} key={exercise.id} exercise={exercise}/>)}
</div>
</div>
)
}
export default Dashboard
If it helps, the data flow I'm working to is:
New array copied from state
New array updated as necessary
New array sent to database
Database listener triggers download of new array
New array saved to state
I have tried to use different methods (set, update and remove) in case that triggered the onValue function.
I have also tried to send null values and deleting empty nodes if the array that will be sent to the db is empty.
The above methods didn't have any impact, there was still a problem with the last array element that was only resolved by refreshing the browser.
I have tried to remove the array dependency and add the workout state as a dependency, resulting in the following error: "Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render."
I think I understand where the issue was:
In the useEffect call, I set up the state to only be updated if the value in returned from the database was null (to prevent an error I ran into). However, this meant that state wasn't being updated at all when I deleted the last item from the array.
I appear to have fixed this by adding an else clause.
useEffect(() => {
function getWorkoutData() {
const dbRef = ref(database, `${user.uid}`);
onValue(dbRef, snapshot => {
if (snapshot.val()) {
console.log(snapshot.val().workouts)
setWorkouts(workouts => workouts = snapshot.val().workouts)
} else {
setWorkouts(workouts => workouts = [])
}
}
)
}
getWorkoutData()
},[])
`````

Parent component does not rerender after updating its state through child component

I checked some of the threads about this and tried to fix it, but no succes yet.
I have a parent and a child component. In the parent component, I declare a state and pass the function to update this state on to my child component.
function ProfileGallery() {
const [selectedPet, setPet] = useState(null);
const [filterText, setFilterText] = useState('');
const [pets, setPets] = useState([]);
const [componentState, setState] = useState('test');
const updateState = (state) => {
setState(...state);
};
return (
<PetInformation
selectedPet={selectedPet}
componentState={componentState}
triggerParentUpdate={updateState}
/>
);
}
In my child component, I do the following:
function PetInformation({ componentState, triggerParentUpdate, ...props }) {
const [status, setStatus] = useState('Delete succesful');
const { selectedPet } = { ...props } || {};
const updatedComponentState = 'newState';
useEffect(() => {
if (status === 'Delete pending') {
deletePet(selectedPet).then(() => setStatus('Delete succesful'));
}
}, [status]);
const handleDelete = () => {
setStatus('Delete pending');
};
return (
<button
className="btn btn-primary pull-right"
onClick={() => {
handleDelete();
triggerParentUpdate(updatedComponentState);
}}
type="button"
>
Delete Pet
</button>
There's of course more code in between, but this shows the gist of what I'm trying to achieve. I want to delete an item I selected in my gallery and have that delete reflected once I click the delete button -> my ProfileGallery needs to re-render. I'm trying to do this by passing that function to update my state on to the child component. I know JS won't consider state changed if the reference remains the same, so that's why I am passing a new const updatedComponentState on, since that should have a different reference from my original componentState.
Everything works, the item gets removed but I still need to manually refresh my app before it gets reflected in the list in my gallery. Why won't ReactJS re-render my ProfileGallery component here? Isn't my state getting updated?

What can I use in functional components to have same behavior as componentDidMount?

My UI was working fine until it was using a class component. Now I am refactoring it to a functional component.
I have to load my UI based on the data I receive from an API handler. My UI will reflect the state of the camera which is present inside a room. Every time the camera is turned on or off from the room, I should receive the new state from the API apiToGetCameraState.
I want the console.log present inside the registerVideoStateUpdateHandlerWrapper to print both on UI load for the first time and also to load every time the video state is changed in the room. However, it doesn't work when the UI is loaded for the first time.
This is how my component looks like:
const Home: React.FunctionComponent<{}> = React.memo(() => {
const [video, setToggleVideo] = React.useState(true);
const registerVideoStateUpdateHandlerWrapper = React.useCallback(() => {
apiToGetCameraState(
(videoState: boolean) => {
// this log does not show up when the UI is loaded for the first time
console.log(
`Video value before updating the state: ${video} and new state is: ${videoState} `
);
setToggleVideo(videoState);
}
);
}, [video]);
React.useEffect(() => {
//this is getting called when the app loads
alert(`Inside use effect for Home component`);
registerVideoStateUpdateHandlerWrapper ();
}, [registerVideoStateUpdateHandlerWrapper ]);
return (
<Grid>
<Camera
isVideoOn={video}
/>
</Grid>
);
});
This was working fine when my code was in class component. This is how the class component looked like.
class Home extends Component {
registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
console.log(`ToggleVideo value before updating the state: ${this.state.toggleCamera} and new state is: ${videoState}`);
this.setStateWrapper(videoState.toString());
})
}
setStateWrapper = (toggleCameraUpdated) => {
console.log("Inside setStateWrapper with toggleCameraUpdated:" + toggleCameraUpdated);
this.setState({
toggleCamera: (toggleCameraUpdated === "true" ) ? "on" : "off",
});
}
constructor(props) {
super(props);
this.state = {
toggleCamera: false,
};
}
componentDidMount() {
console.log(`Inside componentDidMount with toggleCamera: ${this.state.toggleCamera}`)
this.registerVideoStateUpdateHandlerWrapper ();
}
render() {
return (
<div>
<Grid>
<Camera isVideoOn={this.state.toggleCamera} />
</Grid>
);
}
}
What all did I try?
I tried removing the useCallback in the registerVideoStateUpdateHandlerWrapper function and also the dependency array from React.useEffect and registerVideoStateUpdateHandlerWrapper. It behaved the same
I tried updating the React.useEffect to have the code of registerVideoStateUpdateHandlerWrapper in it but still no success.
Move registerVideoStateUpdateHandlerWrapper() inside the useEffect() callback like this. If you want to log the previous state when the state changes, you should use a functional update to avoid capturing the previous state through the closure:
const Home = () => {
const [video, setVideo] = useState(false);
useEffect(() => {
console.log('Inside useEffect (componentDidMount)');
const registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
setVideo((prevVideo) => {
console.log(`Video value before updating the state: ${prevVideo} and new state is: ${videoState}`);
return videoState;
});
});
};
registerVideoStateUpdateHandlerWrapper();
}, []);
return (
<Grid>
<Camera isVideoOn={video} />
</Grid>
);
};
When you no longer actually need to log the previous state, you should simplify registerVideoStateUpdateHandlerWrapper() to:
const registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
setVideo(videoState);
});
};
import React from 'react'
const Home = () => {
const [video, setVideo] = useState(null);
//default video is null, when first load video will change to boolean, when the Camera component will rerender
const registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
setVideo(videoState);
});
};
useEffect(() => {
registerVideoStateUpdateHandlerWrapper();
}, []);
return (
<Grid>
<Camera isVideoOn={video} />
</Grid>
);
};
export default Home
componentDidMount() === useEffect()
'useEffect' => import from 'react'
// componentDidMount()
useEffect(() => {
// Implement your code here
}, [])
// componentDidUpdate()
useEffect(() => {
// Implement your code here
}, [ update based on the props, state in here if you mention ])
e.g:
const [loggedIn, setLoggedIn] = useState(false);
useEffect(() => {
// Implement the code here
}, [ loggedIn ]);
the above code will act as equivalent to the componentDidUpdate based on 'loggedIn' state

Cannot update a component from inside the function body of a different component warning

I wrote a component called component1 as below and it is inside the parent component. The component1 is at the bottom of the page and I don't want to render it unless the user scroll down to that area. Thus I use the InView from 'react-intersection-observer' to determine if this area is in view. If so then fetch data and render data. But I get the warning: Warning: Cannot update a component from inside the function body of a different component. What is the reason of getting this warning? Is it because I set the setInView in the component?
<parent>
<component4 />
<component3 />
<component2 />
<component1 />
</parent>
const component1 = () => {
const [inView, setInView] = React.useState(false);
const [loading, error, data] = fetchData(inView); // hook to fetch data
// data is an array
const content = data.map(d => <div>{d}</div>);
const showEmpty = true;
if (data) {
showEmpty = false;
}
return (<InView>
{({ inView, ref }) => {
setInView(inView);
return (
<div ref={ref}>
<div>{!showEmpty && content}</div>
</div>
)
</InView>);
}
As I understood you want to fetch data when the component gets in view right?
here is my solution with useEffect:
const Componet1 = () => {
const [inView, setInView] = useState(false)
const [myData, setMyData] = useState(null)
useEffect(() => {
if (inView && !myData) {
const [loading, error, data] = fetchData(inView); // hook to fetch data
if (data) {
setMyData(data)
}
}
return () => {
// cleanup
}
}, [inView,myData])
return (
<InView as="div" onChange={(inView) => setInView(inView)}>
{myData ? myData.map((d, i) => <div key={i}>{d}</div>) : 'Loading...'}
</InView>
);
}
Also, this fetches data just once.

Refresh specific component in React

I'm using functional component in React and i'm trying to reload the component after button is clicked.
import { useCallback, useState } from 'react';
const ProfileLayout = () => {
const [reload, setReload] = useState(false);
const onButtonClick = useCallback(() => {
setReload(true);
}, []);
return (
{reload && (
<ProfileDetails />
)}
<Button onClick={onButtonClick} />
);
};
export default ProfileLayout;
I'm not able to see again the component after page loaded.
Note: I don't want to use window.location.reload();
Note: I don't want to use window.location.reload();
That is good because that is not the correct way to do this in React. Forcing the browser to reload will lose all of your component or global state.
The correct way to force a component to render is to call this.setState() or call this.forceUpdate().
If you need to force the refresh, then better use a number than a boolean.
const ProfileLayout = () => {
const [reload, setReload] = useState(0);
const onButtonClick = useCallback(() => {
setReload(p => p+1);
}, []);
return (
{Boolean(reload) && (
<ProfileDetails />
)}
);
};
What do you mean by reloading the component? You want to re-render it or you want to make the component fetch the data again? Like "refresh"?
Anyways the way your component is coded the <ProfileDetails /> component will not show up on the first render since you are doing reload && <ProfileDetails />, but reload is initially false. When you click the button then ProfileDetails will appear, but another click on the button won't have any effect since reload is already set to true.
If you want to refresh the data the component uses, then you need to implement a callback that triggers the data fetching.
Edit after clarification by author
const ProfileContainer = (props) => {
// initialProfile is the profile data that you need for your component. If it came from another component, then you can set it when the state is first initialized.
const [ profile, setProfile ] = useState(props.initialProfile);
const loadProfile = useCallback( async () => {
// fetch data from server
const p = await fetch('yourapi.com/profile'); // example
setProfile(p);
}
return (<><ProfileDetails profile={profile} /> <button onClick={loadProfile} /></>)
}
Alternate approach to load the data within the component
const ProfileContainer = (props) => {
const [ profile, setProfile ] = useState(null);
const loadProfile = useCallback( async () => {
// fetch data from server
const p = await fetch('yourapi.com/profile'); // example
setProfile(p);
}
useEffect(() => loadProfile(), []); // Empty dependency so the effect only runs once when component loads.
return (<>
{ /* needed since profile is initially null */
profile && <ProfileDetails profile={profile} />
}
<button onClick={loadProfile} />
</>);
};

Categories