I'm trying to use a custom react hook to create a counter for each item in a list. the problem is when I increase or decrease a counter, the value of other counters change simultaneously. basically all of the counters show the same value.
here is my custom hook:
import { useState } from "react";
export const useQuantity = (defaultQuantity) => {
const [value, setValue] = useState(defaultQuantity || 1);
const onChange = (e) => {
if (!+e.target.value >= 1) {
setValue(1);
return;
}
setValue(+e.target.value);
};
return {
value,
setValue,
onChange,
};
};
how can I change the value of a counter while it doesn't effect the other counters?
This is the component that I map through the items and for each one of them I render the QuantityInput component.
import { useQuantity } from "../Hook/useQuantity";
import { QuantityInput } from "./QuantityInput";
export const Order = () => {
const quantity = useQuantity();
return (
orders.map((order) => (
<QuantityInput quantity={quantity} />
)
)
}
and this is QuantityInput component:
export const QuantityInput = ({ quantity }) => {
const decrement = () => {
quantity.setValue(quantity.value - 1);
};
const increment = () => {
quantity.setValue(quantity.value + 1);
};
return (
<Button
onClick={decrement}
disabled={quantity.value === 1}
>
-
</Button>
<Input {...quantity} />
<Button onClick={increment}> + </Button>
);
};
Instead of useQuantity in your parent component, you should use it in QuantityInput component so each of them hold their state.
export const QuantityInput = () => {
const quantity = useQuantity(); // Use quantity here, do not need to pass from props
const decrement = () => {
quantity.setValue(quantity.value - 1);
};
const increment = () => {
quantity.setValue(quantity.value + 1);
};
return (
<Button
onClick={decrement}
disabled={quantity.value === 1}
>
-
</Button>
<Input {...quantity} />
<Button onClick={increment}> + </Button>
);
};
The hook that you implemented holds one state value and will only suffice for it.
The solution here is to implement a component that uses this hook. Once you have the component you can render as many instances of it as you want
Also you could simply implement a component instead of custom hook
import { useState } from "react";
export const QuantityInput = (props) => {
const [value, setValue] = useState(props.defaultQuantity || 1);
const onChange = (e) => {
if (!+e.target.value >= 1) {
setValue(1);
return;
}
setValue(+e.target.value);
};
return (
<div>
<div>{value}</div>
<input value={value} onChange={onChange} />
</div>
)
};
const Parent = ({arr}) => {
return arr.map(item => <QuantityInput defaultQuantity={item.defaultQuantity} />)
}
Related
Every time I add or delete a component the whole page reloads, how do I prevent it?
When I use the HandleClick or the handleRemoveItem, the page reset and its components.
export default function Home() {
const [PostArray, setPostArray] = useState<React.ReactNode[]>([]);
let component = <PostSheet />;
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
event.preventDefault();
if (PostArray.length < 8) {
setPostArray((OldArray: any) => [...OldArray, component]);
}
}
const handleRemoveItem = useCallback(
(todo: any) => {
let newPostArray = [...PostArray];
newPostArray.splice(PostArray.indexOf(todo), 1);
setPostArray(newPostArray);
},
[PostArray]
);
const MappedArray = PostArray.map((e) => (
<PostSheet key={uuid()} click={handleRemoveItem} />
));
return (
<DefaultLayout click={handleClick}>
<Wrapper>{MappedArray}</Wrapper>
</DefaultLayout>
);
}
Given the following React Context Provider. A simple Counter class with 2 methods, stored in the React context.
import { createContext, useContext } from "react";
class Counter {
public count: number = 0;
getCount = () => {
return this.count;
};
incrementCount = () => {
this.count = this.count + 1;
};
}
type CounterContextType = {
counter: Counter;
};
const defaults: CounterContextType = {
counter: new Counter()
};
const CounterContext = createContext<CounterContextType>(defaults);
export const CounterProvider: React.FC = ({ children }) => {
const counter = new Counter();
return (
<CounterContext.Provider
value={{
counter
}}
>
{children}
</CounterContext.Provider>
);
};
export const useCounter = () => {
return useContext(CounterContext);
};
I want to listen to changes in the count property of the Counter instance.
Here is what I have tried:
import { useMemo } from "react";
import { CounterProvider, useCounter } from "./CounterProvider";
const DisplayWithMethod = () => {
const { counter } = useCounter();
return <div>Method: {counter.getCount()}</div>;
};
const DisplayWithProperty = () => {
const { counter } = useCounter();
return <div>Prop: {counter.count}</div>;
};
const DisplayWithMemo = () => {
const { counter } = useCounter();
const val = useMemo(() => counter.count, [counter.count]);
return <div>Memo: {val}</div>;
};
const Button = () => {
const { counter } = useCounter();
return <button onClick={counter.incrementCount}>Increment</button>;
};
export default function App() {
return (
<CounterProvider>
<DisplayWithMethod />
<DisplayWithProperty />
<DisplayWithMemo />
<Button />
</CounterProvider>
);
}
None of these work since the counter instance never changes, so no re renders are triggered. Any idea(s) on how to make this work while keeping a class structure for Counter.
https://codesandbox.io/s/nostalgic-fast-cyflg
The issue is with React is not getting that the count has changed and does not rerender. You can get rid of the issue using useState hook.
You should change type definitions like below and construct counter instances using outputs of useState hook.
import { createContext, useContext, useState } from "react";
type CounterContextType = {
counter: {
count: number;
getCount: () => number;
incrementCount: () => void;
};
};
const defaults: CounterContextType = {
counter: {
count: 0,
getCount: () => 0,
incrementCount: () => undefined
}
};
const CounterContext = createContext<CounterContextType>(defaults);
export const CounterProvider: React.FC = ({ children }) => {
const [count, setCount] = useState<number>(0);
return (
<CounterContext.Provider
value={{
counter: {
count,
getCount: () => count,
incrementCount: () => {
setCount((prevCount) => prevCount + 1);
}
}
}}
>
{children}
</CounterContext.Provider>
);
};
export const useCounter = () => {
return useContext(CounterContext);
};
Code Sandbox
I have an 'Autocomplete' grandchild React component which takes data from the child component, helps the user autocomplete a form field, and then passes the value right back up to the grandparent component which posts the whole form.
I am able to get data from grandchild to child, but not able to get the data from there up to the grandparent.
Grandparent - AddPost.jsx
import React, { useState } from 'react';
import GetBeerStyle from './GetBeerStyle';
export default function AddPost(props) {
const [parentBeerStyleData, setParentBeerStyleData] = useState("")
const handleSubmit = (e) => {
e.preventDefault();
// There's some code here that pulls the data together and posts to the backend API
}
return (
<div>
<GetBeerStyle
name="beerstyle"
beerStyleData={childData => setParentBeerStyleData(childData)}
onChange={console.log('Parent has changed')}
/>
// More data input fields are here...
</div>
);
}
Child - GetBeerStyle.jsx
import React, {useState, useEffect } from 'react';
import axios from 'axios';
import Autocomplete from '../Autocomplete';
export default function GetBeerStyle(props) {
const [beerStyleData, setBeerStyleData] = useState("")
const [beerStyles, setBeerStyles] = useState(null)
const apiURL = "http://localhost:8080/api/styles/"
// Code to fetch the beer styles from the API and push down to the grandchild
// component to enable autocomplete
const fetchData = async () => {
const response = await axios.get(apiURL)
const fetchedStyles = Object.values(response.data)
const listOfStyles = []
for (let i = 0; i < fetchedStyles.length; i++) {
(listOfStyles[i] = fetchedStyles[i].style_name)
}
setBeerStyles(listOfStyles)
};
// This snippet pulls data from the Posts table via the API when this function is called
useEffect(() => {
fetchData();
}, []);
return (
<div className="one-cols">
<Autocomplete
suggestions={ beerStyles } // sending the data down to gchild
parentUpdate={childData => setBeerStyleData(childData)}// passing data back to gparent
onChange={() => props.beerStyleData(beerStyleData)}
/>
</div>
);
}
Grandchild - Autocomplete.jsx
import React, { Component, Fragment, useState } from 'react';
export default function Autocomplete(props) {
const [activeSuggestion, setActiveSuggestion] = useState(0);
const [filteredSuggestions, setFilteredSuggestions] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [userInput, setUserInput] = useState("");
const [fieldId, setFieldId] = useState("");
const [parentUpdate, setParentUpdate] = useState("");
const onChange = e => {
const { suggestions } = props;
setActiveSuggestion(0);
setFilteredSuggestions(suggestions.filter(suggestion => suggestion.toLowerCase().indexOf(userInput.toLowerCase()) >-1));
setShowSuggestions(true);
setUserInput(e.currentTarget.value);
setParentUpdate(e.currentTarget.value);
(console.log(parentUpdate));
return props.parentUpdate(parentUpdate);
};
const onClick = e => {
setActiveSuggestion(0);
setFilteredSuggestions([]);
setShowSuggestions(false);
setUserInput(e.currentTarget.innerText);
setFieldId(props.fieldId);
setParentUpdate(e.currentTarget.innerText);
return props.parentUpdate(parentUpdate);
};
const onKeyDown = e => {
// User pressed the ENTER key
if (e.keyCode === 13) {
setActiveSuggestion(0);
setShowSuggestions(false);
setUserInput(filteredSuggestions[activeSuggestion]);
// User pressed the UP arrow
} else if (e.keyCode === 38) {
if (activeSuggestion === 0) {
return;
}
setActiveSuggestion(activeSuggestion - 1);
}
// User pressed the DOWN arrow
else if (e.keyCode === 40) {
if (activeSuggestion - 1 === filteredSuggestions.length) {
return;
}
setActiveSuggestion(activeSuggestion + 1);
}
};
let suggestionsListComponent;
if (showSuggestions && userInput) {
if (filteredSuggestions.length) {
suggestionsListComponent = (
<ul class="suggestions">
{filteredSuggestions.map((suggestion, index) => {
let className;
// Flag the active suggestion with a class
if (index === activeSuggestion) {
className = "suggestion-active";
}
return (
<li className={className} key={suggestion} onClick={onClick}>
{suggestion}
</li>
);
})}
</ul>
);
} else {
suggestionsListComponent = (
<div class="no-suggestions">
<em>No Suggestions Available.</em>
</div>
);
}
}
return (
<Fragment>
<input
type="text"
value={userInput}
onChange={onChange}
onKeyDown={onKeyDown}
id={fieldId}
/>
<div>
{suggestionsListComponent}
</div>
</Fragment>
);
}
While I certainly accept there may be other issues with the code, overriding problem that I seem to have spent an inordinate amount of time googling and researching, is that I can't get the data being entered in the form input, to pull through to the grandparent component!
What have I missed?
I am using usePreviousValue custom hook to get previous props value from my component:
const usePreviousValue = value => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
const MyComponent = ({ count }) => {
const prevCount = usePreviousValue(count)
return (<div> {count} | {prevCount}</div>)
}
But in this case, in prevCount I always have only the first count prop value when a component was rendered, and the next updated prop value is never assigned to it. Are there any ways to properly compare nextProp and prevProp with functional React components?
Your code sample seems to be working just fine. How exactly are you using the component? Try to run the snippet below:
const { useEffect, useRef, useState } = React;
const usePreviousValue = value => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
const MyComponent = ({ count }) => {
const prevCount = usePreviousValue(count);
return (<div> {count} | {prevCount}</div>);
}
function App() {
const [count, setCount] = useState(0);
return (
<div>
<MyComponent count={count} />
<button
onClick={() => setCount((prevCount) => prevCount + 1)}
>
Count++
</button>
</div>
);
}
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
As previously answered, the easiest way to do it is using a custom hook:
import isEqual from "lodash/isEqual";
import { useEffect, useRef } from "react";
const useComponentDidUpdate = (callback, data, checkIfIsEqual) => {
const prevData = useRef(data);
useEffect(() => {
const isTheSame = checkIfIsEqual ? isEqual(data, prevData) : undefined;
callback(prevData.current, isTheSame);
prevData.current = data;
}, [data]);
return null;
};
export default useComponentDidUpdate;
Then in your component:
const Component = ({age})=>{
const [state, setState] = useState({name: 'John', age})
useComponentDidUpdate(prevStateAndProps=>{
if(prevStateAndProps.age !== age || prevStateAndProps.state.name !== state.name){
// do something
}
}, {state, age})
...
}
I try to make a simple meme generator where a user can add a text and change the image on click. Both is working but my clear-button only clears the input field and don't get back to the first image (array[o]).
I mean if I conole.log the "element" it says "0" but it don't change the image to the first one.
My code of App.js so far:
import React, { useEffect, useState } from "react";
import "./styles.css";
function useCounter(initialCount = 0) {
const [count, setCount] = React.useState(initialCount);
const increment = React.useCallback(() => setCount((c) => c + 1), []);
return { count, increment };
}
export default function App() {
let { count: element, increment } = useCounter(0);
const [memes, setMemes] = useState([]);
const [topText, setTopText] = useState("");
useEffect(() => {
async function asyncFunction() {
const initialResponse = await fetch("https://api.imgflip.com/get_memes");
const responseToJSON = await initialResponse.json();
setMemes(responseToJSON.data.memes);
}
asyncFunction();
}, []);
const clear = (e) => {
setTopText("");
element = 0;
console.log(element);
};
return (
<div className="App">
{memes[0] ? (
<div
style={{
height: "300px",
backgroundImage: `url(${memes[element].url})`
}}
>
<p>{topText}</p>
<input
value={topText}
onChange={(e) => setTopText(e.target.value)}
type="text"
/>
<button onClick={clear} type="reset">Clear</button>
<button onClick={increment}>Change Image</button>
</div>
) : (
"loading"
)}
</div>
);
}
What is wrong?
You are attempting to mutate state. You should never directly assign a new value to a stateful variable element = 0. You should use the provided updater function from useState (setCount).
One solution would be to add a reset function to your custom hook and use it:
function useCounter(initialCount = 0) {
const [count, setCount] = React.useState(initialCount);
const increment = React.useCallback(() => setCount((c) => c + 1), []);
const reset = () => setCount(initialCount);
return { count, increment, reset };
}
In your component:
const { count: element, increment, reset: resetCount } = useCounter(0);
const clear = (e) => {
setTopText("");
resetCount();
};
Notice I've also changed the custom hook to use a const instead of let. This is recommended to encourage immutable usage of state, and give helpful errors when breaking that rule.