I am trying to build an ecommerce website, and I hit a problem I cannot seem to resolve. I am very new to react and JS so have some patience please :)
I declared 4 useStates in my app.js:
const [elementeDinState, setElementeDinState] = useState([]);
const [currentCategorie, setCurrentCategorie] = useState("Acasa");
const [subCategorie, setSubcategorie] = useState([]);
const [cartContents, setCartContents] = useState([]);
const fetchData = useCallback(async () => {
const data = await getCategories();
setElementeDinState(data);
}, []);
useEffect(() => {
fetchData().catch(console.error);
}, [fetchData]);
const changeHeader = (dataFromMenuItem) => {
setCurrentCategorie(dataFromMenuItem);
};
const changeCopiiContent = (data1FromThere) => {
setSubcategorie(data1FromThere);
};
const changeCart = (dataFromCart) => {
setCartContents(dataFromCart);
};
I am passing the functions to change those states to different child components as props. my problem is, when I add items to cart it triggers a re render of my component (products listing component) that should not be affected by cartContents and that resets the state of said component to the initial value that changes the items being shown. does useState hook create a single global state comprised of all those states?
If these useState are defined in the app.js and then passed down, when a child will use them chasing the state will happen in the app.js so all the children of <App /> will be re-rendered.
I guess that your app.js looks similar:
function App() {
const [elementeDinState, setElementeDinState] = useState([]);
// ...and the other hooks and methods
return (
<cartContents setElementDinState={setElementeDinState} />
<ProductList />
)
}
In this case the state is in the component so when <CartContents /> changes it, it will trigger a re-render of the and all its children <ProductList /> included.
To avoid this problem think better when each piece of state needs to be and put the state as near as possibile to that component. For example, if the state of the cart does not influence the Product list. Move the useState in the <Cart /> component.
From what I understand, your problem is that you're simply resetting the cartContents state every time you call the changeCart function, correct?
What you probably want, is to add (or remove ?) the item to the cart, like this?
const changeCart = (dataFromCart) => {
setCartContents(oldContents => [...oldContents, dataFromCart]);
};
Here is a description of useState from the oficial site:
useState is a Hook (...). We call it inside a function component to add some local state to it
So it creates just a local state.
About your problem, We need more information, but I believe that some parent component of that widget is trying to render other component instead of your the component that you wanted (let's call it "ProblemComponent") and rendering you ProblemComponent from scratch again, before you can see it.
it's something like that:
function ParentComponent(props: any) {
const isLoading = useState(false);
// Some logic...
if(isLoading) {
return <LoadingComponent/>;
}
return <ProblemComponent/>;
}
If that doesn't work you can also try to use React.memo() to prevent the ProblemComponent to update when it props change.
well, seems like I wanted to change the way react works so I figured out a work around, based on what you guys told me. I declared the state of the productsComponent in the parent component and adding to cart now doesn't force a refresh of the items being shown. thank you!
Related
I have a ParentComponent looking like this:
const ParentComponent = () => {
[product, setProduct] = useState({
name: "Test",
description: "Information goes here..."
})
return(
<ChildComponent product={product} updateProduct={setProduct}/>
);
}
and a ChildComponent as follows:
const ChildComponent = ({product, updateProduct}) => {
// some code here
return(
<RichTextEditor content={product.description} update={updateProduct}/>
)
}
const isEqual = () => {
return true; // I want to force the component to never re-render
}
export default React.memo(ChildComponent, isEqual)
What's happening here is that I have a product object with a description that is updated by the ChildComponent. The value for the description gets set via updateProduct every time the input changes in the RichTextEditor component. I understand that because of the state change of product the ChildComponent gets re-rendered everytime which is unfortunate because that can cause unwanted effects like making the input field inside RichTextEditor lose focus for example. So I tried forcing the ChildComponent to never re-render with my isEqual function that always returns true. It keeps re-rendering though, why is that?
I realize that there are other designs available to avoid this issue completely, and I'm probably going to do that anyways but before I do that I want to understand why I can not force ChildComponent to not re-render.
If you are using useState, useContext or useReducer then the component will still re-render.
This is straight from the docs.
I have a basic e-commerce app for practice. There's a functional component called "Shop" which has two states:
[products, setProducts] = useState([10ProductObjects])
and [cart, setCart] = useState([])
On the first render cycle, the 10 products are loaded and each Product component has an "add to cart" button that adds a product to the cart array.
When I click the button, the cart array gets populated and its length is displayed. However, doing so re-renders the 10 products again even though nothing has been changed on them.
Now as I see it since one of the states changes i.e. the cart array, the whole Shop component is rendered again. Which in turn renders its child components, including those which were not changed.
I've tried to use React.memo but it doesn't help since no props are being changed on "Shop", rather the state is changing. I've also used the useMemo hook and it had some interesting results.
Using the products array as a dependency solves the extra re-rendering problem, but the new products are not added to the cart anymore. Using both [products, cart] as the dependencies works but brings back the original problem.
I know it could be done using shouldComponentUpdate but I need that kind of flexibility in functional components.
N.B: This is my first ever question and I'm all ears for any kind of feedback.
import React, { useState, useMemo } from 'react';
import fakeData from '../../fakeData';
import Product from '../Product/Product';
const Shop = () => {
console.log('[Shop.js] rendered')
const first10 = fakeData.slice(0, 10);
const [products, setProducts] = useState(first10);
const [cart, setCart] = useState([]);
const addProductHandler = (product) => {
console.log(cart, product);
const newCart = [...cart, product];
console.log(newCart);
setCart(newCart);
}
let productsOnScreen = useMemo(() => {
return products.map( prod => {
return <Product product={prod} addProductHandler={addProductHandler} />
});
}, [products])
return (
<div className="shop-container">
<div className="product-container">
{productsOnScreen}
</div>
<div className="cart-container">
<h3>this is cart</h3>
<h5>Order Summary: {cart.length}</h5>
</div>
</div>
);
};
export default Shop;
Memoize addProductHandler with React.useCallback so that the reference to it does not change between renders:
const addProductHandler = React.useCallback((product) => {
setCart(oldCart => {
return [...oldCart, product];
});
}, [setCart]);
Then, memoize <Product> with React.memo. You didn't post your code for that component but it would look something like this:
export const Product = React.memo((props) => {
// normal functional component stuff
return <>product component or whatever</>;
});
These are both necessary for a component to avoid unnecessary rerenders. Why?
React.useCallback allows for comparison-by-reference to work for a callback function between renders. This does not work if you declare the callback function every render, as you have currently.
React.memo wraps a component to enable it to render or not render depending on a shallow comparison of its props. React components do NOT do this by default, you have to explicitly enable it with React.memo.
As the docs mention:
This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs.
In other words, you should only use this as a performance optimization if you are experiencing slowdowns, NOT to prevent renders for any other reason, and ONLY if you are actually experiencing performance issues.
If this doesn't work, it's likely that one of your <Product> props is still changing (in the context of shallow comparison-by-reference) between renders. Keep playing with and memoizing props until you figure out which ones are changing between renders. One way to test this is a simple React.useEffect (for debug purposes only) inside of <Product> which will alert you when a prop has changed:
React.useEffect(() => {
console.log('product prop changed');
}, [product]);
React.useEffect(() => {
console.log('addProductHandler prop changed');
}, [addProductHandler]);
// etc...
I am practising React props by building an input form using hooks. The idea is when I enter the inputs in NewList component and click the submit button, its values will render on the MyJobList component in a form of HTML elements. The issue is when I submitted the form, nothing is displaying on the web page. It could be that I am passing the data incorrectly.
Here are my codes. I have minimized it to highlight possible problems and included a link to full codes down below:
newlist.js
const NewList = () => {
const [inputState, setInputState] = useState({});
const onSubmitList = () => {
setInputState({
positionTitle: `${inputs.positionTitle}`,
companyName: `${inputs.companyName}`,
jobLink: `${inputs.jobLink}`
});
};
const { inputs, handleInputChange, handleSubmit } = CustomForm(onSubmitList);
myjoblist.js
const MyJobList = inputs => (
<div>
<h3>{inputs.positionTitle}</h3>
<p>{inputs.companyName}</p>
<p>{inputs.jobLink}</p>
</div>
);
navbar.js
const Navbar = inputState => (
<Router>
<Switch>
<Route path="/my-list">
<MyJobList inputs={inputState} />
</Route>
</Switch>
</Router>
);
Any help and guidance are much appreciated.
Here's a link to complete code: Code Sandbox
So, you need the persisting state to live in a shared parent component, in the existing components, your choices would be either App.js or navbar.js (side note, you should PascalCase your component file names, so NavBar.js). Ideally, you should make a shared container that will hold the state. When you navigate away from the newlist component to view myjoblist, the newlist component unmounts and you lose the state. With a shared parent component, the parent won't unmount when it renders its children (newlist & myjoblist).
Another problem is that you you are passing a callback to your custom hook, but the callback doesn't have any arguments. In your handle click, you need to pass it the inputs. You also cannot set your state to an empty string before you pass the inputs to your callback, do it after.
const handleSubmit = event => {
e.preventDefault()
callback(inputs)
setInputs({}) // set it back to the default state, not a string
}
Lastly, the inputState in navbar is an undefined variable. You are rendering Navbar inside your app and not passing it any props. The first argument to a functional component in react is props. So, even if you were passing it state, you'd need to extract via props.inputState or with destructuring.
Here's a working example that could still use some clean up:
https://codesandbox.io/s/inputform-with-hooks-wwx2i
I'm new in react hooks and I just don't see this on docs:
const MyComponent = ({myProp}) => {
const [myPropHook, setPropHook] = useState(myProp)
...
}
I'm wondering if this is a good practice?
The value you pass to useState is used as a starting value for the state variable. So, when your component props change, they will not affect the state variable you are using. The initial value would be the first props sent to the component and after that can be modified only using the setPropHook function.
So, in short, it is definitely a code smell to use props as initializers for useState because reading the code does not correctly convey what will actually happen.
You don't see it much because it doesn't make a lot of sense in terms of how a React app should distribute its state.
If a prop value is set higher up the tree, it shouldn't be used as part of the separate state within a component. It makes sense to use prop values to determine the state of a component indirectly as in 'if the prop is this, then set the state to that', but not to directly copy the prop in to the initial value.
In other words, the internal state of a component (accessed via the useState and useReducer Hooks) should be determined by the component, not directly by the parent(s).
Yes, this is bad. What you're doing is passing a prop to the state, and it is discouraged by many.
The React docs says that "using props to generate state often leads to duplication of “source of truth”, i.e. where the real data is.". The danger is that if the props is changed without the component being refreshed, the new prop value will never be displayed, because the initialization of state from props only runs when the component is first created.
The only exception would be to use the prop as a seed for an internally-controlled state. After several years of react development, I've never encountered such a case.
Further reading:
React component initialize state from props (SO question)
React Anti-Patterns: Props in Initial State (medium.com article)
Why Setting Props as State in React.js is Blasphemy (blog post)
If you are trying to receive a prop to that functional component, then yes, but not exactly like you have it written. So in the parent component you will have something like this:
const App = () => {
const [resource, setResource] = useState("posts");
and then there is a component inside the JSX like so:
const App = () => {
const [resource, setResource] = useState("posts");
return (
<div>
<div>
<button onClick={() => setResource("posts")}>Posts</button>
<button onClick={() => setResource("todos")}>Todos</button>
</div>
<ResourceList resource={resource} />
</div>
);
};
That ResourceList component has to be able to receive the props that the App component is passing to it. Inside a class-based component you would do {this.props.resource}, but in our case, where its a functional component using React hooks you want to write it like so:
const ResourceList = (props) => {
const [resources, setResources] = useState([]);
or via ES6 destructuring like so:
const ResourceList = ({ resource }) => {
const [resources, setResources] = useState([]);
I have a route which takes an id and renders the same component for every id, for example :
<Route path='/:code' component={Card}/>
Now the in the Link tag I pass in an id to the component.Now the Card component fetches additional detail based on the id passed. But the problem is it renders only for one id and is not updating if I click back and goto the next id. I searched and found out that componentsWillReceiveProps can be used but during recent versions of React it has been deprecated. So how to do this?
Putting current location as key on component solves problem.
<Route path='/:code' component={(props) => <Card {...props} key={window.location.pathname}/>}/>
I just ran into a similar problem. I think you are conflating updating/rerendering and remounting. This diagram on the react lifecycle methods helped me when I was dealing with it.
If your problem is like mine you have a component like
class Card extend Component {
componentDidMount() {
// call fetch function which probably updates your redux store
}
render () {
return // JSX or child component with {...this.props} used,
// some of which are taken from the store through mapStateToProps
}
}
The first time you hit a url that mounts this component everything works right and then, when you visit another route that uses the same component, nothing changes. That's because the component isn't being remounted, it's just being updated because some props changed, at least this.props.match.params is changing.
But componentDidMount() is not called when the component updates (see link above). So you will not fetch the new data and update your redux store. You should add a componentDidUpdate() function. That way you can call your fetching functions again when the props change, not just when the component is originally mounted.
componentDidUpdate(prevProps) {
if (this.match.params.id !== prevProps.match.params.id) {
// call the fetch function again
}
}
Check the react documentation out for more details.
I actually figured out another way to do this.
We'll start with your example code: <Route path='/:code' component={Card}/>
What you want to do is have <Card> be a wrapper component, functional preferrably (it won't actually need any state I don't think) and render the component that you want to have rendered by passing down your props with {...props}, so that it gets the Router properties, but importantly give it a key prop that will force it to re-render from scratch
So for example, I have something that looks like this:
<Route exact={false} path="/:customerid/:courierid/:serviceid" component={Prices} />
And I wanted my component to rerender when the URL changes, but ONLY when customerid or serviceid change. So I made Prices into a functional component like this:
function Prices (props) {
const matchParams = props.match.params;
const k = `${matchParams.customerid}-${matchParams.serviceid}`;
console.log('render key (functional):');
console.log(k);
return (
<RealPrices {...props} key={k} />
)
}
Notice that my key only takes customerid and serviceid into account - it will rerender when those two change, but it won't re-render when courierid changes (just add that into the key if you want it to). And my RealPrices component gets the benefit of still having all the route props passed down, like history, location, match etc.
If you are looking for a solution using hooks.
If you are fetching data from some API then you can wrap that call inside a useEffect block and pass history.location.pathname as a parameter to useEffect.
Code:
import { useHistory } from "react-router";
const App = () => {
const history = useHistory();
useEffect(() => {
//your api call here
}, [history.location.pathname]);
};
useHistory hook from react-router will give the path name so the useEffect will be called everytime it (url) is changed
as described by #theshubhagrwl but
you can use location.href instead of location.pathname to work in all condition
import { useHistory } from "react-router";
const App = () => {
const history = useHistory();
useEffect(() => {
// do you task here
}, [history.location.href]);
};
You can use use UseLocation() from "react-router-dom"
and then use that object in useEffect dependency array.
import {useLocation} from "react-router-dom";
export default function Card() {
const location = useLocation();
useEffect(()=>{}, [location]);
return(
// your code here
);
}
In React Router v4 Adding a Switch tag after Router fixes the problem