I'm create Activities function component and call child function component called Categories when i send categories list to Categories function component and log "props" data send twice first one is empty and second has data as follow
Activies
function Activities() {
const [category, setCategory] = useState([]);
function handelChange({ target }) {
setCategory({
...category,
[target.name]: target.value,
});
}
useEffect(() => {
getCategories().then((_categories) => setCategory(_categories));
}, []);
return (<Categories category={category} onChange={handelChange} />)
}
and categories component
function Categories(props) {
console.log(props);
return (<div></div>)
}
i'm trying to log props in useEffect but problem still exist
This is happening because of how the life cycle in React works. This is correct and expected behavior. Before you load the categories, it is a blank array on the initial render. Then it gets the categories, updates the state, and re-renders, this time with categories.
renders with the initial state(empty)
goes and fetches categories
re-renders with the categories
This is entirely expected. That double log is the initial render and then the updated state render. Remember React is a heavily async library. The useEffect doesn't happen during render, it happens after render. Every state update will also cause an update and thus another log. It might be helpful to research what will cause a React render and how the life cycle behaves.
I think you handleChange function should update item in a array of object not change the state object completely
function handelChange({ target: {name, value} }) {
setCategory(categories => {
const categoryIndex = categories.findIndex(pr => pr.id === id);
const category = categories[categoryIndex];
return [
...categories.slice(0, categoryIndex)),
{
...category,
[name]: value,
},
...categories.slice(categoryIndex + 1))
]);
}
Related
I got a react functional component:
const DataGrid = (props) =>
{
const [containerName, setContainerName] = useState("");
const [frameworkComponents, setFrameworkComponents] = useState(
{customLoadingOverlay: LoadingOverlayTemplate,
customNoRowsOverlay: UxDataGridCustomNoRows,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting}/>,
});
useEffect(async () =>
{
if(props.containerName && props.containerName !== "")
{
setContainerName(props.containerName);
}
},[props.containerName]);
.
.
.
const onDeleteSetting = async (settingKey) =>
{
console.log("ON DELETE AND CONTAINER NAME:");
console.log(containerName); //HERE THE CONTAINER NAME IS EMPTY
...
}
return (
<UxDataGrid
frameworkComponents={frameworkComponents}/>
);
The container name inside useEffect exists and is not empty. As you can see in the comment in onDeleteSetting, the containerName is empty when this callback is invoked. I tried adding this to the useEffect after setContainerName:
setFrameworkComponents({customLoadingOverlay: LoadingOverlayTemplate,
customNoRowsOverlay: UxDataGridCustomNoRows,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting}/>,
});
That didn't work.
How can I get the name inside the callback? There is no special need to leave that frameworkComponents struct in the state.. it can also be moved to somewhere else if you think its better
Try this in your useEffect, update the onDeleteSetting function with the new containerName when it's updated
.....
useEffect(async() => {
if (props.containerName && props.containerName !== "") {
setContainerName(props.containerName);
// move this function here
const onDeleteSetting = async(settingKey) => {
console.log("ON DELETE AND CONTAINER NAME:");
// use props.containerName since the state update is async
console.log(props.containerName);
...
}
// update your components with the updated functions
setFrameworkComponents(prevComponents => ({
...prevComponents,
editButton: params =>
<ViewAndDeleteSetting
{...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting}
/>,
}));
}
}, [props.containerName]);
.....
This should provide the updated state with the updated function, if it works, I can add more details.
You almost certainly shouldn't be storing it in state. Props are essentially state controlled by the parent. Just use it from props. Copying props to state is usually not best practice.
If you're looking at one of the very rare situations where it makes sense to set derived state based on props, this page in the documentation tells you how to do that with hooks. Basically, you don't use useEffect, you do your state update right away.
Here's a full quote from the linked documentation:
How do I implement getDerivedStateFromProps?
While you probably don’t need it, in rare cases that you do (such as implementing a <Transition> component), you can update the state right during rendering. React will re-run the component with updated state immediately after exiting the first render so it wouldn’t be expensive.
Here, we store the previous value of the row prop in a state variable so that we can compare:
function ScrollView({row}) {
const [isScrollingDown, setIsScrollingDown] = useState(false);
const [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row changed since last render. Update isScrollingDown.
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
This might look strange at first, but an update during rendering is exactly what getDerivedStateFromProps has always been like conceptually.
If you did it the same way they did in that example, your component would still render with containerName set to the default state (""), it's just that it will then almost immediately re-render with the updated containerName. That makes sense for their example of a transition, but you could avoid that by making the prop's initial value the state's initial value, like this:
const DataGrid = (props) => {
const [containerName, setContainerName] = useState(props.containerName); // *** ONLY USES THE INITIAL PROP VALUE
const [frameworkComponents, setFrameworkComponents] = useState(
// ...
});
// *** Updates the state value (on the next render) if the prop changes
if (containerName !== props.containerName) {
setContainerName(props.containerName);
}
// ...
};
Every time the containerName prop changes, though, your component will render twice, which brings us back full circle to: Don't store it in state, just use it from props. :-)
Stepping back and looking at the component as a whole, I don't think you need any state information at all, but if your goal is to avoid having the frameworkComponents you pass UxDataGrid change unnecessarily, you probably want useMemo or React.memo rather than state.
For instance, with useMemo (but keep reading):
const DataGrid = ({containerName}) => {
const frameworkComponents = useMemo(() => {
const onDeleteSetting = async (settingKey) => {
console.log("ON DELETE AND CONTAINER NAME:");
console.log(containerName);
// ...
};
return {
customLoadingOverlay: LoadingOverlayTemplate,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting} />,
};
}, [containerName]);
return (
<UxDataGrid frameworkComponents={frameworkComponents} />
);
};
But if componentName is your only prop, it may well be even simpler with React.memo:
const DataGrid = React.memo(({containerName}) => {
const onDeleteSetting = async (settingKey) => {
console.log("ON DELETE AND CONTAINER NAME:");
console.log(containerName);
// ...
};
return (
<UxDataGrid frameworkComponents={{
customLoadingOverlay: LoadingOverlayTemplate,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting} />,
}} />
);
});
React.memo memoizes your component, so that your component function is only ever called again when the props change. Since everything in the component needs to update based on the componentName prop changing, that looks like a good match (but I don't know what UxDataGrid is).
The problem was with how I tried passing props to ViewAndDeleteSetting. If you want to pass prop to a cell rendered component, you shouldn't be doing it in frameworkComponents, but rather you need to do it in the column definition like this:
useEffect(() =>
{
let columns = [{headerName: '', cellRenderer: 'editButton', width: 90, editable: false,
cellRendererParams: {
openAddConfigurationsWindow: openAddConfigurationsWindow,
onDeleteSetting: onDeleteSetting
}},
.. other columns
]
setColumnDefinition(columns);
},[props.containerName]);
The columns with the cellRendererParams do gets recreated in the useEffect when the name changes, and then the component can access this params regularly via its props
For my react-app/Express app I am trying to update the component state using the useEffects to run once when the component renders. Within useEffect I make a fetch to the express server.
const Favorites = ({ user }) => {
const loggedIn = user.loginname === "" ? false : true;
const [favs, setFavs] = useState([]);
useEffect(() => {
if (loggedIn) {
fetch(`/user/favs/${user.loginname}`)
.then((resp) => resp.json())
.then((data) => {
console.log(data);
setFavs([...data]);
console.log(favs);
});
}
}, []);
return (
<div className="mt-d d-flex justify-content-center">
{loggedIn ? (
<FavoritesList favs={favs} />
) : (
<h3 className="my-2">Please login to use this feature</h3>
)}
</div>
);
};
I make a fetch call on line 11 and am able to print the results on line 14. I then try to update the
component state using setFavs. My issue is that the state seems to not be updated or maybe
there is some async issue.
const FavoritesList = ({ favs, prop }) => {
const [data, setData] = useState([]);
useEffect(() => {
console.log(favs);
// favs.forEach(item => console.log(item))
}, []);
return <h5>Dummy component</h5>;
};
When I try to print favs on line 16 or print favs within the child(FavoritesList) component it is being passed down to, I get an empty array.
When I try to print favs on line 16 or print favs within the child(FavoritesList) component it is being passed down to, I get an empty array.
You're right, it is an async issue: your call to setFavs is async and favs is not yet set on line 16. Calling setFavs will cause your UI to re-render, eventually.
You won't see it in your other useEffect on line 35, either, because that useEffect hook also only runs on first render ([]), so that value is not there yet on first render. To see all updates to favs, try adding it to the dependencies array (like [favs]) or remove the dependencies array altogether.
Change your useEffect on the FavoritesList to:
useEffect(() => {
console.log(favs)
}, [favs])
That way the useEffect will watch for any changes in the favs props. Using an empty array means that the useEffect will only trigger in the first render of the functional component. Meaning that it will not be triggered in any of the props change.
I think above two answers made everyhing clean, here is your lifecycle;
1 -> Favorites component initialized and rendered (state favs = [empty])
2 -> FavoritesList component initialized and rendered (prop favs = [empty])
3 -> FavoritesList useEffect called (console.log(favs) => [empty array])
4 -> Favorites useEffect called (state favs = [now has data])
5 -> FavoritesList component props is updated (prop favs = [has data]
(prop is updated but you didnt call console.log again, after update)
As you can see FavoritesList useEffect called before Favorites useEffect so it called when favs has no data.
So if you add favs prop to array in useEffect in FavoritesList component, you will have one more step;
6 -> FavoritesList useEffect called again (console.log(favs) => [has data])
Because useEffect looks for that arrays elements and if one the elements changed it will triger itself again.
Here is your code with working example: codesandbox.io
I have implemented a code that listen my DB documents and when a new one is added the app renders it as an item on my FlatList.
The problem I have is that everytime I update the data of the FlatList, the already rendered items re-renders again and again...
As my original code is complex, I have build a Snack:
https://snack.expo.io/#victoriomolina/flatlist-re-renders-all-components
I think the problem is that I update the state using a shallow copy of the existing array, but I do it just to re-render the FlatList when new items are added (I don't want to re-render the already rendered items).
Thank you, I will really appreciate your help.
Pd: In my original code the components of the FlatList extends React.PureComponent
My Real Code
The fething part
const [posts, setPosts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const { firebase } = props;
let postsArray = [];
// Realtime database listener
const unsuscribe = firebase
.getDatabase()
.collection("posts")
.doc(firebase.getCurrentUser().uid)
.collection("userPosts")
.orderBy("date") // Sorted by upload date
.onSnapshot((snapshot) => {
let changes = snapshot.docChanges();
changes.forEach((change) => {
if (change.type === "added") {
// Get the new post
const newPost = change.doc.data();
// Add the new post to the posts list
postsArray.unshift(newPost);
}
});
// Reversed order so that the last post is at the top of the list
setPosts([...postsArray]); // Shallow copy of the existing array -> Re-render when new posts added
setIsLoading(false);
});
/* Pd: At the first time, this function will get all the user's posts */
return () => {
// Detach the listening agent
unsuscribe();
};
}, []);
The FlatList
<FlatList
data={data}
keyExtractor={keyExtractor}
legacyImplementation={false}
numColumns={1}
renderItem={this.renderItem}
getItemLayout={this.getItemLayout}
initialNumToRender={12}
windowSize={40}
maxToRenderPerBatch={15}
updateCellsBatchingPeriod={50}
removeClippedSubviews
ListFooterComponent={this.renderFooter()}
/>
The render item method
renderItem = ({ item, index }) => {
const {
images,
dimensions,
description,
location,
likes,
comments,
date,
} = item;
return (
<View
key={index}
onLayout={({ nativeEvent }) => {
this.itemHeights[index] = nativeEvent.layout.height;
}}
>
<Card { /* Extends React.PureComponent */ }
images={images}
postDimensions={dimensions}
description={description}
location={location}
likes={likes}
comments={comments}
date={date}
/>
</View>
);
};
When your data updates, component re-renders.
To prevent that, you need to this this line before calling fetchData()
useEffect(() => {
if (data) return;
fetchData();
}, [data]);
*edit: add data to the dependencies array
What will happen is that the useEffect will run when the component loads, and will call fetchData function which updates your state therefore component re-renders, so the next render data will have any value so the if statement will prevent the 2nd call to fetchData.
I also suggest to initial data with null
const [data, setData] = useState(null);
I removed this:
onEndReached={fetchData}
and it worked fine (see it online). The problem is that react-native calls onEndReached when it ends rendering. so you'll get the initial datas again on each render, and it causes infinit rendering issue. (more)
The solution that has worked for me:
In render item I was passing the index as a key to the item. I have read that this can produce strange behaviour, so I decided to pass item.id (which is an UUID) instead.
Change the PureComponent to a Standard Component and reimplement the componentShouldUpdate life-cycle method. If you have a PureComponent it will be:
// From the RN documentation
shouldComponentUpdate(nextProps, nextState) {
return nextProps !== this.props && nextState !== this.state;
}
So, I have decided to change my Item to a normal Component and do:
shouldComponentUpdate(nextProps, nextState) {
// My component is just a card with an author row and a progressive image (thumbnail + image) with a Skeleton absolutely positioned
return nextState.avatarIsLoaded && nextState.thumbailIsLoaded; // && nextState.imageIsLoaded is unnecesary by the way I have implemented my component
}
Pd: Also it is better to do as I did because if I add the * && nextState.imageIsLoaded * I will have to wait a long time until the full image (which size is bigger than the thumbnail's one) is loaded.
Now my items render twice, firstly when the Skeleton is showed, and secondly when the progressive image is ready to being displayed.
I have a todo list app which users can read and save items to. Here, Todo is a functional component that queries an API for the users current items on their list using the useEffect() hook. When a successful response is received the data is added to the component's state using useState() and rendered as part of the ItemList component.
When a user submits the form within the AddItemForm component a call back is fired that updates the state of newItem, a dependency of useEffect, which triggers another call to the API and a re-render of the component.
Logically, everything above works. However, it seems wrong to make an extra request to the API simply to receive the data that is already available but I can't find the correct pattern that would allow me to push the item available in useCallback to the items array without causing useEffect to loop infinitely yet still update the ItemList component.
Is there away for my app to push new date from the form submission to items array whilst updating the view and only calling the API once when the page loads?
function Todo() {
const [items, setItems] = useState();
const [newItem, setNewItem] = useState();
useEffect(() => {
fetch('https://example.com/items').then(
(response) => {
setItems(response.data.items);
}, (error) => {
console.log(error);
},
);
}, [newItem]);
const updateItemList = useCallback((item) => {
setNewItem(item);
});
return (
<>
<AddItemForm callback={updateItemList} />
<ItemList items={items} />
</>
);
}
function ItemList(props) {
const { items } = props;
return (
<div>
{ items
&& items.map((item) => <p>{item.description}</p>)}
</div>
);
}
Call API only on start by removing newItem from useEffect(...,[]).
Then add item to the items by destructuring in setItems:
const updateItemList = (item) => {
setItems([...items, item]);
}
I have got an hook who catch getBoundingClientRect object of a ref DOM element. The problem is, at the first render, it return null and I need to get the value only on first render on my component.
I use it like that in a functional component:
const App = () => {
// create ref
const rootRef = useRef(null);
// get Client Rect of rootRef
const refRect = useBoundingClientRect(rootRef);
useEffect(()=> {
// return "null" the first time
// return "DOMRect" when refRect is update
console.log(refRect)
}, [refRect])
return <div ref={rootRef} >App</div>
}
Here the useBoundingClientRect hook, I call in App Component.
export function useBoundingClientRect(pRef) {
const getBoundingClientRect = useCallback(() => {
return pRef && pRef.current && pRef.current.getBoundingClientRect();
}, [pRef]);
const [rect, setRect] = useState(null);
useEffect(() => {
setRect(getBoundingClientRect());
},[]);
return rect;
}
The problem is I would like to cache boundingClientRect object on init and not the second time component is rerender :
// App Component
useEffect(()=> {
// I would like to get boundingClientRect the 1st time useEffect is call.
console.log(refRect)
// empty array allow to not re-execute the code in this useEffect
}, [])
I've check few tutorials and documentations and finds some people use useRef instead of useState hook to keep value. So I tried to use it in my useboundingClientRect hook to catch and return the boundingClientRect value on the first render of my App component. And it works... partially:
export function useBoundingClientRect(pRef) {
const getBoundingClientRect = useCallback(() => {
return pRef && pRef.current && pRef.current.getBoundingClientRect();
}, [pRef]);
const [rect, setRect] = useState(null);
// create a new ref
const rectRef = useRef(null)
useEffect(() => {
setRect(getBoundingClientRect());
// set value in ref
const rectRef = getBoundingClientRect()
},[]);
// return rectRef for the first time
return rect === null ? rectRef : rect;
}
Now the console.log(rectRef) in App Component allow to access the value on first render:
// App Component
useEffect(()=> {
console.log(refRect.current)
}, [])
But If I try to return refRect.current from useBoundingClientRect hook return null. (What?!)
if anyone can explain theses mistakes to me. Thanks in advance!
You need to understand references, mututation, and the asynchronous nature of updates here.
Firstly, when you use state to store the clientRect properties when your custom hook in useEffect runs, it sets value in state which will reflect in the next render cycle since state updates are asynchronous. This is why on first render you see undefined.
Secondly, when you are returning rectRef, you are essentially returning an object in which you later mutate when the useEffect in useBoundingClientRect runs. The data is returned before the useEffect is ran as it runs after the render cycle. Now when useEffect within the component runs, which is after the useEffect within the custom hook runs, the data is already there and has been updated at its reference by the previous useEffect and hence you see the correct data.
Lastly, if you return rectRef.current which is now a immutable value, the custom hook updates the value but at a new reference since the previous one was null and hence you don't see the change in your components useEffect method.