change useState when redux state change - javascript

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])

Related

React State / DOM Not Updating When All Items Deleted

I'm building an app with React and Firebase Realtime Database. Objects are added to an array and sent to the database.
The arrays are updated in React and the result is sent to the database.
The functionality to remove items/objects from the list works fine when there are more than one (i.e. button clicked, database, DOM and state updated immediately).
However, whenever there's one item left and you click its delete button, it's deleted from the database but the state and React DOM aren't updated - you have to refresh the page for it to be removed.
I've tried using different methods to update the database in case it triggered a different response but that didn't work - any ideas would be greatly appreciated:
import React, {useState, useEffect} from 'react'
import { Button } from "react-bootstrap";
import Exercise from "./Exercise";
import AddNewWorkout from "./AddNewWorkout";
import { v4 as uuidv4 } from "uuid";
import WorkoutComponent from './WorkoutComponent';
import AddNewExercise from "./AddNewExercise"
import { database, set, ref, onValue, update } from "../firebase"
const Dashboard = ({user}) => {
const [selectedWorkout, setSelectedWorkout] = useState();
const [workouts, setWorkouts] = useState([])
const [creatingNewWorkout, setCreatingNewWorkout] = useState(false);
const [addingNewExercise, setAddingNewExercise] = useState(false)
function selectWorkout(number) {
const selection = [...workouts].filter(workout => number == workout.id);
setSelectedWorkout(selection[0])
}
function toggleNewWorkoutStatus(e) {
e.preventDefault()
setCreatingNewWorkout(creatingNewWorkout => !creatingNewWorkout)
}
function toggleNewExerciseStatus() {
setAddingNewExercise(addingNewExercise => !addingNewExercise)
}
function writeData() {
const newWorkouts = [...workouts]
const workoutTitle = document.getElementById("workoutTitle").value || new Date(Date.now()).toString()
const workoutDate = document.getElementById("workoutDate").value;
newWorkouts.push({
id: uuidv4(),
title: workoutTitle,
date: workoutDate,
exercises: []
})
set(ref(database, `${user.uid}/workouts/`), newWorkouts )
}
function addWorkoutToListDB(e) {
e.preventDefault();
writeData(user.uid)
}
function removeWorkoutFromList(id) {
const newWorkouts = [...workouts].filter(workout => id !== workout.id);
update(ref(database, `${user.uid}`), {"workouts": newWorkouts} )
}
function addExerciseToWorkout(e) {
e.preventDefault();
if (selectedWorkout === undefined) {
alert("No workout selected")
return
}
const newWorkouts = [...workouts]
const exerciseID = uuidv4();
const exerciseName = document.getElementById("exerciseName").value
const exerciseSets = document.getElementById("exerciseSets").value
const exerciseReps = document.getElementById("exerciseReps").value
const exerciseWeight = document.getElementById("exerciseWeight").value
const exercisetTarget = document.getElementById("exercisetTarget").checked
const exerciseNotes = document.getElementById("exerciseNotes").value;
const newExercise = {
id: exerciseID,
name: exerciseName,
sets: exerciseSets,
reps: exerciseReps,
weight: `${exerciseWeight}kg`,
target: exercisetTarget,
notes: exerciseNotes,
}
for (let key of newWorkouts) {
if (key.id === selectedWorkout.id) {
if (key.exercises) {
key.exercises.push(newExercise)
} else {
key.exercises = [newExercise]
}
}
}
update(ref(database, `${user.uid}`), {"workouts": newWorkouts} )
}
function removeExerciseFromWorkout(id) {
const newWorkouts = [...workouts];
for (let workout of newWorkouts) {
if(selectedWorkout.id === workout.id) {
if (!workout.exercises) {return}
workout.exercises = workout.exercises.filter(exercise => exercise.id !== id)
}
}
const newSelectedWorkout = {...selectedWorkout}
newSelectedWorkout.exercises = newSelectedWorkout.exercises.filter(exercise => exercise.id !== id)
setSelectedWorkout(newSelectedWorkout)
update(ref(database, `${user.uid}`), {"workouts": newWorkouts} )
}
useEffect(() => {
function getWorkoutData() {
const dbRef = ref(database, `${user.uid}`);
onValue(dbRef, snapshot => {
if (snapshot.val()) {
console.log(snapshot.val().workouts)
setWorkouts(workouts => workouts = snapshot.val().workouts)
}
}
)
}
getWorkoutData()
},[])
return (
<div>
{creatingNewWorkout && <AddNewWorkout addWorkoutToListDB={addWorkoutToListDB} toggleNewWorkoutStatus={toggleNewWorkoutStatus} /> }
<div id="workoutDiv">
<h2>Workouts</h2><p>{selectedWorkout ? selectedWorkout.title : "No workout selected"}</p>
<Button type="button" onClick={toggleNewWorkoutStatus} className="btn btn-primary">Add New Workout</Button>
{workouts && workouts.map(workout => <WorkoutComponent key={workout.id} removeWorkoutFromList={removeWorkoutFromList} selectWorkout={selectWorkout} workout={workout}/> )}
</div>
<div>
<h2>Exercise</h2>
{addingNewExercise && <AddNewExercise selectedWorkout={selectedWorkout} addExerciseToWorkout={addExerciseToWorkout} toggleNewExerciseStatus={toggleNewExerciseStatus}/> }
<Button type="button" onClick={toggleNewExerciseStatus} className="btn btn-primary">Add New Exercise</Button>
{selectedWorkout && selectedWorkout.exercises && selectedWorkout.exercises.map(exercise => <Exercise removeExerciseFromWorkout={removeExerciseFromWorkout} key={exercise.id} exercise={exercise}/>)}
</div>
</div>
)
}
export default Dashboard
If it helps, the data flow I'm working to is:
New array copied from state
New array updated as necessary
New array sent to database
Database listener triggers download of new array
New array saved to state
I have tried to use different methods (set, update and remove) in case that triggered the onValue function.
I have also tried to send null values and deleting empty nodes if the array that will be sent to the db is empty.
The above methods didn't have any impact, there was still a problem with the last array element that was only resolved by refreshing the browser.
I have tried to remove the array dependency and add the workout state as a dependency, resulting in the following error: "Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render."
I think I understand where the issue was:
In the useEffect call, I set up the state to only be updated if the value in returned from the database was null (to prevent an error I ran into). However, this meant that state wasn't being updated at all when I deleted the last item from the array.
I appear to have fixed this by adding an else clause.
useEffect(() => {
function getWorkoutData() {
const dbRef = ref(database, `${user.uid}`);
onValue(dbRef, snapshot => {
if (snapshot.val()) {
console.log(snapshot.val().workouts)
setWorkouts(workouts => workouts = snapshot.val().workouts)
} else {
setWorkouts(workouts => workouts = [])
}
}
)
}
getWorkoutData()
},[])
`````

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() } })
}
};
}

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

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.

I am trying to update the state using the React useState hook?? The Create Read and Delete part has been done but iam not able to update

I am making a To Do App using useState react hook.
I have complete with Create Read and Delete parts but
I have not been able to update the state.
Can somebody please help me.
I have complete the same with Class component.
/****************************** MY app.js file ********************************************/
import React, { useState } from "react";
import "./App.css";
import ToDoList from "./Components/ToDoList";
function App() {
const [change, handleChange] = useState("");
const [items, addItem] = useState([]);
let handleSubmit = (e) => {
e.preventDefault();
// console.log(change)
if (change !== "") {
addItem([...items, { text: change, key: Date.now() }]);
handleChange("");
}
};
let removeTask = (key) => {
let item = items.filter((ele) => {
return ele.key !== key;
});
console.log(item);
addItem([...item]);
};
let updateToDo = (value, key) => { // <<<<<<< I need to make changes in this piece of code.
let allItem = items.map((e) => {
if (e.key === key) {
e.text = value;
}
console.log(...allItem);
// addItem([...items, { allItem }]);
});
};
return (
<div className="toDoContainer">
<form onSubmit={handleSubmit}>
<input
type="text"
onChange={(e) => handleChange(e.target.value)}
value={change}
placeholder="Add Item"
/>
<button>Add Item</button>
</form>
<ToDoList items={items} removeTask={removeTask} updateToDo={updateToDo} />
</div>
);
}
export default App;
/*************************************** My ToDoList.js *************************************/
import React from "react";
import "./ToDoList.css";
function ToDoList({ items, removeTask, updateToDo }) {
let toDoItems = items.map((item) => {
return (
<div className="toDoItems" key={item.key}>
<p>
<input
type="text"
id = {item.key}
value={item.text}
onChange={(e) => updateToDo(e.target.value, item.key)}
/>
<span onClick={() => removeTask(item.key)}>✘</span>
</p>
</div>
);
});
return <div>{toDoItems}</div>;
}
export default ToDoList;
You can map items into new array and when the item key matches the key parameter update the text property.
let updateToDo = (value, key) => {
const allItem = items.map(item => {
const newItem = {...item};
if (item.key === key) {
newItem.text = value;
}
return newItem;
});
console.log(...allItem);
addItem(allItem);
};
buddy!
First fo all, I suggest you can read document about React Hooks, it have clear explain how to update State when you using useState, I split several parts below:
On here const [items, addItem] = useState([]);, The Hooks useState will return a array, the first item is your value of state, at this time is a empty array [], the second item is a method which can update value of state.
Next, in your update method updateToDo, you used map to update original value of state and create the new value of state. so why didn't you call addItem to update your value of state?(Maybe you tried, but I have no idea for why you comment out that line?)
You just need to pass new value of state for addItem, and I suggest you can rename it to setItem instead of addItem.
You can following:
let updateToDo = (value, key) => { // <<<<<<< I need to make changes in this piece of code.
let allItem = items.map((e) => {
if (e.key === key) {
e.text = value;
}
addItem(allItem);
});
};

React onClick changes Paginated data one step Behind

Writing a simple frontend pagination, with a next and a back button. When I console.log(state) may state seems to be changing according to my plan when clicking the buttons. However the webpage displays the state data from one page back.
import React, { useEffect, useState } from "react";
import { useStateValue } from "../state";
import getChar from "../api/index";
import Spinner from "../components/loading"
export default function Characters(props) {
// access state and dispatch functions here
const [state, dispatch] = useStateValue();
const [loading, setLoading] = useState(true);
const [index, setIndex] = useState(0);
let [page, setPage] = useState(1);
const apiCall = async (num) => {
const first = (4 * num) - 4;
const last = (4 * num) - 1;
const res = await getChar.getCharacters();
const display = res.results.filter((char, index) => {
return (index >= first && index <= last);
})
await dispatch({
type: 'GET_CHARACTERS',
data: display
});
setLoading(false)
setIndex(res.results.length)
}
// move to next page
const next = () => {
const newPage = page + 1;
const maxPages = Math.ceil(index / 4);
if (newPage > maxPages) {
setPage(maxPages)
apiCall(maxPages)
} else {
setPage(newPage)
apiCall(newPage)
}
}
// move back a page
const back = () => {
const newPage = page - 1;
if (newPage === 0) {
setPage(1)
apiCall(1)
} else {
setPage(newPage)
apiCall(newPage)
}
}
useEffect(() => {
apiCall(1)
}, []);
if (loading) {
return <div><Spinner /></div>
}
return (
<div>
<h1>Characters Page</h1>
{
state.characters.map((char, index)=> (
(char.starships.length === 0) ?
<div className="List" key={index}>
<h3 className="character">
{char.name}
</h3>
</div>
:
<div className="List" key={index}>
<h3 className="character">
{char.name} - pilot
</h3>
</div>
))
}
<button onClick={back}>back</button>
<button onClick={next}>next</button>
</div>
);
}
export default function reducer(state, action) {
switch (action.type) {
case "GET_CHARACTERS":
state.characters = action.data;
return state; default: return state;
}
}
Shows the previous states data before click until the last page in the pagination. Will not show the last bit of paginated data until click the back button viceversa for the first bit of data.
The react can't detect a change in a mutable object. A new state object should be returned in every reducer.
case "GET_CHARACTERS":
state.characters = action.data;
return {...state};
or
case "GET_CHARACTERS":
return {...state, characters: action.data};
Some useful information:
This is a note on state and setState in the react documentation. using-state-correctly
This is mentioned in the introduction of the useState of the react, how does the react compare the two states before and after. Bailing out of a state update
This is what was mentioned when redux introduced the reducer. We don't mutate the state.
this is a demo of mutable object.
var a= {name:1}
var b = a;
b.name=2
var result = Object.is(a,b)
console.log(result) // true

Categories