I might be not understanding completely the principals oh useState hook and there is a problem I came across a few times now.
I'm trying to set an initial state in my component using useEffect without dependencies and later on compare it to some other variables I get from redux.
// get the customer saved in redux
const Customer = useGetCustomer();
const [prevCustomerNumber, setPrevCustomerNumber] = useState(null)
useEffect(() => {
// if there is already customer saved in redux, set it as the previous customer on mount
const { CustomerNumber } = Customer || {}
setPrevCustomerNumber(CustomerNumber)
}, [])
const submit = () => {
//check if there is any change between the customer in redux and the previous one
const { CustomerNumber } = Customer || {}
const submitWithoutChanges = Customer && ( CustomerNumber === prevCustomerNumber)
submitCustomer(Customer, submitWithoutChanges )
}
My problem is - when clicking submit, prevCustomerNumber is always null.
My questions are:
Is it because useState runs again after the first useEffect and override it?
What should I do to save initial state properly?
you can set your initial state of prevCustomerNumber as
const [prevCustomerNumber, setPrevCustomerNumber] = useState("some value")
Your problem is that you get the customer from redux only when the component renders. When you click submit button, the function does not tries to read the data from redux again and only uses the data that already has which may be wrong or null. In order to have the latest data from redux store you need to wrap your component with the Connect function:
export default connect ((state) => {
const customerNumber = state.customer.customerNumber
return {customerNumber}
})(YourComponent)
YourComponent will now receive the customerNumber props every time your redux store changes.
Or if you prefer hooks you can use useSelector hook:
import { useSelector } from 'react-redux'
const customerNumber = useSelector(state => state.customer.customerNumber)
Now every times the components renders or the redux store changes the useSelector hook will return the update value
Related
I am new to React and I am a little confused about what gets updated every time the state or props change. For instance:
const Foo = props => {
const [someState, setSomeState] = useState(null)
let a
useEffect(() => {
a = 'Some fetched data'
}, [])
}
Now if the state (i.e. someState) or props get updated, does it run through the function again, making a undefined? Does only JSX elements that depend on the state/props and the hooks that use them get affected? What changes exactly?
To understand how the state affect your component, you could check this example on Stackblitz. It shows you how the local variable of your component act when a state changes. This behavior is exactly the same when the component receive a new prop. Here is the code just in case :
import React, { Component } from "react";
import { render } from "react-dom";
const App = () => {
const [state, setState] = React.useState('default');
// see how the displayed value in the viewchanges after the setTimeout
let a = Math.floor(Math.random() * 1000);
React.useEffect(() => {
a = "new a"; // Does nothing in the view
setTimeout(() => setState("hello state"), 2000);
}, []);
return (
<div>
{a} - {state}
</div>
);
};
render(<App />, document.getElementById("root"));
What you can see is :
a is initialized with a random value and it is displayed in the view
useEffect is triggered. a is edited but does not re-render your component
After 2 seconds, setState is called, updating the state state
At this point, a value has changed for a new one because your component is re-rendered after a state update. state also changed for what you gave to it
So for your question :
if the state (i.e. someState) or props get updated, does it run through the function again, making a undefined? Does only JSX elements that depend on the state/props and the hooks that use them get affected? What changes exactly?
The answer is yes, it does run through the function again, and the state/props that has been updated are updated in the JSX. Local variable like a will be set to their default value.
I'm new to react hooks. I need help in re-rendering the store (from redux) after deleting items from it. The deleted item is removed from the redux store, but it doesn't render unless I reload the page. I used window.location.reload(false), but I need an alternative that wont require page reload. Help is so much appreciated.
reducers
case "REMOVE_POST": {
const deletePost = [
...state.posts.filter(item => item.id !== actions.posts.id)
];
return {
...state,
posts: deletePost
};
}
component
import { store } from "../../store";
...
const PostsComponent = () => {
const storedPosts = store.getState();
const updatedPosts = storedPosts.posts.posts;
const deletePost = id => {
store.dispatch({
type: "REMOVE_POST",
posts: { id }
});
return updatedPosts;
};
}
...
<button
onClick={() => {
deletePost(post.id);
// window.location.reload(false);
}}
>
close
</button>
Thanks. The question wasn't formatted correctly. I wasn't referring to redux, but to re-render the state after an action. I realise the state in the component was not included. Simply including useState helped.
Thanks again.
Well, you cannot access store by just importing it. Or more precisely if you import it directly you should manually subscribe() for its changes and unsubsrcibe() on unmount(to prevent memory leaks).
You better follow guides and official docs and either use connect() HOC or useSelector hook. Then your component will re-render automatically after changes in the store.
I am trying to do some kind of online shop for myself and I got a problem.
I want to render my shopping cart size in the NavBar component (which is on every page).
I created a Cart Items service where I put all my added items, and it also has functions to addItem, removeItem, getCart, getCartSize.
When I click on Add/Remove on specific product, I would like to do that the value on NavBar with cart size would be changing depending on the cart size (from getCartSize method). I already tried to use useEffect hook, but it does not recognize when the value of cartSize is changed, how can I do that?
This is what I have done already.
navbar.jsx:
//...
//...
import {getTotalCount} from '../../services/myCart';
export default function Navbar() {
// ...
const [count, setCount] = useState();
useEffect(() => {
setCount(getTotalCount());
console.log('counto useeffect');
},[getTotalCount()]);
//},[getTotalCount]); doesn'work also.
//},[count]); doesn'work also.
return (
<>
<header className="navbar">
<Link className="navbar__list-item__home-icon" to="/"><span><FaHome/></span></Link>
<Link className="navbar__list-item" to="/products">Products</Link>
<h2>cart size--> {count}</h2>
<Link className="navbar__list-item__cart-img" to="shopping-cart"><span><FaShoppingCart/></span></Link>
</header>
</>
);
}
myCart.js all functions work fine and I call them when I click add/remove button in on my component in the products page.
var InTheCart = [
];
var totalCount = 0;
export function AddToCart(item) {
// ... logic
totalCount++;
}
export function ContainsInTheCart(id) {
return InTheCart.findIndex(x=> x.item.id == id) != -1 ? true: false;
}
export function RemoveFromCart(id) {
// ... logic
totalCount--;
}
export function getCart() {
return InTheCart;
}
export function getTotalCount() {
return totalCount;
}
React is called react because it re-renders when it reacts to something that changed.
For that to happen, the react components need to know, somehow, that something changed.
In the code example you show, the totalCount (which global access provided by exported function getTotalCount) is independent from react component tree, so that data can change without react knowing it.
So, the question is: how to make your react component aware of those changes?
A few mechanisms exist, here are a few of them:
Use a global state manager like redux, with redux actions and reducer to mutate the state and useSelector in the component that will make them aware of any change, and rerender
React context which will hold the cart state. That context can export both that data, and th function that will mutate the data, so they can be shared between components that have access to that context provider.
Your context could be used in the component that way:
const cartContext = useContext(CartContext)
const { cartState, addToCart, removeFromCart } = cartContext
const { totalCount, inTheCart } = cartState
[...somwhere later...]
<ul><li onClick={() => removeFromCart(index)}>{inTheCart[index].name}</li></ul>
<footer>{`${totalCount} items`}</footer>
I let you build the context object around your existing functions ;)
A simple solution is to lift the state up. Create the state in your layout file from where Header and Cart can access the count as Props. useEffect will be invoked whenever there is a change in the state or props of a component.
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.
I have a working React class component that I want to convert to a functional component to use hooks for state etc. I am learning React hooks. The class component version works fine, the functional component is where I need help.
The data structure consists of a client list with three "clients". An image of it is here:
All I am trying to do is get this data, iterate over it and display the data of each name key to the user. Simple enough.
The problem is that a call to firebase from my component leads to erratic behavior in that the data is not retrieved correctly. The last client name is continuously called and it freezes up the browser. :)
Here is an image of the result:
Here is the code:
import React, {Component,useContext,useEffect, useState} from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '#material-ui/core/styles';
import Paper from '#material-ui/core/Paper';
import Grid from '#material-ui/core/Grid';
import ListItem from '#material-ui/core/ListItem';
import Button from '#material-ui/core/Button';
import firebase from 'firebase/app';
import {Consumer,Context} from '../../PageComponents/Context';
const styles = theme => ({
root: {
flexGrow: 1,
},
paper: {
padding: theme.spacing.unit * 2,
textAlign: 'center',
color: theme.palette.text.secondary,
},
});
const FetchData = (props) =>{
const [state, setState] = useState(["hi there"]);
const userID = useContext(Context).userID;
useEffect(() => {
let clientsRef = firebase.database().ref('clients');
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key;
setState([...state, client])
});
});
//____________________________________________________BEGIN NOTE: I am emulating this code from my class component and trying to integrate it
// this.clientsRef.on('child_added', snapshot => {
// const client = snapshot.val();
// client.key = snapshot.key;
// this.setState({ clients: [...this.state.clients, client]})
// });
//___________________________________________________END NOTE
console.log(state)
return (
<ul>
{
state.map((val,index)=>{
return <a key={index} > <li>{val.name}</li> </a>
})
}
</ul>
)
}
FetchData.propTypes = {
classes: PropTypes.object.isRequired
}
export default withStyles(styles)(FetchData)
By default, useEffect callback is run after every completed render (see docs) and you're setting up a new firebase listener each such invocation. So when the Firebase emits the event each of such listeners receives the data snapshot and each of them adds to the state a received value.
Instead you need to set the listener once after component is mounted, you can do so by providing an empty array of the dependencies ([]) as a second argument to useEffect:
useEffect(() => {
// your code here
}, []) // an empty array as a second argument
This will tell React that this effect doesn't have any dependencies so there is no need to run it more than once.
But there is another one important moment. Since you setup a listener then you need to clean it up when you don't need it anymore. This is done by another callback that you should return in the function that you pass to useEffect:
useEffect(() => {
let clientsRef = firebase.database().ref('clients');
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key;
setState([...state, client])
});
return () => clientsRef.off('child_added') // unsubscribe on component unmount
}, []);
Basically this returned cleanup function will be invoked before every new effect is called and right before a component unmounts (see docs) so only this cleanup function should solve your solution by itself, but there's no need to call your effect after every render anyway hence [] as a second argument.
Your problem is that by default, useEffect() will run every single time your component renders. What is happening, is that your effect triggers a change in the component, which will trigger the effect running again and you end up with something approximating an endless loop.
Luckily react gives us some control over when to run the effect hook in the form of an array you can pass in as an additional parameter. In your case for example:
useEffect(() => {
let clientsRef = firebase.database().ref('clients');
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key;
setState([...state, client])
});
}, []);//An empty array here means this will run only once.
The array tells react which properties to watch. Whenever one of those properties changes it will run the cleanup function and re-run the effect. If you submit an empty array, then it will only run once (since there are no properties to watch). For example, if you were to add [userId] the effect would run every time the userId variable changes.
Speaking of cleanup function, you are not returning one in your effect hook. I'm not familiar enough with firebase to know if you need to clean anything up when the component is destroyed (like for example remove the 'child_added' event binding). It would be good practice to return a method as the last part of your use effect. The final code would look something like:
useEffect(() => {
let clientsRef = firebase.database().ref('clients');
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key;
setState([...state, client])
});
return () => { /* CLEANUP CODE HERE */ };
}, []);//An empty array here means this will run only once.
Effects, by default, run after every render, and setting state causes a render. Any effect that updates state needs to have a dependency array specified, otherwise you'll just have an infinite update-render-update-render loop.
Also, remember to clean up any subscriptions that effects create. Here, you can do that by returning a function which calls .off(...) and removes the listener.
Then, make sure to use the function form of state update, to make sure the next state always relies on the current state, instead of whatever the closure value happened to be when binding the event. Consider using useReducer if your component's state becomes more complex.
const [clients, setClients] = useState([])
useEffect(() => {
const clientsRef = firebase.database().ref("clients")
const handleChildAdded = (snapshot) => {
const client = snapshot.val()
client.key = snapshot.key
setClients(clients => [...clients, client])
}
clientsRef.on("child_added", handleChildAdded)
return () => clientsRef.off('child_added', handleChildAdded)
}, [])
Also see:
How to fetch data with hooks
React Firebase Hooks
A complete guide to useEffect