I have a <Day/> component which renders a table full of times through the day.
Right now I have a handleChange function in the <TableLayout/> which is listening for changes in the text-area.
function TableLayout({ sendEventTargetToParent }) {
var [amountOfRows, setAmountOfRows] = useState(24);
var [textValue, setTextValue] = useState('');
var [eventName, setEventName] = useState('');
useEffect(() => {
sendDataToParent();
}, [eventName, textValue]);
function sendDataToParent() {
sendEventTargetToParent(eventName, textValue);
}
function handleChange(event) {
var { name, value } = event.target;
setEventName(name);
setTextValue(value);
}
The markup:
<TextArea
rows={2}
name="textarea"
value={textValue}
onChange={(e) => handleChange(e)}
placeholder="Tell us more"
/>
I'd like to send that info from the text-area to the component, you probably noticed already noticed the prop TableLayout is consuming:
function TableLayout({ sendEventTargetToParent }) {
Which I thought belongs in useEffect because it's creating a side-effect:
useEffect(() => {
sendDataToParent();
}, [eventName, textValue]);
function sendDataToParent() {
sendEventTargetToParent(eventName, textValue);
}
Anyway the idea is when those local variables/state change in TableLayout it gets kicked up to Day...
export default function Day({ dayInfo }) {
var [dayInfoInChild, setDayInfoInChild] = useState({});
var [currentDate, setCurrentDate] = useState('');
var [timeOfDay, setTimeOfDay] = useState('');
var [eventNameFromChild, setEventNameFromChild] = useState('');
var [textValueFromChild, setTextValueFromChild] = useState('');
function getEventTargetFromChild(eventName, textValue) {
setEventNameFromChild(eventName);
setTextValueFromChild(textValue);
}
useEffect(() => {
if (dayInfo !== null) {
var modifiedDayInfo = dayInfo
.split(' ')
.map((item) => {
if (item.indexOf(',')) return item.replace(/,/g, '');
})
.join('-');
setCurrentDate(modifiedDayInfo);
if (localStorage.getItem(modifiedDayInfo)) {
var stringVersionOfModifiedDayInfo = modifiedDayInfo;
modifiedDayInfo = JSON.parse(localStorage.getItem(modifiedDayInfo));
if (!dayInfoInChild.hasOwnProperty(stringVersionOfModifiedDayInfo)) {
setDayInfoInChild({
...dayInfoInChild,
[stringVersionOfModifiedDayInfo]: modifiedDayInfo,
});
}
} else {
localStorage.setItem(modifiedDayInfo, JSON.stringify({}));
}
if (dayInfoInChild.hasOwnProperty(currentDate)) {
setDayInfoInChild({
...dayInfoInChild,
[currentDate]: {
[eventNameFromChild]: textValueFromChild,
},
});
}
}
}, []);
And the code in the Day'suseEffectessentially creates an JSON object in localstorage with its key based on the date if needed if not it pulls that key and object converts it to a JS object and puts it into the state usinguseState`.
var [eventNameFromChild, setEventNameFromChild] = useState('');
var [textValueFromChild, setTextValueFromChild] = useState('');
function getEventTargetFromChild(eventName, textValue) {
setEventNameFromChild(eventName);
setTextValueFromChild(textValue);
}
this is the part where the state from the Child gets set in Day.
if (dayInfoInChild.hasOwnProperty(currentDate)) {
setDayInfoInChild({
...dayInfoInChild,
[currentDate]: {
[eventNameFromChild]: textValueFromChild,
},
});
}
But right now I able to add one key stroke into the object and then the UI locks:
So how can I create a cohesive flow from my Child to the Parent using hooks?
By understanding your problem i suggest you to use useContext
Here is a link which can help you to solve this problem.
https://vimalselvam.com/post/react-hooks-lift-up-pass-down-state-using-usecontext-and-usereducer/
Related
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]);
So I have built app which takes value from input -> set it to the state-> state change triggers functions in useEffect (this part is in custom hook) -> functions fetch data from api -> which triggers functions in useEffect in component to store data in array. The thing is that there are two problems that I am trying to solve :
When user is putting the same value in input and setting it in state it's not triggering useEffect functions (I solved it by wrapping value in object but I am looking for better solution).
When user uses the same value in short period of time api will send the same data which again makes problem with triggering function with useEffect (I tried to solved with refresh state that you will see in code below, but it looks awful)
The question is how can I actually do it properly? Or maybe the solutions I found aren't as bad as I think they are. Thanks for your help.
component
const [nextLink, setNextLink] = useState({ value: "" });
const isMounted = useRef(false);
const inputRef = useRef(null);
const { shortLink, loading, error, refresh } = useFetchLink(nextLink);
const handleClick = () => {
setNextLink({ value: inputRef.current.value });
};
useEffect(() => {
setLinkArr((prev) => [
...prev,
{
id: prev.length === 0 ? 1 : prev[prev.length - 1].id + 1,
long: nextLink.value,
short: shortLink,
},
]);
if (isMounted.current) {
scrollToLink();
} else {
isMounted.current = true;
}
inputRef.current.value = "";
}, [refresh]);
custom hook
const useFetchLink = (linkToShorten) => {
const [shortLink, setShortLink] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [refresh, setRefresh] = useState(false);
const isMounted = useRef(false);
const fetchLink = async (link) => {
setLoading(true);
try {
const response = await fetch(
`https://api.shrtco.de/v2/shorten?url=${link}`
);
if (response.ok) {
const data = await response.json();
setShortLink(data.result.short_link);
setRefresh((prev) => !prev);
} else {
throw response.status;
}
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (isMounted.current) {
if (checkLink(linkToShorten.value)) {
setError(checkLink(linkToShorten.value));
} else {
fetchLink(linkToShorten.value);
}
} else {
isMounted.current = true;
}
}, [linkToShorten]);
const value = { shortLink, loading, error, refresh };
return value;
};
export default useFetchLink;
Pretty new to React Hooks and I ran into this issue. I have a functional component that takes an input and sends it to the parent component when I hit the enter key (keycode = 13). The component looks something like this.
const SearchTermBar = (props) => {
const {resetStateSearchTerm, handlePowerToggleParent} = props;
const [inputTerm, handleInputChange] = useState('');
const inputRef = useRef();
useEffect(() => {
const keyPressEvent = (e) => {
if (e.keyCode === 13) {
resetStateSearchTerm(inputTerm);
handleInputChange('');
handlePowerToggleParent('search');
}
};
inputRef.current.addEventListener('keydown', keyPressEvent);
let parentInputRef = inputRef;
return () => {
console.log('remove event listener');
parentInputRef.current.removeEventListener('keydown', keyPressEvent);
}
}, [inputTerm, resetStateSearchTerm, handlePowerToggleParent]);
return (
<div className='SearchTermBar'>
<input
type='text'
placeholder='Enter search term here (Press return to confirm)'
className='SearchTermBar__Input'
value={inputTerm}
onChange={(e) => handleInputChange(e.target.value)}
ref={inputRef}
/>
</div>
);
The problem is that the event is registered and unregistered every time the inputTerm or props value changes. But I am not able to figure out the correct way to handle the event registration/removal (which should happen once ideally) I understand it is because of the dependency on the inputTerm but I would like to know a better solution to this problem.
You already has the input ref, you don't really need a state:
const NOP = () => {};
const DEFAULT_INPUT = "";
function SearchTermBar(props) {
const { resetStateSearchTerm = NOP, handlePowerToggleParent = NOP } = props;
const inputRef = useRef();
useEffect(() => {
const keyPressEvent = (e) => {
if (e.keyCode === 13) {
resetStateSearchTerm(inputRef.current.value);
inputRef.current.value = DEFAULT_INPUT;
handlePowerToggleParent("search");
}
};
inputRef.current.addEventListener("keydown", keyPressEvent);
let parentInputRef = inputRef;
return () => {
console.log("remove event listener");
parentInputRef.current.removeEventListener("keydown", keyPressEvent);
};
}, [resetStateSearchTerm, handlePowerToggleParent]);
return (
<input
type="text"
placeholder="Enter search term here (Press return to confirm)"
style={{ width: "50%" }}
ref={inputRef}
/>
);
}
Either way, if you want to keep the state, its value should be duplicated into a ref to fix the closure in the useEffect. It can be done by adding another useEffect which will update the mentioned ref.
I've re edited the question as it was not relevant... I got an issue in appearing in my browser when I launch my app, this issue is:
Rendered more hooks than during the previous render.
I've look all over the internet, but still don't manage to make it work.
Here is my code:
const DefaultValue = () => {
let matchingOption = options.find((option) => option.value.includes(countryLabel))
let optionSelected = options.find((option) => option.value === value)
const hasCountryLabelChanged = countryHasChanged(countryLabel)
const [selectedPathway, changeSelectedPathway] = useState(matchingOption)
useEffect(() => {
if (hasCountryLabelChanged) {
if(matchingOption) {
changeSelectedPathway(matchingOption)
} else {
changeSelectedPathway(options[0])
}
} else {
changeSelectedPathway(optionSelected)
}
},[matchingOption, optionSelected, selectedPathway, hasCountryLabelChanged])
if(selectedPathway !== undefined) {
const newLevers = levers.map((lever, index) => {
lever.value = +pathways[selectedPathway.value][index].toFixed(1) * 10
return lever
})
dispatch(Actions.updateAllLevers(newLevers))
}
return selectedPathway
}
const countryHasChanged = (countryLabel) => {
const prevValue = UsePrevious(countryLabel)
return prevValue !== countryLabel
}
const UsePrevious = (countryLabel) => {
const ref = useRef()
useEffect(() => {
ref.current = countryLabel
})
return ref.current
}
the "selectedPathway" is shown in < select value={DefaultValue} />
Your optionValueCheck call should happen inside a useEffect with one of the dependency params as countryLabel. So that whenever countryLabel updates, your function is executed.
I am new to React and I am building a budget calculator. I am taking the amount from one input and adding it to another input so I can come up with the balance. I have tried reduce and concat and they are coming up to sum but the value is wrong. I don't know what I'm doing wrong. Can anyone point me in the right direction. I think the problem is that the values are rendering twice and that's throwing off the math. I don't know.
Here is my code:
// this is the component to get the balance
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
balance: []
}
}
getBalance = (total) => {
this.setState((prevState) => ({
balance: [prevState.balance, total].reduce((acc, currentVal) => {
return Number(currentVal) + Number(acc)
}, 0)
}));
}
render() {
return (
<div className="App" >
<div className="count">
<h2 className="balancetitle">Your Balance</h2>
<h1 style={{ color: this.state.balance >= 0 ? 'green' : 'red' }}>${this.state.balance}</h1>
</div>
<Transactions getBalance={(total) => this.getBalance(Number(total))} />
<Income getBalance={(total) => this.getBalance(Number(total))} />
</div>
);
}
}
// this is the code to get the transaction. I have another component that is identical to get the sum of the income.
const Transactions = (props) => {
const [expenses, setExpense] = useState([])
const [amount, setAmount] = useState([])
const [id, setId] = useState([])
const [listOfTrans, setListofTrans] = useState([])
const [total, setTotal] = useState([0])
//fires on click or enter
const handleSubmit = (e) => {
e.preventDefault()
addExpense({
amount,
expenses,
id
});
setAmount('')
setExpense('')
}
//get value of inputs
const getValue = (hookSetter) => (e) => {
let { value } = e.target;
return hookSetter(value)
}
// turn amount and expense into objects and put them setListofTranas
const addExpense = (expenseObject) => {
setListofTrans([...listOfTrans, expenseObject])
}
const show = () => {
if (listOfTrans.legnth > 1) {
return listOfTrans
} else return null
}
// get total amount of listoftrans
const getAmount = () => {
if (listOfTrans.length > 0) {
let listAmount = listOfTrans.map(list => {
if (list.amount) {
return -Math.abs(list.amount);
} else {
return 0;
}
})
return listAmount.reduce((acc, currentValue) => {
return Number(acc) + Number(currentValue)
}, 0)
} else return 0
}
//update amount total on click
useEffect(() => {
setTotal(getAmount())
props.getBalance(getAmount())
}, [listOfTrans])
// delete item from array
const deleteExpense = (i) => {
let objExpense = i
setListofTrans(listOfTrans.filter((list) => {
return list.id !== objExpense
}))
}
I am adding it here as the suggestion is not possible to add long description in comments section.
What you are doing buggy in the the solution above is making use of useEffect to do the calcualtions. The approach can be real buggy and difficult to debug.
//update amount total on click
useEffect(() => {
setTotal(getAmount())
props.getBalance(getAmount())
}, [listOfTrans])
In the code above listOfTans is an array , may be changing due to various operation, which cause the useEffect callback to run repeatedly. The callback is reponsible for updating the sum.
So instead of doing that, you should just call
props.getBalance(getAmount())
in onClick Handler.
This is just the suggestion for what I can understand from the question above.