I'm trying to update a react table data with async data. With this first example, my memo function is not called:
export const DataTableComponent = (props: State) => {
let internal_data: TableData[] = [];
const data: TableData[] = React.useMemo(
() => internal_data,
[internal_data]
);
data_provider.get_data().then(table_data => internal_data = table_data);
...
Changing internal_data to a state, I get the memo function fired, but now I have infinite loop:
export const DataTableComponent = (props: State) => {
const [internal_data, setInternalData] = React.useState<TableData[]>([]);
const data: TableData[] = React.useMemo(
() => internal_data,
[internal_data]
);
data_provider.get_data().then(table_data => setInternalData(table_data));
...
How to update my memo properly?
Using state is correct, but you should only update state within functions that will be called conditionally. Based on the example you gave, it seems useEffect will be the best solution.
useEffect(() => {
data_provider.get_data().then(table_data => setInternalData(table_data));
}, []);
Related
so i have 2 redux state and selectors that is working well. but i want to call the second selector (get the detail list based on category.id) inside map() loop. how can i do that?
const Dashboard = () => {
const [data, setData] = useState([]);
const categories = useSelector(viewFinalCategories);
// categories is loaded well
const createFinalData = () => {
const finalData = categories.map((category) => {
return {
title: category.label,
category: category,
data: useSelector(viewInventoriesByCategory(category.id)), // <- error hook cannot called here..
};
});
setData(finalData);
};
useEffect(() => {
createFinalData();
}, [categories]);
return (
// SectionList of RN here...
)
Since it violates the hook rule, you can't call useSelector inside a function.
the solution is to get the data in the component level and do the filtering inside the function
const {inventories} = useSelector(state => state)
const createFinalData = () => {
const finalData = categories.map((category) => {
return {
title: category.label,
category: category,
data: inventories.filter((item) => item.idCategory === idCategory)
};
});
setData(finalData);
};
useSelector is a hook and it has to follow hook rules, one of them is it can't be used inside a loop. Instaed, you need move all this logic from component to yet another selector. I would use createSelector from reselect in this case since you can combine selecting categories into newly created selectFinalData:
import { createSelector } from 'reselect'
const selectFinalData = () =>
createSelector(
viewFinalCategories,
(state, categories) => categories.map((category) => ({
title: category.label,
category: category,
data: viewInventoriesByCategory(category.id)),
})
)
)
and use it in component :
const finalData = useSelector(selectFinalData())
setData(finalData);
I am using a context like the following:
const placeCurrentOrder = async () => {
alert(`placing order for ${mealQuantity} and ${drinkQuantity}`)
}
<OrderContext.Provider
value={{
placeCurrentOrder,
setMealQuantity,
setDrinkQuantity,
}}
>
and I'm calling this context deep down with something like this (when the user clicks a button):
const x = () => {
orderContext.setMealQuantity(newMealQuantity)
orderContext.setDrinkQuantity(newDrinkQuantity)
await orderContext.placeCurrentOrder()
}
Sort of like I expect, the state doesn't update in time, and I always get the previous value of the state. I don't want to have a useEffect, because I want control over exactly when I call it (for example, if mealQuantity and drinkQuantity both get new values here, I don't want it being called twice. The real function is far more complex.)
What is the best way to resolve this? I run into issues like this all the time but I haven't really gotten a satisfactory answer yet.
You can set them in a ref. Then use the current value when you want to use it. The easiest way is probably to just create a custom hook something like:
const useStateWithRef = (initialValue) => {
const ref = useRef(initialValue)
const [state, setState] = useState(initialValue)
const updateState = (newState) => {
ref.current = typeof newState === 'function' ? newState(state) : newState
setState(ref.current)
}
return [state, updateState, ref]
}
then in your context provider component you can use it like:
const [mealQuantity, setMealQuantity, mealQuantityRef] = useStateWithRef(0)
const [drinkQuantity, setDrinkQuantity, drinkQuantityRef] = useStateWithRef(0)
const placeOrder = () => {
console.log(mealQuantityRef.current, drinkQuantityRef.current)
}
You can also just add a ref specifically for the order and then just update it with a useEffect hook when a value changes.
const [drinkQuantity, setDrinkQuantity] = useState(0)
const [mealQuantity, setMealQuantity] = useState(0)
const orderRef = useRef({
drinkQuantity,
mealQuantity
})
useEffect(() => {
orderRef.current = {
drinkQuantity,
mealQuantity,
}
}, [drinkQuantity, mealQuantity])
const placeOrder = () => {
console.log(orderRef.current)
}
I tried to calling a function more then 2 times using useEffect hook but the result it only calling that function with last call.
here is my code and try :
const [ selectValues, setSelectValues ] = useState([]);
const selectHandler = (e) => {
const filteredValues = selectValues.filter((i) => i.id !== e.id);
setSelectValues([ ...filteredValues, e ]);
};
useEffect(() => {
const obj1 = {...}
const obj2 = {...}
selectHandler(obj1)
selectHandler(obj2) // this is the only object will be saved to the state
},[])
I hope that issue explained properly
To be able to save any intermediate values from the state, you should update it in a callback manner, because selectValues contains the value which was there when component was rendered (initial value in our case).
const selectHandler = (e) => {
setSelectValues((prevValues) => [...prevValues.filter((i) => i.id !== e.id), e]);
};
I'm working on a hotel feature where the user can filter through and display the corresponding rooms available, however when I set the onClick to update the filters and display the filtered rooms, the rooms display correctly after the second click and there after.
const toggleSelection = (e) => {
setFilters((prevFilters) => ({
...prevFilters,
[e.name]: e.id,
}));
filterRooms();
};
const filterRooms = () => {
....
....
setRooms((prevRooms) => ({
...prevRooms,
filtered: filtered_rooms,
}));
};
useState() (and class component's this.setState()) are asynchronous, so your second state updater won't have an up to date value for filtered_rooms when it runs.
Rather than:
const [some_state, setSomeState] = useState(...);
const [some_other_state, setSomeOtherState] = useState(...);
const someHandler = e => {
setSomeState(...);
setSomeOtherState(() => {
// Uses `some_state` to calculate `some_other_state`'s value
});
};
You need to setSomeOtherState within a useEffect hook, and ensure to mark some_state as a dependency.
const [some_state, setSomeState] = useState(...);
const [some_other_state, setSomeOtherState] = useState(...);
useEffect(() => {
setSomeOtherState(() => {
// Uses `some_state` to calculate `some_other_state`'s value
});
}, [some_state]);
const someHandler = e => {
setSomeState(...);
};
It is hard to give an suggestion for your code since it is fairly edited, but it'd probably look like this:
const filterRooms = () => {
// ...
setRooms((prevRooms) => ({
...prevRooms,
filtered: filtered_rooms,
}));
};
useEffect(() => {
filterRooms();
}, [filtered_rooms]);
const toggleSelection = (e) => {
setFilters((prevFilters) => ({
...prevFilters,
[e.name]: e.id,
}));
};
See this codepen for a simple (albeit a bit contrived) example.
Since React hooks rely on the execution order one should generally not use hooks inside of loops. I ran into a couple of situations where I have a constant input to the hook and thus there should be no problem. The only thing I'm wondering about is how to enforce the input to be constant.
Following is a simplified example:
const useHookWithConstantInput = (constantIdArray) => {
const initialState = buildInitialState(constantIdArray);
const [state, changeState] = useState(initialState);
const callbacks = constantIdArray.map((id) => useCallback(() => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
}));
return { state, callbacks };
}
const idArray = ['id-1', 'id-2', 'id-3'];
const SomeComponent = () => {
const { state, callbacks } = useHookWithConstantInput(idArray);
return (
<div>
<div onClick={callbacks[0]}>
{state[0]}
</div>
<div onClick={callbacks[1]}>
{state[1]}
</div>
<div onClick={callbacks[2]}>
{state[2]}
</div>
</div>
)
}
Is there a pattern for how to enforce the constantIdArray not to change? My idea would be to use a creator function for the hook like this:
const createUseHookWithConstantInput = (constantIdArray) => () => {
...
}
const idArray = ['id-1', 'id-2', 'id-3'];
const useHookWithConstantInput = createUseHookWithConstantInput(idArray)
const SomeComponent = () => {
const { state, callbacks } = useHookWithConstantInput();
return (
...
)
}
How do you solve situations like this?
One way to do this is to use useEffect with an empty dependency list so it will only run once. Inside this you could set your callbacks and afterwards they will never change because the useEffect will not run again. That would look like the following:
const useHookWithConstantInput = (constantIdArray) => {
const [state, changeState] = useState({});
const [callbacks, setCallbacks] = useState([]);
useEffect(() => {
changeState(buildInitialState(constantIdArray));
const callbacksArray = constantIdArray.map((id) => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
});
setCallbacks(callbacksArray);
}, []);
return { state, callbacks };
}
Although this will set two states the first time it runs instead of giving them initial values, I would argue it's better than building the state and creating new callbacks everytime the hook is run.
If you don't like this route, you could alternatively just create a state like so const [constArray, setConstArray] = useState(constantIdArray); and because the parameter given to useState is only used as a default value, it'll never change even if constantIdArray changes. Then you'll just have to use constArray in the rest of the hook to make sure it'll always only be the initial value.
Another solution to go for would be with useMemo. This is what I ended up implementing.
const createCallback = (id, changeState) => () => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
};
const useHookWithConstantInput = (constantIdArray) => {
const initialState = buildInitialState(constantIdArray);
const [state, changeState] = useState(initialState);
const callbacks = useMemo(() =>
constantIdArray.map((id) => createCallback(id, changeState)),
[],
);
return { state, callbacks };
};