How to wait for state to not be undefiened in ReactJS? - javascript

With this code on the component mount, there is a bug where priceToDisplay is first set as undefined, and it won't change state until some part of the component is not rerendered. If I remove that const from the dependencie array in useEffect, the correct value will be set.
const [isOnDiscount, setIsOnDiscount] = useState(
parseInt(product_data.discount_price && product_data.discount_price) !== 0
);
const [priceToDisplay, setPriceToDisplay] = useState(
isOnDiscount ? product_data.discount_price : product_data.price_of_product
);
useEffect(() => {
const isOnDiscountEffect = parseInt(product_data.discount_price) !== 0;
const priceToDisplayEffect = 0;
console.log(priceToDisplay);
if (isOnDiscountEffect) {
setPriceToDisplay(product_data.discount_price);
} else if (doesVariationAffectPrice()) {
// price of variation
} else {
setPriceToDisplay(product_data.price_of_product);
}
}, [priceToDisplay]);

Difficult to tell without knowing the whole component, but I assume that you are trying to do something like this:
export const Simple = ({product_data}) => {
const [priceToDisplay, setPriceToDisplay] = useState(null);
const isOnDiscount = parseInt(product_data.discount_price) !== 0;
useEffect(() => {
if (isOnDiscount) {
setPriceToDisplay(product_data.discount_price);
} else if (doesVariationAffectPrice()) {
// price of variation
} else {
setPriceToDisplay(product_data.price_of_product);
}
}, [isOnDiscount]);
return <div>
{priceToDisplay}
</div>
};
you dont need a state variable for isOnDiscountfor that.

Related

How to get state from custom hooks to update in "parent" component?

I am trying to separate some logic from my component into a custom hook. I feel like i'm misunderstanding some fundamentals but I thought my code would work. I basically update my state in my custom useTrip hook, and i want my map component to have that same updated state.
useTrip.js:
export const useTrip = () => {
const [businesses, setBusinesses] = useState([])
useEffect(()=>{
console.log(businesses) //prints expected results
},[businesses])
const fetchData = async (name, lat, lng) => {
const response = await fetch('http://localhost:5000/category/' + lat + "/" + lng + '/' + name)
const result = await response.json();
setBusinesses(result)
}
return { businesses, fetchData }
}
Map.js (component that uses useTrip):
export const Map= (props) => {
const {businesses} = useTrip()
return(<>
{businesses.map((.....)}
</>)
}
Parent.js (parent of map.js):
export const Parent= (props) => {
const {fetchData} = useTrip()
useEffect(() => {
fetchData(title, lat, lng)
}, [origin])
return(<>
</>)
}
The businesses is always an empty array when inside the Map component. my code was working before i started refactoring. Isnt the updated state in the custom hook suppose to be consistent across the components that use it?
You must use your custom hook on Parent component, and send the businesses to your Map component via props.
i.e.
function Parent (props) {
const { fetchData, businesses } = useTrip()
useEffect(() => {
fetchData(title, lat, lng)
}, [origin])
return (
<Map businesses={businesses} />
)
}
function Map (props) {
const { businesses } = props
return (
<>
{businesses.map(/* ... */)}
</>
)
}
If you call your custom hook on each component, they will get their own state
I have played around with this a bit, and come up with a better, solution. It is in the first code block.
import {useEffect, useState} from 'react';
import { v4 as uuidv4 } from 'uuid';
const constant_data = {
altering_var: null,
queue: {},
default_set: false
};
export const useConstantVariable = (defaultUser) => {
//set an id to a unique value so this component can be identified
const [id, setId] = useState(uuidv4());
//use this variable to force updates to screen
const [updateId, setUpdateId] = useState({});
//set the data contained in this hook
const setData = (data) => {
constant_data.altering_var = data;
};
//force an update of screen
const updateScreen = () => {
setUpdateId({...updateId});
};
//make a copy of the data so it is seen as a new constant instance
const saveData = () =>{
//if the value is an array copy the array
if(Array.isArray(constant_data.altering_var)){
constant_data.altering_var = [...constant_data.altering_var];
//if the value is an object copy it with its prototype
} else if(typeof constant_data.altering_var === 'object' && constant_data.altering_var !== null){
constant_data.altering_var = completeAssign({}, constant_data.altering_var);
} else {
//do no operation on basic types
}
}
//update all instances of this hook application wide
const updateAll = () => {
saveData();
//now get all instances and update them, remove broken links.
Object.keys(constant_data.queue).map((k)=> {
const value = constant_data.queue[k];
if (typeof value !== 'undefined' && value !== null) {
constant_data.queue[k]();
} else {
delete constant_data.queue[k]
}
return true;
});
};
//set the function to call to update this component
constant_data.queue[id] = updateScreen;
//for the first instance of this hook called set the default value.
if (typeof defaultUser !== 'undefined' && !constant_data.default_set) {
constant_data.default_set = true;
setData(defaultUser);
}
//when this component is destroyed remove all references to it in the queue used for updating.
useEffect(() => {
return () => {
delete constant_data.queue[id];
};
}, []);
//return the new variable to the constant
return [
constant_data.altering_var,
(data) => {
setData(data);
updateAll();
}
];
};
function completeAssign(target, source) {
target = Object.assign(target, source);
Object.setPrototypeOf(target, Object.getPrototypeOf(source));
return target;
}
OLD ANSWER
This is how we managed to solve this issue, it is not perfect, and I am open to suggestions for improvements. But we created a user component to share our user across the entire app.
const users = {client: {isSet: () => { return false; } } }
const instances = {client: []}
export const useClientUser = (defaultUser) => {
const [updateId, setUpdateId] = useState(uuidv4());
const setClientUser = (data) => {
users.client = new Person(data);
}
const updateScreen = () => {
setUpdateId(uuidv4());
}
useEffect(()=>{
if(defaultUser !== '' && typeof defaultUser !== 'undefined'){
setClientUser(defaultUser);
}
instances.client.push(updateScreen);
}, []);
return [users.client , (data) => { setClientUser(data);
instances.client = instances.client.filter((value)=> {
if(typeof value !== 'undefined'){ return true } else { return false }
} );
instances.client.map((value)=> {if(typeof value !== 'undefined') { value() } })
} ];
}
I have rewritten our component to show how yours would hypothetically work.
import { v4 as uuidv4 } from 'uuid';
//create super globals to share across all components
const global_hooks = {businesses: {isSet: false } }
const instances = {businesses: []}
export const useTrip = () => {
//use a unique id to set state change of object
const [updateId, setUpdateId] = useState(uuidv4());
//use this function to update the state causing a rerender
const updateScreen = () => {
setUpdateId(uuidv4());
}
//when this component is created add our update function to the update array
useEffect(()=>{
instances.businesses.push(updateScreen);
}, []);
useEffect(()=>{
console.log(global_hooks.businesses) //prints expected results
},[updateId]);
const fetchData = async (name, lat, lng) => {
const response = await fetch('http://localhost:5000/category/' + lat + "/" + lng + '/' + name)
const result = await response.json();
global_hooks.businesses = result;
global_hooks.businesses.isSet = true;
}
return {businesses: global_hooks.businesses, fetchData: (name, lat, lng) => {
//fetch your data
fetchData(name, lat, lng);
//remove update functions that no longer exist
instances.businesses = instances.business.filter((value)=> {
if(typeof value !== 'undefined'){ return true } else { return false }
} );
//call update functions that exist
instances.businesses.map((value)=> {if(typeof value !== 'undefined') { value() } })
}
};
}

change useState when redux state change

I have a functional component with a useState hook. Its values are coming from my redux store and I want to update the state with the are store state every time a dispatch an action.
Right now I have hardcoded an array that the useState starts with. I want to be able to push in new elements in the array via redux and have react re-render the new content.
See code below:
import React, { useState } from "react";
import "./style.scss";
import { FormEquation } from "../calc/interfaces/form";
import { FlowrateCalc } from "../calc/calculators/FlowrateCalc";
import { useSelector } from "react-redux";
import { RootState } from "../state/reducers";
import { ValveKvsCalc } from "../calc/calculators/ValveKvsCalc";
function Calculator() {
const state = useSelector((state: RootState) => state.calc);
// const state = [
// {
// ...FlowrateCalc,
// priorityList: FlowrateCalc.inputs.map((input) => input.name),
// },
// {
// ...ValveKvsCalc,
// priorityList: ValveKvsCalc.inputs.map((input) => input.name),
// },
// ];
// Usestate is run once after render and never again. How do I update this state whenever new content arrived from "useSelector"??
const [formsEQ, setformsEQ] = useState<FormEquation[]>([...state]);
const inputsHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
// Copy form and get index of affected form
const formCopy = formsEQ.slice();
const [formName, inputFieldName] = e.target.name.split("-");
const formIndex = formsEQ.findIndex((formEQ) => formEQ.name === formName);
if (formIndex === -1) return;
// if anything other than a number or dot inputted, then return
// meTODO: if added number then trying to delete all numbers will stop!
const isInputNum = e.target.value.match(/[0-9]*\.?[0-9]*/);
if (!isInputNum || isInputNum[0] === "") return;
// Update priority list to calculate the last updated input
formCopy[formIndex].priorityList = formCopy[formIndex].priorityList.sort((a, b) => {
if (a === inputFieldName) return 1;
if (b === inputFieldName) return -1;
else return 0;
});
// Update selected input field
formCopy[formIndex].inputs = formCopy[formIndex].inputs.map((input) => {
if (input.name === inputFieldName) {
input.value = e.target.value;
}
return input;
});
// If more than two inputs empty do not calculate
const emptyInputs = formCopy[formIndex].inputs.reduce(
(acc, nV) => (nV.value === "" ? (acc += 1) : acc),
0
);
// Calculate the last edited input field
formCopy[formIndex].inputs = formCopy[formIndex].inputs.map((input) => {
if (input.name === formCopy[formIndex].priorityList[0] && emptyInputs <= 1) {
const calculatedValue = formCopy[formIndex].calculate(formCopy[formIndex].priorityList[0]);
input.value = calculatedValue;
}
return input;
});
// Final set hook, now with calculated value
setformsEQ([...formCopy]);
};
const formInputs = formsEQ.map((formEQ) => {
return (
<form className="form" key={formEQ.name}>
{formEQ.inputs?.map((formInput) => {
return (
<div className="form__input" key={formInput.name}>
<label>{formInput.label}: </label>
<input
name={`${formEQ.name}-${formInput.name}`}
onChange={inputsHandler}
placeholder={`${formInput.label} (${formInput.selectedUnit})`}
value={formInput.value}
/>
</div>
);
})}
</form>
);
});
return <div>{formInputs}</div>;
}
export default Calculator;
To whomever is reading this and is a rookie in react like me.
The solution for me was to use useEffect hook; And whenever useSelector updates the state constant, the useEffect hook will use the useState set function to update the state.
See added code below that fixed my problem:
useEffect(() => {
setformsEQ([...state])
}, [state])

Rendered more hooks than during the previous render React issue

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.

How to stop restarting for loop when dependency change in useEffect (React js)

I am trying to match pi value from input.
"pracInput" dependency has been used in useEffect so I can get latest value from input and check.
But the problem is that when I input some value the for loop restart.
if I input 9;
expected value=14159; counting: 5; progress width : 60 ;
if I input another value 2;
expected => value=141592; counting: 6; progress width : 72;
import React, { useEffect, useState } from "react";
const PiGame = () => {
const [pracInput, setPracInput] = useState("1415");
const pi = "141592653589793238462643";
const [widthText, setWidthText] = useState(0);
const [counting, setCounting] = useState(0);
useEffect(() => {
const runLoop2 = async () => {
for (let i = 0; i < pracInput.length; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
if (pracInput[i] === pi[i]) {
console.log(i)
console.log(true);
setWidthText((prev) => prev + 12);
setCounting((prev) => prev + 1);
} else {
console.log(false);
alert('not match')
}
}
};
runLoop2();
}, [pracInput]);
const handleChange = (e) => {
const val = e.target.value;
if (/^[0-9]+$/.test(val)) {
setPracInput(val);
}
};
return (
<div>
<div>
value: <input
type="text"
style={{
width: "80%",
marginTop: 100,
height: 25,
fontSize:25
}}
value={pracInput}
onChange={handleChange}
/>
<div style={{ fontSize: 25 }}>counting : {counting}</div>
<div style={{ backgroundColor: "green", width: widthText, height: 20 }}></div>
<div>progress width : {widthText}</div>
</div>
</div>
);
};
export default PiGame;
According to what I understood from question:
import React, { useEffect, useState } from "react";
const PiGame = () => {
const [pracInput, setPracInput] = useState("1415");
const pi = "141592653589793238462643";
useEffect(() => {
let subscribed = true;
const runLoop2 = async () => {
for (let i = 0; i < pracInput.length; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
if (subscribed) {
if (pracInput[i] === pi[i]) {
console.log(i)
console.log(true);
} else {
console.log(false);
}
}
}
};
runLoop2();
return () => subscribed = false; // to avoid memory leak on unmount of component
}, []);. // Passing empty array will make it execute only once, at component mount
const handleChange = (e) => {
const val = e.target.value;
if (/^[0-9]+$/.test(val)) {
setPracInput(val);
}
};
return (
<div>
<div>
<input
type="text"
style={{
width: "80%",
}}
value={pracInput}
onChange={handleChange}
/>
<div>{pracInput}</div>
</div>
</div>
);
};
export default PiGame;
As you are doing asynchronous task in useEffect you must use some logic/trick to avoid memory leak.
Because you added "pracInput" in the array. "UseEffect" excutes everytime you call it and therefore will always call the looping function to start over. To stop this, you could either remove the "pracInput" from the dependency array and get the value in another way or you could use the "UseState" hook to set a certain value when the loop starts and then base a condition on your loop function call.
Something like this
const [cond, setCond] = useState(false);
Set its value to be true when the loop starts then add a condition like this
if(cond == false)
runLoop2();
Are you looking for this?
import React, { useEffect, useState, useRef } from "react";
const PiGame = () => {
const [pracInput, setPracInput] = useState("1415");
const pi = "141592653589793238462643";
const counter = useRef(4); // store the counter
useEffect(() => {
const runLoop2 = async () => {
for (let i = counter.current; i < pracInput.length; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
if (pracInput[i] === pi[i]) {
console.log(i)
console.log(true);
} else {
console.log(false);
}
}
counter.current = counter.current + pracInput.length;
};
runLoop2();
}, [pracInput]);
// the rest same as before
See your for loop is present inside the useEffect which gets triggered whenever your pracInput gets changed,so everything inside that useEffect will be triggered.Your pracInput is being changed when handleChange is being called ,so that means whenevr some input will be provided and if (/^[0-9]+$/.test(val)) { setPracInput(val); } becomes true,your pracInput will change and useEffect will be triggered and hence that for loop will start.And since your for loop requires your changes of pracInput ,moving it outside of the useEffect also wont make sense.
If you want to just start your for loop once only then remove it from that useEffect and do it like this:
useEffect(()=>{
for loop goes here
},[]);
This will ensure that for loop runs just once ,that is when the component will render for the first time.
Edit: According to your output i think this should work:
const [widthText, setWidthText] = useState(0);
const [counting, setCounting] = useState(0);
useEffect(() => {
const TimeoutId = setTimeout(() => {
if(pracInput==="1415")
{
setCounting(4);
setWidthText(60);
}
else
{
setCounting(pracInput.length+1);
setWidthText(60+(pracInput.length-4)*12);
}
}, 1000);
return () => clearTimeout(TimeoutId);
}, [pracInput]);
const handleChange = (e) => {
const val = e.target.value;
if (/^[0-9]+$/.test(val)) {
setPracInput(val);
}
};
And you can now remove the for loop

The component isn't updating when I pass in a filtered variable on a timer

So I was trying to implement a filter that is controlled by a search bar input. So I think part of the problem is that I have this filter hooked on a timer so that while the user is typing into the search bar, it isn't re-running for each letter typed in.
What it is currently doing is that after the item is typed in the search bar, the timer goes off and the filters are working but it doesn't appear that the app is re-rendering with the new filtered variable.
I suspect that it might have something to do with useEffect but I'm having trouble wrapping my head around it and it wasn't working out for whatever I was doing with it.
Here's the code:
const RecipeCards = (props) => {
const inputTypingRef = useRef(null);
let preparingElement = props.localRecipes;
let cardElement;
let elementsSorted;
const ingredientCountSort = (recipes) => {
elementsSorted = ...
}
const elementRender = (element) => {
cardElement = element.map((rec) => (
<RecipeCard
name={rec.name}
key={rec.id}
ingredients={rec.ingredients}
tags={rec.tags}
removeRecipe={() => props.onRemoveIngredients(rec.id)}
checkAvail={props.localIngredients}
/>
));
ingredientCountSort(cardElement);
};
if (inputTypingRef.current !== null) {
clearTimeout(inputTypingRef.current);
}
if (props.searchInput) {
inputTypingRef.current = setTimeout(() => {
inputTypingRef.current = null;
if (props.searchOption !== "all") {
preparingElement = props.localRecipes.filter((rec) => {
return rec[props.searchOption].includes(props.searchInput);
});
} else {
preparingElement = props.localRecipes.filter((rec) => {
return rec.includes(props.searchInput);
});
}
}, 600);
}
elementRender(preparingElement);
return (
<div className={classes.RecipeCards}>{!elementsSorted ? <BeginPrompt /> : elementsSorted}</div>
);
};
Don't worry about ingredientCountSort() function. It's a working function that just rearranges the array of JSX code.
Following up to my comment in original question. elementsSorted is changed, but it doesn't trigger a re-render because there isn't a state update.
instead of
let elementsSorted
and
elementsSorted = ...
try useState
import React, { useState } from 'react'
const RecipeCards = (props) => {
....
const [ elementsSorted, setElementsSorted ] = useState();
const ingredientCountSort = () => {
...
setElementsSorted(...whatever values elementsSorted supposed to be here)
}
Reference: https://reactjs.org/docs/hooks-state.html
I used useEffect() and an additional useRef() while restructuring the order of functions
const RecipeCards = (props) => {
//const inputTypingRef = useRef(null);
let preparingElement = props.localRecipes;
let finalElement;
const [enteredFilter, setEnteredFilter] = useState(props.searchInput);
let elementsSorted;
const [elementsFiltered, setElementsFiltered] = useState();
const refTimer = useRef();
const filterActive = useRef(false);
let cardElement;
useEffect(() => {
setEnteredFilter(props.searchInput);
console.log("updating filter");
}, [props.searchInput]);
const filterRecipes = (recipes) => {
if (enteredFilter && !filterActive.current) {
console.log("begin filtering");
if (refTimer.current !== null) {
clearTimeout(refTimer.current);
}
refTimer.current = setTimeout(() => {
refTimer.current = null;
if (props.searchOption !== "all") {
setElementsFiltered(recipes.filter((rec) => {
return rec.props[props.searchOption].includes(enteredFilter);
}))
} else {
setElementsFiltered(recipes.filter((rec) => {
return rec.props.includes(enteredFilter);
}))
}
filterActive.current = true;
console.log(elementsFiltered);
}, 600);
}else if(!enteredFilter && filterActive.current){
filterActive.current = false;
setElementsFiltered();
}
finalElement = elementsFiltered ? elementsFiltered : recipes;
};
const ingredientCountSort = (recipes) => {
console.log("sorting elements");
elementsSorted = recipes.sort((a, b) => {
...
filterRecipes(elementsSorted);
};
const elementRender = (element) => {
console.log("building JSX");
cardElement = element.map((rec) => (
<RecipeCard
name={rec.name}
key={rec.id}
ingredients={rec.ingredients}
tags={rec.tags}
removeRecipe={() => props.onRemoveIngredients(rec.id)}
checkAvail={props.localIngredients}
/>
));
ingredientCountSort(cardElement);
};
//begin render /////////////////// /// /// /// /// ///
elementRender(preparingElement);
console.log(finalElement);
return (
<div className={classes.RecipeCards}>{!finalElement[0] ? <BeginPrompt /> : finalElement}</div>
);
};
There might be redundant un-optimized code I want to remove on a brush-over in the future, but it works without continuous re-renders.

Categories