React custom hook: Is there a way to set a property dynamically? - reactjs

I have a row of headers in which I would like to attach a clickHandler to control the ASC and DESC of data for their respective column.
My brute force attempt was to loop all the headers and create an object which would have a property to represent each header with a value to let the hook know which is active or not i.e. in ASC or DESC:
{ 1: false, 2: false, 3: false....}
The code:
function addPropToObj(arr) {
var obj = {}
arr.forEach(o => {
obj[o.id] = false;
})
return obj;
}
const activeObj = addPropToObj(userData)
function useActiveToggle(initialState = activeObj, bool = false) {
const [state, setState] = React.useState(initialState);
const toggle = React.useCallback((id) => {
return setState((previousState) => ({ ...previousState, [id]: !previousState[id] }))
}, [])
return [state, toggle]
}
They're only 199 objects in the array, but what if there were 1000, or 10,000?
I know I've heard objects are cheap in JS but it doesn't feel right.
So I tried this:
function useActiveToggle(initialState = {}, bool = false) {
const [state, setState] = React.useState(initialState);
const toggle = React.useCallback((id) => {
return setState((previousState) => {
if (!previousState[id]) {
previousState = { ...previousState, ...{ [id]: true } }
}
return ({ ...previousState, [id]: !previousState[id] })
})
}, [])
return [state, toggle]
}
But that results in a mess! Strange behaviors!
A part of me thinks my brute force idea might not be too horrible, correct me if I'm wrong but how could react do something like this with all the reconciliation it has to deal with?
Anyone have an idea how to approach this?

Just guessing this is what you want?
function useActiveToggle(initialState = {}, bool = false) {
const [state, setState] = React.useState(initialState);
const toggle = React.useCallback((id) => {
setState((previousState) => { // not "return setState", just do "setState"
const prevValue = Boolean(previousState[id]); // get previous value, undefined also equals to false
const nextState = {
...previousState,
[id]: !prevValue, // invert the value
};
return nextState;
});
}, [])
return [state, toggle]
}

Related

How can i update the value to use in other function?

I am using useState for this object 'selected', i want to get the updated value in this function verifyActive
const [selected, setSelected] = useState({ a: '', b: '' })
and i have a function that is active since a button
const setActive = (value) => {
setSelected({ ...selected, b: value })
verifyActive()
}
const verifyActive=()=>{
console.log('selected', selected) // is not updated
}
...
console.log(selected) //here is value is updated
return (
<button
onchange={(value) => { setActive(value) }}
/>
The local const selected is never going to change, but what you can do is save the new state to a variable, and pass it in to verifyActive:
const setActive = (value) => {
const newState = { ...selected, b: value };
setSelected(newState);
verifyActive(newState);
}
const verifyActive = (value) => {
console.log('selected', value);
}
change your setActive to this way
const setActive = (value) => {
setSelected((prevSelected) => { ...prevSelected, b: value })
verifyActive()
}

How to solve a situation when a component calls setState inside useEffect but the dependencies changes on every render?

I have this component:
const updateUrl = (url: string) => history.replaceState(null, '', url);
// TODO: Rename this one to account transactions ATT: #dmuneras
const AccountStatement: FC = () => {
const location = useLocation();
const navigate = useNavigate();
const { virtual_account_number: accountNumber, '*': transactionPath } =
useParams();
const [pagination, setPagination] = useState<PaginatorProps>();
const [goingToInvidualTransaction, setGoingToInvidualTransaction] =
useState<boolean>(false);
const SINGLE_TRANSACTION_PATH_PREFIX = 'transactions/';
// TODO: This one feels fragile, just respecting what I found, but, we could
// investigate if we can jsut rely on the normal routing. ATT. #dmuneras
const transactionId = transactionPath?.replace(
SINGLE_TRANSACTION_PATH_PREFIX,
''
);
const isFirst = useIsFirstRender();
useEffect(() => {
setGoingToInvidualTransaction(!!transactionId);
}, [isFirst]);
const {
state,
queryParams,
dispatch,
reset,
setCursorAfter,
setCursorBefore
} = useLocalState({
cursorAfter: transactionId,
includeCursor: !!transactionId
});
const {
filters,
queryParams: globalQueryParams,
setDateRange
} = useGlobalFilters();
useUpdateEffect(() => {
updateUrl(
`${location.pathname}?${prepareSearchParams(location.search, {
...queryParams,
...globalQueryParams
}).toString()}`
);
}, [transactionId, queryParams]);
useUpdateEffect(() => dispatch(reset()), [globalQueryParams]);
const account_number = accountNumber;
const requestParams = accountsStateToParams({
account_number,
...state,
...filters
});
const { data, isFetching, error, isSuccess } =
useFetchAccountStatementQuery(requestParams);
const virtualAccountTransactions = data && data.data ? data.data : [];
const nextPage = () => {
dispatch(setCursorAfter(data.meta.cursor_next));
};
const prevPage = () => {
dispatch(setCursorBefore(data.meta.cursor_prev));
};
const onRowClick = (_event: React.MouseEvent<HTMLElement>, rowData: any) => {
if (rowData.reference) {
if (rowData.id == transactionId) {
navigate('.');
} else {
const queryParams = prepareSearchParams('', {
reference: rowData.reference,
type: rowData.entry_type,
...globalQueryParams
});
navigate(
`${SINGLE_TRANSACTION_PATH_PREFIX}${rowData.id}?${queryParams}`
);
}
}
};
const checkIfDisabled = (rowData: TransactionData): boolean => {
return !rowData.reference;
};
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [data?.meta]);
const showTransactionsTable: boolean =
Array.isArray(virtualAccountTransactions) && isSuccess && data?.data;
const onTransactionSourceLoaded = (
transactionSourceData: PayoutDetailData
) => {
const isIncludedInPage: boolean = virtualAccountTransactions.some(
(transaction: TransactionData) => {
if (transactionId) {
return transaction.id === parseInt(transactionId, 10);
}
return false;
}
);
if (!goingToInvidualTransaction || isIncludedInPage) {
return;
}
const fromDate = dayjs(transactionSourceData.timestamp);
const toDate = fromDate.clone().add(30, 'day');
setDateRange({
type: 'custom',
to: toDate.format(dateFormat),
from: fromDate.format(dateFormat)
});
setGoingToInvidualTransaction(false);
};
const fromDate = requestParams.created_after || dayjs().format('YYYY-MM-DD');
const toDate = requestParams.created_before || dayjs().format('YYYY-MM-DD');
const routes = [
{
index: true,
element: (
<BalanceWidget
virtualAccountNumber={account_number}
fromDate={fromDate}
toDate={toDate}
/>
)
},
{
path: `${SINGLE_TRANSACTION_PATH_PREFIX}:transaction_id`,
element: (
<TransactionDetails
onTransactionSourceLoaded={onTransactionSourceLoaded}
/>
)
}
];
return (........
I get this 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.
The useEffect where the issue is, it is this one:
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [data?.meta]);
Considering previous answers, would the solution be to make sure I return a new object each time? But I am not sure what would be the best approach. Any clues ?
did you want the useEffect to start every changes of 'data?.meta' ?
Without reading all the code, I believe the data.meta object changes on every render. There is a way to change the useEffect to narrow done its execution conditions:
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [!data?.meta, data?.meta?.has_previous_page, data?.meta?.has_next_page]);
Please note the ! before data.?.meta which makes the hook test only for presence or absence of the object, since your code doesn't need more than that information.

React and Type script createContext initial data for functions

I have a context that is used to create, update, list and delete a set of data.
It is created as follows with the following types to be returned.
type TDataContext = {
loading: boolean,
getDataList: () => TData[],
addNewData : (data: TData) => Promise<boolean> ,
editData: (dataId: string, data: TData) => Promise<boolean>,
deleteData: (dataId: string) => Promise<boolean>,
}
const ADataContext = createContext<TGymClientDataContext>({
// How to initialize these here ?
})
export const ADataProvider: FC<{}> = ({children}) => {
const [loading, setLoading] = useState(true)
const [snapshot, loading, error] = useCollection<TDataStore>(db.collection(FIRESTORE_COLLECTIONS.DATA_COLLECTION), {});
const getDataList = async (): TData[] => {
if(loading == true) {
return []
}
if(typeof snapshot == "undefined"){
return []
}
return snapshot?.docs.map((item) => {
const fetched = item.data();
return ({
id: item.id,
doj: fetched.doj.toDate(),
firstName: fetched.firstName,
lastName: fetched.lastName,
})
})
}
// ... Function definitions
const values = {
loading,
getDataList,
addNewData,
editData,
deleteData,
}
return <ADataContext.Provider value={values}>
{children}
</ADataContext.Provider>
}
export const useDataHook = () => useContext(ADataContext)
I would like to know what will be the good possible way to pass initial data to createContext ?
I'm creating this with the following usage to be expected
const {loading, getClientList, editClientData, addNewClient, deleteClientData} = useGymClient()
The functions are simply expected to return true or false depending on the operations being successful or not.
Another question is,
The data has to be always caught via getDataList function.
is directly passing the snapshot variable down better approach ? In that case, what will be a better initializer for the create context

is there a way to shorten this change handler that uses hooks in React?

With class based components you could use the computed properties to in order to use one single handler for several inputs with the help of an id attribute like this:
function handleChange(evt) {
const value = evt.target.value;
setState({
...state,
[evt.target.name]: value
});
}
This is thebest I caome up with but im sure there must be a shorter way:
const handleChange = (e) => {
if (e.target.id === 'name') setName(e.target.value)
if (e.target.id === 'genre') setGenre(e.target.value)
if (e.target.id === 'description') setDescription(e.target.value)
}
Any ideas?
Thanks.
You could have create an object in the useState and update just like the first example.
// Declare the state with a object default value
const [state, setState] = useState({})
const handleChange = (e) => {
setState(prev => ({
...prev,
[e.target.id]: e.target.value
}))
}
If you can't change the useState and have a very strong reason to have these 3 separeted useStates, you could create an object that returns the 3 functions based on the e.target.id
const updater = {
name: setName,
genre: setGenre,
description: setDescription,
}
And when updating you do
const handleChange = (e) => {
const updateValue = updater[e.target.id]
if (updateValue) {
updateValue(e.target.value)
}
else {
// No function to update found
// Do something else
}
}

How to convert this.setState to setItemIdToSelectedMap?

I want to convert this function setState using hooks
this.setState(previousState => {
const newItemIdToSelectedMap = {
...previousState.itemIdToSelectedMap,
[itemId]: !previousState.itemIdToSelectedMap[itemId],
};
return {
itemIdToSelectedMap: newItemIdToSelectedMap,
};
});
Here is my initial state
const [itemIdToSelectedMap, setItemIdToSelectedMap] = useState({});
I want to convert to something like this
const toggleItem = itemId => {
setItemIdToSelectedMap(state => ({
...state,
[itemId]: !state.itemIdToSelectedMap[itemId]
}));
};
This is for the checkbox function. I want to make my specific checkbox checked but it's not working. Let me know what you think. Thanks
you dont need to use itemIdToSelectedMap again inside of your calback function that you are passing to your state setter, the state itself has the same value, your function should like below:
const toggleItem = itemId => {
setItemIdToSelectedMap(state => ({
...state,
[itemId]: !state[itemId]
}));
};

Resources