Container
import { InputField } from './InputField';
const sleep = (time: number) => new Promise((res) => setTimeout(res, time, ''));
export const Container = () => {
const [inputValue, setInputValue] = React.useState('');
React.useEffect(() => {
(async () => await sleep(1000))();
async function fetchMyAPI(time, value) {
await sleep(time);
setInputValue(value);
}
fetchMyAPI(1000, 'vbc1');
fetchMyAPI(2000, 'dgi1');
}, []);
const inputChange = (value) => {
setInputValue(value);
};
return <InputField inputValue={inputValue} inputChange={inputChange} />;
};
InputField
export const InputField = ({
inputValue,
inputChange,
}: {
inputValue: string;
inputChange: (value: string) => void;
}) => {
const [value, setValue] = React.useState('');
React.useEffect(() => {
setValue(inputValue.slice(0, -1));
}, [inputValue]);
const handleChange = (event) => {
setValue(event.target.value);
inputChange(event.target.value + '1');
};
return <input value={value} onChange={handleChange} />;
};
inputValue above can change multiple times.
also a local variable in input is used to display , and inputValue is slightly different from it. So when we keep track of InputValue , we pass the cleared data to the local variable. And vice versa, we modify the data to put in the inputValue.
React.useEffect(() => {
setValue(inputValue.slice(0, -1));
}, [inputValue]);
Every time we call handleChange : we do setValue and inputChange. Thus, we change the value variable and the inputValue variable. After the inputValue is changed, useEffect is called which observes the inputValue. And overwrites exactly the same value of the Value variable. This is problem!
What is the correct solution to this problem?
You can create a boolean state effectRan to track whether the effect already ran or not, and only invoke the effect's logic if effectRan == false, then set it to true.
When the effect runs again with it as true, have it set it back to false to prepare to run again in the next change.
I changed the code a bit to highlight the approach:
const {useState, useEffect } = React
const InputField = () => {
const [value, setValue] = React.useState('');
const [effectRan, setEffectRan] = useState(true);
React.useEffect(() => {
if (!effectRan) {
setValue(prev => prev + '-');
setEffectRan(true)
console.log('Effect just ran');
} else {
setEffectRan(false)
}
}, [value]);
const handleChange = (event) => {
setValue(event.target.value);
};
return <input onChange={handleChange} value={value} />;
};
ReactDOM.render(<InputField />, root)
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Related
Language used: javascript with react
I have made a custom HOOK to keep the previous state
here my custom hooks
import { useEffect, useRef } from 'react';
export const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
here where I am using it :
export const ArticleQuantity = () => {
const [quantity, setQuantity] = useState(1);
const prevQuantity = usePrevious(quantity);
useEffect(() => {
console.log(prevQuantity + quantity);
}, [quantity]);
<div>
<input
onChange={(e) => setQuantity(e.target.value)}
defaultValue={quantity}/>
<div>
}
Problem : if my user enter "3" in the input and then remove the "3" to enter "5", my previous state will be "null" because the last value is removed.
How can i keep "3" instead of null ?
Thank you.
Add a condition to check null
useEffect(() => {
if(value){
ref.current = value;
}
}, [value]);
i've been solving this problem without any progress for the pas 2 hours or so, here is code:
export const useFetchAll = () => {
const [searchResult, setSearchResult] = useState([]);
const [loading, setLoading] = useState(false);
const [searchItem, setSearchItem] = useState("");
const [listToDisplay, setListToDisplay] = useState([]);
// const debouncedSearch = useDebounce(searchItem, 300);
const handleChange = (e) => {
setSearchItem(e.target.value);
if (searchItem === "") {
setListToDisplay([]);
} else {
setListToDisplay(
searchResult.filter((item) => {
return item.name.toLowerCase().includes(searchItem.toLowerCase());
})
);
}
console.log(searchItem);
};
useEffect(() => {
const searchRepo = async () => {
setLoading(true);
const { data } = await axios.get("https://api.github.com/repositories");
setSearchResult(data);
setLoading(false);
};
if (searchItem) searchRepo();
}, [searchItem]);
the problem is that when i enter characters in input and set state to event.target.value it doesn't pick up last character. here is an image:
enter image description here
BTW this is a custom hook, i return the onchange function here:
const HomePage = () => {
const { searchResult, loading, searchItem, handleChange, listToDisplay } =
useFetchAll();
and then pass it as a prop to a component like so:
<Stack spacing={2}>
<Search searchItem={searchItem} handleChange={handleChange} />
</Stack>
</Container>
any help? thanks in advance.
You are handling the searchItem and searchResult state variables as if their state change was synchronous (via setSearchItem and setSearchResult) but it isn't! React state setters are asynchronous.
The useEffect callback has a dependency on the searchItem state variable. Now every time the user types something, the state will change, that change will trigger a re-rendering of the Component and after that render finishes, the side-effect (the useEffect callback) will be executed due to the Components' lifecycle.
In our case, we don't want to initiate the fetch request on the next render, but right at the moment that the user enters something on the search input field, that is when the handleChange gets triggered.
In order to make the code work as expected, we need some a more structural refactoring.
You can get rid of the useEffect and handle the flow through the handleChange method:
export const useFetchAll = () => {
const [ loading, setLoading ] = useState( false );
const [ searchItem, setSearchItem ] = useState( "" );
const [ listToDisplay, setListToDisplay ] = useState( [] );
const handleChange = async ( e ) => {
const { value } = e.target;
// Return early if the input is an empty string:
setSearchItem( value );
if ( value === "" ) {
return setListToDisplay( [] );
}
setLoading( true );
const { data } = await axios.get( "https://api.github.com/repositories" );
setLoading( false );
const valueLowercase = value.toLowerCase(); // Tiny optimization so that we don't run the toLowerCase operation on each iteration of the filter process below
setListToDisplay(
data.filter(({ name }) => name.toLowerCase().includes(valueLowercase))
);
};
return {
searchItem,
handleChange,
loading,
listToDisplay,
};
};
function used for updating state value is asynchronous that why your state variable is showing previous value and not the updated value.
I have made some change you can try running the below code .
const [searchResult, setSearchResult] = useState([]);
const [loading, setLoading] = useState(false);
const [searchItem, setSearchItem] = useState("");
const [listToDisplay, setListToDisplay] = useState([]);
// const debouncedSearch = useDebounce(searchItem, 300);
const handleChange = (e) => {
setSearchItem(e.target.value); // this sets value asyncronously
console.log("e.target.value :" + e.target.value); // event.target.value does not omitting last character
console.log("searchItem :" + searchItem); // if we check the value then it is not set. it will update asyncronously
};
const setList = async () => {
if (searchItem === "") {
setListToDisplay([]);
} else {
setListToDisplay(
searchResult.filter((item) => {
return item.name.toLowerCase().includes(searchItem.toLowerCase());
})
);
}
};
const searchRepo = async () => {
const { data } = await axios.get("https://api.github.com/repositories");
setSearchResult(data);
setLoading(false);
};
// this useeffect execute its call back when searchItem changes a
useEffect(() => {
setList(); // called here to use previous value stored in 'searchResult' and display something ( uncomment it if you want to display only updated value )
if (searchItem) searchRepo();
}, [searchItem]);
// this useeffect execute when axios set fetched data in 'searchResult'
useEffect(() => {
setList();
}, [searchResult]);
// this useeffect execute when data is updated in 'listToDisplay'
useEffect(() => {
console.log("filtered Data") // final 'listToDisplay' will be availble here
console.log(listToDisplay)
}, [listToDisplay]);
Wondering what the best method is to handle callbacks that get passed to children/into custom hooks that are used inside useEffect blocks (or any hook with dependency arrays)
This is going by the assumption we don't have access to that callback to wrap in a useCallback or define it outside of the parent/changing scope ourselves.
Is there better ways than suggested below? Do I need to worry about stale fns/closures?
function Parent() {
const [value, setValue] = useState(initialValue);
const onChange = (value) => {
setValue(value);
}
return (
<Child onChange={onChange} />
)
}
function Child({ onChange }) {
useEffect(() => {
// ...
onChange(changingValue);
}, [changingValue, onChange]); // <- always changing
}
// Is there a drawback to this approach?
function Child({ onChange }) {
const callbackRef = useRef();
callbackRef.current = onChange;
useEffect(() => {
// ...
callbackRef.current(changingValue);
}, [changingValue])
}
// or should it be updated in useLayoutEffect?
function Child({ onChange }) {
const callbackRef = useRef();
useLayoutEffect(() => {
callbackRef.current = onChange;
});
useEffect(() => {
// ...
callbackRef.current(changingValue);
}, [changingValue]);
}
I think the best is the first one, it's simpler, and if you use a useCallback where you define onChange, then the function will be called just one time every time the value changes, like this:
function Parent() {
const [value, setValue] = useState(initialValue);
const onChange = useCallback((input) => {
setValue(input);
}, []);
return (
<Child onChange={onChange} />
)
}
function Child({ onChange }) {
const [changingValue, setChangingValue] = useState('');
useEffect(() => {
onChange(changingValue);
}, [changingValue, onChange]);
return (
<input type="text" value={changingValue} onChange={(event) => setChangingValue(event.target.value)} />
)
}
By the way, in this particular case, of course you can call the function directly on the onChange, passing the value, like this:
function Parent() {
const [value, setValue] = useState(initialValue);
const onChange = useCallback((input) => {
setValue(input);
}, []);
return (
<Child onChange={onChange} value={value} />
)
}
function Child({ onChange, value }) {
return (
<input type="text" value={value} onChange={(event) => onChange(event.target.value)} />
)
}
onClick2 causes the state to receive the changed value.
However, onClick3 wants to deliver the changed state value only when onClick2 is pressed.
How can I control the changed value of the input?
When onClick3 is pressed when the input value is changed to 456
Is it possible to get the previous value of onChange ?
my code...
import React, { useRef, useState } from "react";
const App = () => {
const [value, setValue] = useState("123");
const onChangeInput = (e) => {
setValue(e.target.value);
};
const onClick2 = () => {
console.log(value);
};
const onClick3 = () => {
// how to onclick2 preve
console.log(value);
};
return (
<div>
<input type="text" onChange={onChangeInput} value={value} />
<button onClick={onClick2}>Button1</button>
<button onClick={onClick3}>Button2</button>
</div>
);
};
export default App;
You should use 2 useState one for prevValue and one for currentValue like this-
const [value, setValue] = useState("123")
const [prevValue, setPrevValue] = useState(value)
const onChangeInput = (e) => {
setValue(e.target.value);
}
const onClick2 = () => {
setPrevValue(value)
}
const onClick3 = () => {
console.log('Previous : ', prevValue)
console.log('Current : ', value)
}
When you click on onClick2 btn it will update previous Value and when you click on onClick3 btn it will give you output as previous and updated Values.
# If your current value is 456
output:
Previous : 123
Current : 456
Just add this code and you will get your answer.
I'm trying to avoid using lodash in my project, and I've eliminated everything except this one debounce function. According to this You-Dont-Need, this debounce should work just fine. But, for some reason, it's not being called.
Here's what I have,
import * as React from "react";
export default function DebouncedSearch() {
const [value, setValue] = React.useState("");
const debounce = (
func: (...params: any[]) => any,
wait: number,
immediate = false
) => {
let timeout;
return function () {
const later = () => {
timeout = null;
if (!immediate) func();
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func();
};
};
const handleChange = React.useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
debounce(() => {
setValue(value);
}, 500);
},
[]
);
return <input value={value} onChange={handleChange} />;
}
What am I doing wrong here?
Edit
If I do this,
const handleChange = React.useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
debounce(() => {
console.log("called");
setValue(value);
}, 500)();
},
[]
);
It prints called as many times as the onChange event fires, not once.
One way of doing this and avoiding render state issues, is to use a useRef.
Below I've just used a setInterval, instead of debounce logic, I prefer that to debounce..
But if you prefer debounce, it should be trivial to alter to do it using that instead.
const {useState, useEffect, useRef} = React;
function MyInput() {
const [value, setValue] = useState('');
const v = useRef({value: '', changed:false});
useEffect(() => {
const i = setInterval(() => {
if (v.current.changed) {
console.log('change: ' + v.current.value);
v.current.changed = false;
}
}, 500);
return () => clearInterval(i);
}, []);
return <input
onChange={(e) => {
setValue(e.target.value);
v.current.changed = true;
v.current.value = e.target.value;
}}
value={value}
/>;
}
ReactDOM.render(<MyInput/>, document.querySelector('#mount'));
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="mount"></div>