I have an array of objects in my React state. I want to be able to map through them, find the one I need to update and update its value field. The body of my request being sent to the server should look like:
{ name: "nameOfInput", value:"theUserSetValue" type: "typeOfInput" }
What I thought would be simple is causing me some heartache. My reducer function calls, and I hit the "I AM RUNNING" log where it then jumps over my map and simply returns my state (which is empty). Please note that I NEVER see the "I SHOULD RETURN SOMETHING BUT I DONT" log.
NOTE: I have learned that I could be simply handingling this with useState
function Form(props) {
const title = props.title;
const paragraph = props.paragraph;
const formBlocks = props.blocks.formBlocks
const submitEndpoint = props.blocks.submitEndpoint || "";
const action = props.blocks.action || "POST";
const formReducer = (state, e) => {
console.log("I AM RUNNING")
state.map((obj) => {
console.log("I SHOULD RETURN SOMETHING BUT I DONT")
if (obj.name === e.target.name) {
console.log("OBJ EXISTS", obj)
return {...obj, [e.target.name]:obj.value}
} else {
console.log("NO MATCH", obj)
return obj
}
});
return state
}
const [formData, setFormData] = useReducer(formReducer, []);
const [isSubmitting, setIsSubmitting] = useState(false);
=====================================================================
Where I am calling my reducer from:
<div className="form-block-wrapper">
{formBlocks.map((block, i) => {
return <FormBlock
key={block.title + i}
title={block.title}
paragraph={block.paragraph}
inputs={block.inputs}
buttons={block.buttonRow}
changeHandler={setFormData}
/>
})}
</div>
Issues
When using the useReducer hook you should dispatch actions to effect changes to the state. The reducer function should handle the different cases. From what I see of the code snippet it's not clear if you even need to use the useReducer hook.
When mapping an array not only do you need to return a value for each iterated element, but you also need to return the new array.
Solution
Using useReducer
const formReducer = (state, action) => {
switch(action.type) {
case "UPDATE":
const { name, value } = action.payload;
return state.map((obj) => obj.name === name
? { ...obj, [name]: value }
: obj
);
default:
return state;
}
};
...
const [formData, dispatch] = useReducer(formReducer, []);
...
{formBlocks.map((block, i) => {
return (
<FormBlock
key={block.title + i}
title={block.title}
paragraph={block.paragraph}
inputs={block.inputs}
buttons={block.buttonRow}
changeHandler={e => dispatch({
type: "UPDATE",
payload: {...e.target}
})}
/>
);
})}
Using useState
const [formData, setFormData] = useState([]);
...
const changeHandler = e => {
const { name, value } = e.target;
setFormData(data => data.map(obj => obj.name === name
? { ...obj, [name]: value }
: obj
));
};
...
{formBlocks.map((block, i) => {
return (
<FormBlock
key={block.title + i}
title={block.title}
paragraph={block.paragraph}
inputs={block.inputs}
buttons={block.buttonRow}
changeHandler={changeHandler}
/>
);
})}
I have come to understand my problem much better now and I'll update my question to reflect this.
As the user interacted with an input I needed to figure out if they had interacted with it before
If they did interact with it before, I needed to find that interaction in the state[] and update the value as required
If they didn't I needed to add an entirely new object to my forms state[]
I wrote two new functions, an AddObjectToArray function and an UpdateObjectInArray function to serve these purposes.
const handleFormInputChange = (e) => {
const { name, value, type } = e.target;
const addObjectToArray = (obj) => {
console.log("OBJECT TO BE ADDED TO ARRAY:", obj)
setFormData(currentArray => ([...currentArray, obj]))
}
const updateObjectInArray = () => {
const updatedObject = formData.map(obj => {
if (obj.name === name) {
//If the name matches, Update the value of the input
return ({...obj, value:value})
}
else {
//if no match just return the object as is
return obj
}
})
setFormData(updatedObject)
}
//Check if the user has already interacted with this input
if (formData.find(input => input.name === name)) {
updateObjectInArray()
}
else {
addObjectToArray({name, value, type})
}
}
I could get more complicated with this now and begin to write custom hooks that take a setState function as a callback and the data to be handled.
Related
I have a table where I'm setting data inside a useEffect from an api. My filter logic iterates through the "rows" variable which is being set inside this useEffect. However, every-time the user searches via an input which has an onChange event the useEffect setRows I believe is setting the data over and over again.
What would be a better way to set the data so it doesn't conflict with my filtering logic?
//State
const [documents, setDocuments] = useState<IDocument[]>([]);
const [rows, setRows] = useState<Data[]>([]);
//useEffect to setData
useEffect(() => {
//setDocuments from claimStore when component mounts
setDocuments(claimsStore.getIncomingDocuments());
//setRows from documents when component mounts
setRows(
documents.map((document) =>
createData(
document.documentAuthor ?? '',
document.documentMetadataId.toLocaleString(),
document.documentMetadataId.toLocaleString(),
document.documentName ?? '',
document.documentSource ?? '',
document.documentType,
document.featureId ?? '',
document.mimeType,
document.uploadDateTime,
),
),
);
}, [claimsStore, documents]);
//Filter logic that updates rows as user input values captured
const filterBySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const newFilters = { ...filters, [name]: value };
//Update filters with user input
setFilters(newFilters);
//Filter documents based on user input
const updatedList = rows.filter((document) => {
return (
document.documentAuthor.toLowerCase().includes(filters.documentAuthor.toLowerCase()) &&
document.documentName.toLowerCase().includes(filters.documentName.toLowerCase()) &&
document.documentSource.toLowerCase().includes(filters.documentSource.toLowerCase()) &&
document.documentType.includes(filters.documentType === 'All' ? '' : filters.documentType) &&
document.featureId.includes(filters.featureId)
);
});
//Trigger render with updated values
setRows(updatedList);
};
Use of filterBySearch:
<TableCell align={'center'} className={classes.tableCell}>
<input
value={filters.featureId}
onChange={(e) => filterBySearch(e)}
name="featureId"
className={classes.inputCell}
/>
</TableCell>
This is one of the things useMemo is good for: Have an array of filtered rows, that you update as necessary when rows or filters changes:
const [documents, setDocuments] = useState<IDocument[]>([]);
const [rows, setRows] = useState<Data[]>([]);
// ...
const filteredRows = useMemo(
() => rows.filter((document) => (
document.documentAuthor.toLowerCase().includes(filters.documentAuthor.toLowerCase()) &&
document.documentName.toLowerCase().includes(filters.documentName.toLowerCase()) &&
document.documentSource.toLowerCase().includes(filters.documentSource.toLowerCase()) &&
document.documentType.includes(filters.documentType === 'All' ? '' : filters.documentType) &&
document.featureId.includes(filters.featureId)
)),
[rows, filters]
);
Then display filteredRows, not rows.
With that change, filterBySearch just sets the filter, it doesn't actually do the filtering:
const filterBySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const newFilters = { ...filters, [name]: value };
//Update filters with user input
setFilters(newFilters);
};
useMemo will only call your callback when either rows or filters changes; otherwise, it'll just return the previous filtered array.
Here's a simplified demo — it shows words filtered by whatever you type in the filter, and randomly adds a word once every couple of seconds (this demonstrates that the filtering is repeated when the filter changes or when the rows change):
const { useState, useEffect, useRef, useMemo } = React;
const words = "one two three four five six seven eight nine ten".split(" ");
let nextRowId = 1;
const Example = () => {
const [rows, setRows] = useState(
words.slice(0, 5).map((value) => ({ id: nextRowId++, value }))
);
const [filter, setFilter] = useState("");
const filteredRows = useMemo(() => {
console.log(`Filtering rows`);
if (!filter) {
return rows;
}
return rows.filter((row) => row.value.includes(filter));
}, [rows, filter]);
useEffect(() => {
let handle;
tick();
function tick() {
handle = setTimeout(() => {
const value = words[Math.floor(Math.random() * words.length)];
console.log(`Adding "${value}"`);
setRows((rows) => [...rows, { id: nextRowId++, value }]);
tick();
}, 2000);
}
return () => {
clearTimeout(handle);
};
}, []);
const filterChange = ({ currentTarget: { value } }) => {
console.log(`Setting filter to "${value}"`);
setFilter(value);
};
return (
<div>
<div>
Filter: <input type="text" value={filter} onChange={filterChange} />
</div>
Rows - showing {filteredRows.length} of {rows.length} total:
<div>
{filteredRows.map((row) => (
<div key={row.id}>{row.value}</div>
))}
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
React's documentation says that useMemo is just for performance enhancement, it isn't a semantic guarantee (basically, React may call your callback even when nothing has actually changed). If you want a semantic guarantee, you can do it with a ref. You can even wrap that up into a hook that provides the semantic guarantee — I call it useHardMemo:
const useHardMemo = (fn, deps) => {
const ref = useRef(null);
let { current } = ref;
if (current) {
// Consistency check
if (
(deps && !current.deps) ||
(!deps && current.deps) ||
(deps && deps.length !== current.deps.length)
) {
throw new Error(
`Invalid call to useHardMemo, the dependency array must either always be present ` +
`or always be absent, and if present must always have the same number of items.`
);
}
}
if (!current || !deps?.every((dep, index) => Object.is(current.deps?.[index], dep))) {
ref.current = current = {
deps: deps?.slice(),
value: fn(),
};
}
return current.value;
};
Live Example:
const { useState, useEffect, useRef, createElement } = React;
const useHardMemo = (fn, deps) => {
const ref = useRef(null);
let { current } = ref;
if (current) {
// Consistency check
if (
(deps && !current.deps) ||
(!deps && current.deps) ||
(deps && deps.length !== current.deps.length)
) {
throw new Error(
`Invalid call to useHardMemo, the dependency array must either always be present ` +
`or always be absent, and if present must always have the same number of items.`
);
}
}
if (!current || !deps?.every((dep, index) => Object.is(current.deps?.[index], dep))) {
ref.current = current = {
deps: deps?.slice(),
value: fn(),
};
}
return current.value;
};
const words = "one two three four five six seven eight nine ten".split(" ");
let nextRowId = 1;
const Example = () => {
const [rows, setRows] = useState(
words.slice(0, 5).map((value) => ({ id: nextRowId++, value }))
);
const [filter, setFilter] = useState("");
const filteredRows = useHardMemo(() => {
console.log(`Filtering rows`);
if (!filter) {
return rows;
}
return rows.filter((row) => row.value.includes(filter));
}, [rows, filter]);
useEffect(() => {
let handle;
tick();
function tick() {
handle = setTimeout(() => {
const value = words[Math.floor(Math.random() * words.length)];
console.log(`Adding "${value}"`);
setRows((rows) => [...rows, { id: nextRowId++, value }]);
tick();
}, 2000);
}
return () => {
clearTimeout(handle);
};
}, []);
const filterChange = ({ currentTarget: { value } }) => {
console.log(`Setting filter to "${value}"`);
setFilter(value);
};
// I'm using `createElement` because I had to turn off SO's hopelessly outdated Babel because
// I wanted to be able to use optional chaining and such; so I couldn't use JSX.
// return (
// <div>
// <div>
// Filter: <input type="text" value={filter} onChange={filterChange} />
// </div>
// Rows - showing {filteredRows.length} of {rows.length} total:
// <div>
// {filteredRows.map((row) => (
// <div key={row.id}>{row.value}</div>
// ))}
// </div>
// </div>
// );
return createElement(
"div",
null,
createElement(
"div",
null,
"Filter: ",
createElement("input", { type: "text", value: filter, onChange: filterChange })
),
`Rows - showing ${filteredRows.length} of ${rows.length} total:`,
createElement(
"div",
null,
filteredRows.map((row) => createElement("div", { key: row.id }, row.value))
)
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(createElement(Example));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
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() } })
}
};
}
I'm now learning React and I have a problem with re-rendering component.
App.js code:
function App() {
const [expenses, setExpenses] = useState(INITIAL_EXPENSES);
const addNewExpenseHandler = (expense) => {
setExpenses((prevState) => {
return [expense, ...prevState];
}, changeYearHandler(filteredYear));
};
const filterExpenses = (expenses, year) => {
const newFilteredExpenses = expenses.filter((expense) => {
if (String(expense.date.getFullYear()) === year) {
return expense;
}
});
return newFilteredExpenses;
};
const [filteredYear, setFilteredYear] = useState('2019');
const [filteredExpenses, setFilteredExpenses] = useState(
filterExpenses(expenses, filteredYear)
);
const changeYearHandler = (value) => {
setFilteredYear(
value,
setFilteredExpenses(() => {
const newValue = filterExpenses(expenses, value);
return newValue;
})
);
};
return (
<>
<NewExpense onAddNewExpense={addNewExpenseHandler} />
<ExpenseFilter expenses={expenses} />
<ShowExpenses
onChangeYear={changeYearHandler}
data={filteredExpenses}
/>
</>
);
}
export default App;
The problem is that filteredExpenses isn't up-to-date. It's always retarded and it's the previous state. I was trying to call a changeYearHandler in callback of setExpenses and setFilteredExpense inside setFilteredYear but it's still doesn't work and I don't know why.
It feels like you're using a roundabout way to filter your expenses. What about just creating a memoized version of a filteredExpenses directly, using useMemo()?
const filteredExpenses = useMemo(() => {
return expenses.filter((expense) => {
if (String(expense.date.getFullYear()) === filteredYear) {
return expense;
}
});
}, [expenses, filteredYear]);
The dependency array will ensure that whenever expenses or filteredYear changes, then filteredExpenses will recompute and return a new filtered array (that is subsequently cached).
I have a component which return a form to CRUD with a state object. It perform well with data I entered manually. But when I update it with another object which I get data from excel. It doesn't perform as I expected. It doesn't access to the state object which clearly has data.
Here's the structure of it.
const ManageABC = () => {
const [obj, setObj] = useState({});
const [excelLoadedItems, setExcelLoadedItems] = useState({}); // loaded
useEffect(() => {
// fetching data for obj
}, []);
const ExcelListItem = ({index, name, onAdd}) => {
return (
<li key={index} onClick={onAdd}>{name}</li>
);
}
const handleOnAdd = (values) => {
// return a promise to add the item to obj
return new Promise((resolve) => {
console.log(obj);
// this log the obj when data entered manually but not when
// this function called from handleOnItemExcelAdd()
// which is important cuz I need it to check the condition below
if (values["id"] in obj) {
// not add
console.log('obj has this value');
resolve(true);
} else {
// add
console.log("obj doesn't have this value");
resolve(false);
}
}
}
const handleOnExcelItemAdd = (values, event) => {
handleOnAdd(values).then((res) => {
// remove li item if user says yes
if (res) {
const thisEle = event.target;
thisEle.closest("li").remove();
}
});
}
const handleExcelLoad(file) {
// read excel then
setExcelLoadedItems(
data.map((item, index) => (
<ExcelListItem
index={index}
name={item["name"]}
// this add and remove li good but it does not get the obj state to check the condition, the obj state remain nothing
onAdd={(event) => handleOnExcelItemAdd(item, event)}
/>
))
);
}
return (
{excelLoadedItems}
{obj.map(
// mapped out obj
)}
);
}
So because it does not check for condition, it auto modify the exist key in obj
You are just modifying elements from the actual DOM level. Since React has the mechanism of the virtual DOM. I guess this may cause the problem of bypassing this mechanism.
To let React knows that the DOM should be updated, use your setState hook to do so.
const handleOnExcelItemAdd = (values, event) => {
handleOnAdd(values).then((res) => {
if (res) {
const thisEle = event.target;
thisEle.closest("li").remove();
//construct a new object here
const yourNewObject = //some operation
//setObj makes the component render again, you will then see the new changes
setObj(yourNewObject);
}
});
}
I have useState variable that gets set based on a Promise that's resolved, how can I access the variable once it been setup
at the moment to get the correct values I have to use a setTimeout function, just wondering if there was a better way of doing that.
const FlagScreen = ({ t, i18n, history }) => {
const [flagAvailability, setFlagAvailability] = useState([]);
const [showFlags, setShowFlags] = useState([]);
useEffect(() => {
let flagsAvailable = [];
for (let [key, value] of Object.entries(flags)) {
if (key.indexOf(i18n.language) !== -1) {
for (const v of value) {
checkForAvailableAgent(`sales_${v}`, LINK_TO_STUDIO, SERVICE_ID)
.then(res => {
flagsAvailable[v] = res;
// Sets the flags availability i.e de: false, en: true
setFlagAvailability(flagsAvailable);
})
.catch(error => {
console.log("an error happened.");
});
}
}
}
}, [i18n.language]);
useEffect(() => {
//the value of flagAvailability is not available yet, I have to set a timeout function for
// 3 seconds for it to be available
console.log("flag availability: ", Object.entries(flagAvailability));
for (let [k, v] of Object.entries(flagAvailability)) {
console.log("key is: ", k);
if (v === true) {
setShowFlags(k);
}
}
}, [flagAvailability]);
}
<Container className="h-100">
<Row className="h-45 mt-5 text-center">
{ALL_STUDIOS_FLAGS.filter(item => {
return showFlags.includes(item);
}).map((item, index) => (
<Col key={index}>
<img
src={require(`../assets/flags/${item}.png`)}
alt={`${item} flag`}
/>
<span>{item.toUpperCase()}</span>
</Col>
))}
</Row>
any help would be appreicated,
The setFlagAvailability(flagsAvailable); in the first useEffect will only update the value of flagAvailability in second render
the second useEffect in the first render will only get the initial value of flagAvailability ( I know it's like a circle)
Every render has its own useEffect and useState
One of the solutions is to skip the first render in setShowFlags useEffect by creating a useRef flag like this
const ref = useRef(false)
const [flagAvailability, setFlagAvailability] = useState(true);
const [showFlags, setShowFlags] = useState(true);
useEffect(()=> {
if(ref.current){
...
setShowFlags();
ref.current = false;
}
},[flagAvailability])
useEffect(() => {
...
setFlagAvailability();
ref.current = true
},[i18n.language])
or you can simple change flagAvailability as useRef cause the refvalue will always be the new value.
const flagRef = useRef([]])
const [showFlags, setShowFlags] = useState(true);
useEffect(() => {
flagRef.current = flagsAvailable
},[i18n.language])
useEffect(()=> {
for (let [k, v] of Object.entries(flagRef.current)) {
setShowFlags();
}
},[showFlags])