Requiring a Hook to update on state change - javascript

So this is a component I have in React JS
const EnterSetData = ({ user_card_set }) => {
const [cardlist, setCardlist] = useState({ data: [] });
let setlist= useGetSetDB();
//Save and reset form
const sendSets = () => {
const dupes = findDuplicates(setlist, cardlist);
if (dupes.length > 0) {
saveSets(setlist)
setCardlist({ data: [] });
} else {
handleDupes(dupes);
}
};
useEffect(() => {
setCardlist(card_set);
}, [card_set]);
return (
<>
{cardlist.data.map((card, key) => (
<a> .......</a>))}
<button
type="button"
onClick={sendSets}
>
Save Cards
</button>
</div>
The main issue is while data is saved, there is a major bug. useGetSetDB() is used to fetch the entire set list from an API. After saving, I use setCardlist to update the component state. It is here that I notice that setlist does not change. The new entries are not there and this causes findDuplicates to fail. My DB checks for duplicates, and thus console.logs will show that the inserts failed. How do I force useGetSetDB to update on every state change.This is useGetSetDB
const useGetSetDB = () => {
const[state,setState] = useState([]);
const fetchData = async () => {
const data = await fetch("/sets");
const res = await data.json();
return res;
};
useEffect(()=>{
fetchData().then(response => {setState(response)})
},[])
return state
}
export default useGetSetDB;

You can pass cardlist to useGetSetDB like below
const [cardlist, setCardlist] = useState({ data: [] });
let setlist = useGetSetDB(cardlist); //whenever cardlist gets updated, we trigger side-effect again
You also need to modify useGetSetDB to align with state change
const useGetSetDB = (cardlist) => {
const[state,setState] = useState([]);
const fetchData = async () => {
const data = await fetch("/sets");
const res = await data.json();
return res;
};
useEffect(()=>{
fetchData().then(response => {setState(response)})
}, [cardlist]) //add `cardlist` to dependency list
return state
}
export default useGetSetDB;

Related

Empty Object on React useEffect

In my project I have the component ExportSearchResultCSV. Inside this component the nested component CSVLink exports a CSV File.
const ExportSearchResultCSV = ({ ...props }) => {
const { results, filters, parseResults, justify = 'justify-end', fileName = "schede_sicurezza" } = props;
const [newResults, setNewResults] = useState();
const [newFilters, setNewFilters] = useState();
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true)
const [headers, setHeaders] = useState([])
const prepareResults = () => {
let newResults = [];
if (results.length > 1) {
results.map(item => {
newResults.push(parseResults(item));
}); return newResults;
}
}
const createData = () => {
let final = [];
newResults && newResults?.map((result, index) => {
let _item = {};
newFilters.forEach(filter => {
_item[filter.filter] = result[filter.filter];
});
final.push(_item);
});
return final;
}
console.log(createData())
const createHeaders = () => {
let headers = [];
newFilters && newFilters.forEach(item => {
headers.push({ label: item.header, key: item.filter })
});
return headers;
}
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters])
return (
<div className={`flex ${justify} h-10`} title={"Esporta come CSV"}>
{results.length > 0 &&
<CSVLink data={createData()}
headers={headers}
filename={fileName}
separator={";"}
onClick={async () => {
await setNewFilters(filters);
await setNewResults(prepareResults());
await setData(createData());
await setHeaders(createHeaders());
}}>
<RoundButton icon={<FaFileCsv size={23} />} onClick={() => { }} />
</CSVLink>}
</div >
)
}
export default ExportSearchResultCSV;
The problem I am facing is the CSV file which is empty. When I log createData() function the result is initially and empty object and then it gets filled with the data. The CSV is properly exported when I edit this component and the page is refreshed. I tried passing createData() instead of data to the onClick event but it didn't fix the problem. Why is createData() returning an empty object first? What am I missing?
You call console.log(createData()) in your functional component upon the very first render. And I assume, upon the very first render, newFilters is not containing anything yet, because you initialize it like so const [newFilters, setNewFilters] = useState();.
That is why your first result of createData() is an empty object(?). When you execute the onClick(), you also call await setNewFilters(filters); which fills newFilters and createData() can work with something.
You might be missunderstanding useEffect(). Passing something to React.useEffect() like you do
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters]) <-- look here
means that useEffect() is only called, when results or filters change. Thus, it gets no executed upon initial render.

Store copy of data state before applying and after clearing filter

I have a simple app that calls an API, returns the data (as an array of objects), sets a data state, and populates a few charts and graphs.
const loadData = async () => {
const url = 'https://my-api/api/my-api';
const response = await fetch(url);
const result = await response.json();
setData(result.data);
}
After setting the data, the data state is sent to every component and everything is populated. I created a filters pane that can filter the existing, populated data (for example, a gender filter that filters the data on the selected gender). What I did, and it's obviously wrong, is created an onChange handler that filters the data to the selected gender then uses the setData (sent as a prop; also the state variable, data) to set the filtered data. When I clear the filter, the original, non-filtered data is replaced by the filtered data so the original data is lost.
const genderFilterHanlder = (e) => {
const filteredData = data.filter(x => x.gender === e.target.value);
setData(filteredData);
}
I tried creating an intermediary state the preserves the original data then upon clearing the filters, it sets the data (setData) to the original. But this breaks when I have a filter that allows you to choose multiple values (like multiple languages; I can choose one language, clear it successfully, but if I choose two languages, then clear one, it breaks as the data is now the first chosen filter data).
How would I go about this?
I'd leave data itself alone and have a separate filteredData state member that you set using an effect:
const [filteredData, setFilteredData] = useState(data);
const [filter, setFilter] = useState("");
// ...
useEffect(() => {
const filteredData = filter ? data.filter(/*...apply filter...*/) : data;
setFilteredData(filteredData);
}, [filter, data]); // <=== Note our dependencies
// ...
// ...render `filteredData`, not `data`...
Then your change handler just updates filter (setFilter(/*...the filter...*/)).
That way, any time the filter changes, or any time data changes, the data gets filtered and rendered.
Live Example:
const { useState, useEffect } = React;
const Child = ({data}) => {
const [filteredData, setFilteredData] = useState(data);
const [filter, setFilter] = useState("");
useEffect(() => {
if (!filter) {
setFilteredData(data);
return;
}
const lc = filter.toLocaleLowerCase();
const filteredData = filter
? data.filter(element => element.toLocaleLowerCase().includes(lc))
: data;
setFilteredData(filteredData);
}, [filter, data]); // <=== Note our dependencies
return <div>
<input type="text" value={filter} onChange={({currentTarget: {value}}) => setFilter(value)} />
<ul>
{filteredData.map(element => <li key={element}>{element}</li>)}
</ul>
</div>;
};
const greek = [
"alpha",
"beta",
"gamma",
"delta",
"epsilon",
"zeta",
"eta",
"theta",
"iota",
"kappa",
"lambda",
"mu",
"nu",
"xi",
"omicron",
"pi",
"rho",
"sigma",
"tau",
"upsilon",
"phi",
"chi",
"psi",
"omega",
];
const initialData = greek.slice(0, 4);
const Example = () => {
const [data, setData] = useState(initialData);
useEffect(() => {
const handle = setInterval(() => {
setData(data => {
if (data.length < greek.length) {
return [...data, greek[data.length]];
}
clearInterval(handle);
return data;
});
}, 800);
return () => {
clearInterval(handle);
};
}, []);
return <Child data={data} />;
};
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
You need to have two states in your case. You can follow below example.
const data = [
{ id: 1, name: "Mahela" },
{ id: 2, name: "Sangakkara" },
{ id: 3, name: "Dilshan" },
{ id: 4, name: "Malinga" },
{ id: 5, name: "Rangana" },
{ id: 6, name: "Kulasekara" }
];
const [inputValue, setInputValue] = useState("");
const [filtered, setFiltered] = useState([]);
const inputChangeHandler = (e) => {
setInputValue(e.target.value);
setFiltered(data.filter((item) => item.name === e.target.value));
};
return (
<div className="App">
<input type="text" value={inputValue} onChange={inputChangeHandler} />
{filtered.length > 0 && filtered.map((item) => <p>{item.name}</p>)}
{filtered.length === 0 && data.map((item) => <p>{item.name}</p>)}
</div>
);
Here I have used constant for data array. You can use your data state. Don't replace data state with new data instead use second state for the filtered data.
dont change your state after filtering .
hold your original data and get a copy of it in a variable and do your stuff on it like filtering. then use that variable for rendering the filtered data and if you want to clear the input or anything you can use your original state that you have been storing.
const FilterData = () => {
const [data, setData] = useState([]);
const [select, setSelect] = useState();
const loadData = async () => {
const url = "https://my-api/api/my-api";
const response = await fetch(url);
const result = await response.json();
setData(result.data);
};
const filteredData = data.filter((item) => {
if (!select) return null;
return item.gender !== select;
});
return (
<div>
<select>...</select>
{filteredData.map(...)}
</div>
);
};
In this value must the male/female (or any) and filter must be the type (key) now on change on the filter or value filterData gives you the result
use filterData to render elements and data state will remain constant always.
const[data, setDate] = useState([]);
const[filterData, setFilterData] = useState([]);
const[filter, setFilter] = useState('gender');
const[value, setValue] = useState('male');
const fetchData = () => {
const url = '';
const data = await fetch(url);
setData([...data]);
setFilterData([...data]);
}
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
//initially I have set the filter to gender, but it should be null, once the user selects any filter change the state of the filter, so for the first time when the screen mounts else part executes.
if(filter){
const newFilterData = data.filter(item => item[filter] == value);
setFilterData([...newFilterData]);
}
}, [filter, value]);

how to display my list from store at initialization with easy-peasy?

I want to get a list of churches from the store at initialization but i can't. The log get my initial array and the new one but doesn't display. Log below:
log
here is my model:
const churchModel = {
items: [],
// ACTIONS
setAllChurches: action((state, payload) => {
state.items = payload;
}),
getInitialChurches: thunk(async (actions) => {
const { data } = await axios.post(
'http://localhost:3000/api/geo/closeto?latlong=2.3522219 48.856614&distance=10000'
);
let array = [];
const resData = data.map(async (index) => {
const res = await axios.get(`http://localhost:3000/api/institutions/all?idInstitution=${index.idInstitution}`);
array.push(res.data[0]);
});
actions.setAllChurches(array);
})
}
and my component:
const ChurchList = () => {
const classes = useStyles();
const setInitialChurches = useStoreActions(action => action.churches.getInitialChurches);
const churches = useStoreState(state => state.churches.items);
const [activeItem, setActiveItem] = React.useState(null);
useEffect(() => {
setInitialChurches()
}, []);
return (
<div className={classes.root} style={{marginTop: '20px',}}>
{ churches.map( (church) => (
<ChurchItem
key={ church.idInstitution }
church={ church }
setActiveItem={setActiveItem}
activeItem={activeItem}
/>)
), console.log(churches)}
</div>
)
};
export default ChurchList;
I tried a useEffect but nothing true. Could you help me please ?
that is not a good location to put console.log in, either put it outside the component render, inside the map or on a useEffect.
You can achieve it by using useEffect and passing churches on the array.
useEffect(() => {
// this will log everytime churches changes / initialized churches
console.log(churches);
}, [churches]);

How can I run multiple queries on load of functional component using UseEffect and get result in render method?

I have the following functional component where, on load of the component, it needs to loop through an array and run some async queries to populdate a new array I want to display in render method.
import React, { useEffect, useState, useContext } from 'react';
import { AccountContext } from '../../../../providers/AccountProvider';
import { GetRelationTableCount } from '../../../../api/GetData';
import { getTableAPI } from '../../../../api/tables';
const RelatedRecordsPanel = (props) => {
const { userTokenResult } = useContext(AccountContext);
const { dataItem } = props;
const [relatedTableItems, setRelatedTableItems] = useState([]);
useEffect(() => {
const tempArray = [];
const schema = `schema_${userTokenResult.zoneId}`;
const fetchData = async () => {
return Promise.all(
dataItem.tableinfo.columns
.filter((el) => el.uitype === 1)
.map(async (itm) => {
const tblinfo = await getTableAPI(itm.source.lookuptableid);
const tblCount = await GetRelationTableCount(
dataItem.tableid,
itm.source.jointable,
schema,
);
const TableIconInfo = { name: tblinfo.name, icon: tblinfo.icon, count: tblCount };
tempArray.push(TableIconInfo);
})
);
};
fetchData();
setRelatedTableItems(tempArray)
}, []);
return (
<div>
{relatedTableItems.length > 0 ? <div>{relatedTableItems.name}</div> : null}
</div>
);
};
In the above code, the queries run correctly and if I do a console.log in the loop, I can see if fetches the data fine, however, the array is always [] and no data renders. How do I write this async code such that it completes the queries to populate the array, so that I can render properly?
Thx!
You aren't using the return value of the Promise.all and since all your APIs are async, the tempArray is not populated by the time you want to set it into state
You can update it like below by waiting on the Promise.all result and then using the response
useEffect(() => {
const schema = `schema_${userTokenResult.zoneId}`;
const fetchData = async () => {
return Promise.all(
dataItem.tableinfo.columns
.filter((el) => el.uitype === 1)
.map(async (itm) => {
const tblinfo = await getTableAPI(itm.source.lookuptableid);
const tblCount = await GetRelationTableCount(
dataItem.tableid,
itm.source.jointable,
schema,
);
const TableIconInfo = { name: tblinfo.name, icon: tblinfo.icon, count: tblCount };
return TableIconInfo;
})
);
};
fetchData().then((res) => {
setRelatedTableItems(res);
});
}, []);

How to prevent a state update on a react onClick function (outside useEffect)?

When I use useEffect I can prevent the state update of an unmounted component by nullifying a variable like this
useEffect(() => {
const alive = {state: true}
//...
if (!alive.state) return
//...
return () => (alive.state = false)
}
But how to do this when I'm on a function called in a button click (and outside useEffect)?
For example, this code doesn't work
export const MyComp = () => {
const alive = { state: true}
useEffect(() => {
return () => (alive.state = false)
}
const onClickThat = async () => {
const response = await letsbehere5seconds()
if (!alive.state) return
setSomeState('hey')
// warning, because alive.state is true here,
// ... not the same variable that the useEffect one
}
}
or this one
export const MyComp = () => {
const alive = {}
useEffect(() => {
alive.state = true
return () => (alive.state = false)
}
const onClickThat = async () => {
const response = await letsbehere5seconds()
if (!alive.state) return // alive.state is undefined so it returns
setSomeState('hey')
}
}
When a component re-renders, it will garbage collect the variables of the current context, unless they are state-full. If you want to persist a value across renders, but don't want to trigger a re-renders when you update it, use the useRef hook.
https://reactjs.org/docs/hooks-reference.html#useref
export const MyComp = () => {
const alive = useRef(false)
useEffect(() => {
alive.current = true
return () => (alive.current = false)
}
const onClickThat = async () => {
const response = await letsbehere5seconds()
if (!alive.current) return
setSomeState('hey')
}
}

Categories

Resources