Setting multiple possible initial states in useState hook - reactjs

The aim is to set the initial state:
if the StateColumn is visible/applicable (true)
and it should be accepted or waiting_for_review or rejected (or nothing if the StateColumn is not shown), depending on this order if they exist
This is what I did:
const [selectedStuffState, setSelectedStuffState] = useState( showStateColumn? ('accepted' || 'waiting_for_review' || 'rejected') : '' );
Is there a better way of doing this?

You will need 1 function to be run on every state change of stateColumn. Where will set the state of setSelectedStuffState. But for that also, you will require 1 more state which will decide the optional values of your state.
The logic may work like this
const YourComponent = (props) => {
const { showStateColumn, status } = props;
const [selectedStuffState, setSelectedStuffState] = useState("");
useEffect(() => {
if (showStateColumn) {
// now here you will require condition to check if the selectedStuffState is 'accepted' || 'waiting_for_review' || 'rejected'
// you will require 1 more status prop which will decide the state of selectedStuffState
if (status === "accepted") {
setSelectedStuffState("");
} else if (status === "waiting_for_review") {
setSelectedStuffState("waiting_for_review");
} else {
setSelectedStuffState("rejected");
}
}
}, [showStateColumn]); //useEffect will run on every change of showStateColumn
};

const setInitalState = () => {
return showStateColumn? ('accepted' || 'waiting_for_review' || 'rejected') : ''
}
const [selectedStuffState, setSelectedStuffState] = useState(setInitalState());

Related

PReact useEffect doesn't trigger on state change

I want to do a recursive function which basically runs each time a folder has a subfolder under it, so I can take all the content from all the subfolders available.
Not sure what I am missing here, but the state change of subFolders does not trigger the useEffect which have it as dependency:
const [imageList, setImageList] = useState([]) as any;
const [subFolders, setSubFolders] = useState([]) as any;
const getFilesFromFolder = (fileId: string) => {
let noToken = false;
const requestFunction = ((pageToken?: string) => {
gapi.client.drive.files.list({
q: `'${fileId}' in parents`,
pageToken
}).execute((res: any) => {
const token = res.nextPageToken && res.nextPageToken || null;
const images = res.files.filter((file: any ) =>
file.mimeType === 'image/jpeg' ||
file.mimeType === 'image/png' ||
file.mimeType === 'image/jpg'
);
setSubFolders([...subFolders, ...res.files.filter((file: any ) => file.mimeType === 'application/vnd.google-apps.folder')]);
setImageList([...imageList, ...images])
if (token) {
requestFunction(token);
} else {
noToken = true;
}
}).catch((err: any) => {
console.log('err', err)
})
});
if (!noToken) {
requestFunction();
}
}
useEffect(() => {
if (subFolders && subFolders.length > 0) {
subFolders.forEach((subFolder: any) => {
getFilesFromFolder(subFolder.id);
});
}
}, [subFolders])
Since you are basically looping over file structure and enqueueing state updates I'm going to just assume any issues you have are because you are using normal state updates. When these are enqueued in loops or multiple times within a render cycle they overwrite the previous enqueued update.
To resolve you should really use functional state updates. This is so each update correctly updates from the previous state, and not the state from the previous render cycle. It's a subtle but important difference and oft overlooked issue.
Functional Updates
setSubFolders(subFolders => [
...subFolders,
...res.files.filter((file: any ) =>
file.mimeType === 'application/vnd.google-apps.folder'
),
]);
setImageList(imageList => [...imageList, ...images]);

Cannot read property of null comparing state after state change

I'm making a rock, paper, scissors game.
I want to increment wins after player has selected their option.
I get an error as if the if statement is unaware of any value.
Heres the code:
What the state looks like
const [wins, setWins] = useState(0);
const [playerSelect, setPlayerSelect] = useState(null);
const [computerSelect, setComputerSelect] = useState(null);
// in browser
{id: 1, name:'rock'}
Player select function
const handleSelection = choice => {
const optionClicked = options.find(o => o.id === choice);
setPlayerSelect(optionClicked);
if (playerSelect.name === 'rock' && computerSelect.name === 'scissors') {
setWins(wins => wins + 1);
}
};
I have tried added precautions to check that playerSelect state contains a value first to no prevail. win state in not incremented.
Example:
const handleSelection = choice => {
const optionClicked = options.find(o => o.id === choice);
setPlayerSelect(optionClicked);
if (
playerSelect &&
playerSelect.name &&
playerSelect.name === 'rock' &&
computerSelect.name === 'scissors'
) {
setWins(wins => wins + 1);
}
};
Edit: I have tried the following code and it works after the second click only.
const checkWhoWins = () => {
if (playerSelect.name === 'rock' && computerSelect.name === 'scissors') {
setWins(wins => wins + 1);
}
};
playerSelect && checkWhoWins();
if you want to do an action after a state update you would use useEffect for that, and specify its dependencies as second argument:
useEffect(() => {
// you can use optional chaining to avoid triggering errors here
if (playerSelect?.name === 'rock' && computerSelect?.name === 'scissors') {
setWins(wins => wins + 1);
}
}, [playerSelect, computerSelect])
// playerSelect & computerSelect are the correct dependencies here

Trigger completeMethod in PrimeReact Autocomplete on callback

I am using primereact's Autocomplete component. The challenge is that I don't want to set the options array to the state when the component loads; but instead I fire an api call when the user has typed in the first 3 letters, and then set the response as the options array (This is because otherwise the array can be large, and I dont want to bloat the state memory).
const OriginAutocomplete = () => {
const [origins, setOrigins] = useState([]);
const [selectedOrigin, setSelectedOrigin] = useState(null);
const [filteredOrigins, setFilteredOrigins] = useState([]);
useEffect(() => {
if (!selectedOrigin || selectedOrigin.length < 3) {
setOrigins([]);
}
if (selectedOrigin && selectedOrigin.length === 3) {
getOrigins(selectedOrigin).then(origins => {
setOrigins([...origins]);
});
}
}, [selectedOrigin, setOrigins]);
const handleSelect = (e) => {
//update store
}
const searchOrigin = (e) => {
//filter logic based on e.query
}
return (
<>
<AutoComplete
value={selectedOrigin}
suggestions={ filteredOrigins }
completeMethod={searchOrigin}
field='code'
onChange={(e) => { setSelectedOrigin(e.value) }}
onSelect={(e) => { handleSelect(e) }}
className={'form-control'}
placeholder={'Origin'}
/>
</>
)
}
Now the problem is that the call is triggered when I type in 3 letters, but the options is listed only when I type in the 4th letter.
That would have been okay, infact I tried changing the code to fire the call when I type 2 letters; but then this works as expected only when I key in the 3rd letter after the api call has completed, ie., I type 2 letters, wait for the call to complete and then key in the 3rd letter.
How do I make the options to be displayed when the options array has changed?
I tried setting the filteredOrigins on callback
getOrigins(selectedOrigin).then(origins => {
setOrigins([...origins]);
setFilteredOrigins([...origins])
});
But it apparently doesn't seem to work.
Figured it out. Posting the answer in case someone ponders upon the same issue.
I moved the code inside useEffect into the searchOrigin function.
SO the searchOrigin functions goes like below:
const searchOrigin = (e) => {
const selectedOrigin = e.query;
if (!selectedOrigin || selectedOrigin.length === 2) {
setOrigins([]);
setFilteredOrigins([]);
}
if (selectedOrigin && selectedOrigin.length === 3) {
getOrigins(selectedOrigin).then(origins => {
setOrigins([...origins]);
setFilteredOrigins(origins);
});
}
if (selectedOrigin && selectedOrigin.length > 3) {
const filteredOrigins = (origins && origins.length) ? origins.filter((origin) => {
return origin.code
.toLowerCase()
.startsWith(e.query.toLowerCase()) ||
origin.name
.toLowerCase()
.startsWith(e.query.toLowerCase()) ||
origin.city
.toLowerCase()
.startsWith(e.query.toLowerCase())
}) : [];
setFilteredOrigins(filteredOrigins);
}
}

react setting state in a condition inside useEffect

I'm trying to avoid showing an alert in React Native more than once.
To do that, I am trying to update the state inside a condition which is inside a useEffect:
const [permissionStatus, setPermissionStatus] = useState('');
const [permissionsAlertShown, setPermissionsAlertShown] = useState(false);
useEffect(() => {
function handleAppStateChange() {
if (
AppState.currentState === 'active' &&
permissionStatus === 'denied' &&
!permissionsAlertShown
) {
setPermissionsAlertShown(true);
Alert.alert(
...
);
}
}
AppState.addEventListener('change', handleAppStateChange);
}, [permissionStatus, permissionsAlertShown]);
My issue is that if I navigate away from my app and come back to it, AppState.currentState changes and since setPermissionsAlertShown(true) is ignored, I am shown the alert again.
How do I handle this situation?
The answer was to create a callback function that will remove the listener. I will share if someone else ever looks for this.
const [permissionStatus, setPermissionStatus] = useState('');
const [permissionsAlertShown, setPermissionsAlertShown] = useState(false);
const handleAppStateChange = useCallback(() => {
if (
AppState.currentState === 'active' &&
permissionStatus === 'denied' &&
!permissionsAlertShown
) {
setPermissionsAlertShown(true);
Alert.alert(
...
);
}
}, [permissionStatus, permissionsAlertShown]);
useEffect(() => {
AppState.addEventListener('change', handleAppStateChange);
return () => AppState.removeEventListener('change', handleAppStateChange);
}, [handleAppStateChange]);
You just need to persist the state, with asyncStorage or some other storage.
The ideal case is to use what persisted of your action (an permission enabled, an API data saved etc).

React - useState doesn't update state on setInterval

The state doesn't update the value even though I'm setting it to the oldvalue + 1.
When logging out the values of ltrNewValue or rtlNewValue it's always the same. It's as it's being overwritten by the initial state.
const Row = (props) => {
const [rowState, setRowState] = useState({
renderInterval: null,
value: 0,
});
useEffect(() => {
const interval = setInterval(counterIntervalFunction, props.speed);
setRowState({ ...rowState, renderInterval: interval });
}, []);
const counterIntervalFunction = () => {
if (props.isRunning && props.direction === 'ltr') {
const ltrNewValue = rowState.value === 2 ? 0 : rowState.value + 1;
console.log(ltrNewValue); // always 1
setRowState({ ...rowState, value: ltrNewValue });
console.log(rowState.value); // always 0
props.setRotatingValue(props.index, rowState.value);
} else if (props.isRunning && props.direction === 'rtl') {
const rtlNewValue = rowState.value === 0 ? 2 : rowState.value - 1;
setRowState({ ...rowState, value: rtlNewValue });
props.setRotatingValue(props.index, rowState.value);
} else {
clearCounterInterval();
}
};
My end goal is to increment the rowState.value up to 2 and then setting it to 0 in a infinite loop. How do I do this correctly?
I'm not certain, but it looks like you have a problem with a stale callback here.
useEffect(() => {
const interval = setInterval(counterIntervalFunction, props.speed);
setRowState({ ...rowState, renderInterval: interval });
}, []);
This effect only runs once - When the component is mounted the first time. It uses the counterIntervalFunction function for the interval:
const counterIntervalFunction = () => {
if (props.isRunning && props.direction === 'ltr') {
const ltrNewValue = rowState.value === 2 ? 0 : rowState.value + 1;
console.log(ltrNewValue); // always 1
setRowState({ ...rowState, value: ltrNewValue });
console.log(rowState.value); // always 0
props.setRotatingValue(props.index, rowState.value);
} else if (props.isRunning && props.direction === 'rtl') {
const rtlNewValue = rowState.value === 0 ? 2 : rowState.value - 1;
setRowState({ ...rowState, value: rtlNewValue });
props.setRotatingValue(props.index, rowState.value);
} else {
clearCounterInterval();
}
};
The counterIntervalFunction captures the reference of props and uses it to determine what to display to the user. However, because this function is only run when the component is mounted, the event will only be run with the props passed to the function originally! You can see an example of this happening in this codesandbox.io
This is why you should put all external dependencies inside of the dependencies array:
useEffect(() => {
const interval = setInterval(counterIntervalFunction, props.speed);
setRowState({ ...rowState, renderInterval: interval });
}, [counterIntervalFunction, props.speed, rowState]);
However, this will cause an infinite loop.
Setting state in useEffect is usually considered a bad idea, because it tends to lead to infinite loops - changing the state will cause the component to re-render, causing another effect to be triggered etc.
Looking at your effect loop, what you're actually interested in is capturing a reference to the interval. This interval won't actually have any impact on the component if it changes, so instead of using state, we can use a ref to keep track of it. Refs don't cause re-renders. This also means we can change value to be a stand-alone value.
Because we now no longer depend on rowState, we can remove that from the dependencies array, preventing an infinite render. Now our effect only depends on props.speed and counterIntervalFunction:
const renderInterval = React.useRef();
const [value, setValue] = React.useState(0);
useEffect(() => {
renderInterval.current = setInterval(counterIntervalFunction, props.speed);
return () => {
cancelInterval(renderInterval.current);
};
}, [props.speed, counterIntervalFunction]);
This will work, but because counterIntervalFunction is defined inline, it will be recreated every render, causing the effect to trigger every render. We can stablize it with React.useCallback(). We'll also want to add all the dependencies of this function to ensure that we don't capture stale references to props and we can change setRowState to setValue. Finally, because the interval is cancelled by useEffect, we don't need to call clearCounterInterval anymore.
const counterIntervalFunction = React.useCallback(() => {
if (props.isRunning && props.direction === 'ltr') {
const ltrNewValue = value === 2 ? 0 : value + 1;
setValue(ltrNewValue);
props.setRotatingValue(props.index, ltrNewValue);
} else if (isRunning && props.direction === 'rtl') {
const rtlNewValue = value === 0 ? 2 : value - 1;
setValue(rtlNewValue);
props.setRotatingValue(props.index, rtlNewValue);
}
}, [value, props]);
This can be simplified even further by moving the required props to the arguments:
const counterIntervalFunction = React.useCallback((isRunning, direction, setRotatingValue, index) => {
if (isRunning === false) {
return;
}
if (direction === 'ltr') {
const ltrNewValue = value === 2 ? 0 : value + 1;
setValue(ltrNewValue);
setRotatingValue(index, ltrNewValue);
} else if (props.direction === 'rtl') {
const rtlNewValue = value === 0 ? 2 : value - 1;
setValue(rtlNewValue);
setRotatingValue(index, rtlNewValue);
}
}, [value]);
This could be even simpler if not for setRotatingValue: Right now, you have a component that both maintains it's own state and tells the parent when its state changes. You should be aware that the component state value might not necessarily update when you call it, but setRotatingValue absolutely will. This may lead to a situation where the parent sees a different state than the child does. I would recommend altering the way your data flows such that it's the parent that owns the current value and passes it via props, not the child.
This gives us the following code to finish off:
function Row = (props) => {
const renderInterval = React.useRef();
const [value, setValue] = React.useState(0);
useEffect(() => {
renderInterval.current = setInterval(counterIntervalFunction, props.isRunning, props.direction, props.setRotatingValue, props.index);
return () => {
cancelInterval(renderInterval.current);
};
}, [props, counterIntervalFunction]);
const counterIntervalFunction = React.useCallback((isRunning, direction, setRotatingValue, index) => {
if (isRunning === false) {
return;
}
if (direction === 'ltr') {
const ltrNewValue = value === 2 ? 0 : value + 1;
setValue(ltrNewValue);
setRotatingValue(index, ltrNewValue);
} else if (props.direction === 'rtl') {
const rtlNewValue = value === 0 ? 2 : value - 1;
setValue(rtlNewValue);
setRotatingValue(index, rtlNewValue);
}
}, [value]);
...
}
In this code, you'll notice that we run the effect every time the props or the function changes. This will mean that, unfortunately, the effect will return every loop, because we need to keep a fresh reference to value. This component will always have this problem unless you refactor counterIntervalFunction to not notify the parent with setRotatingValue or for this function to not contain its own state. An alternatively way we could solve this would be using the function form of setValue:
const counterIntervalFunction = React.useCallback((isRunning, direction, setRotatingValue, index) => {
if (isRunning === false) {
return;
}
setValue(value => {
if (direction === 'ltr') {
return value === 2 ? 0 : value + 1;
} else if (direction ==' rtl') {
return value === 0 ? 2 : value - 1;
}
})
}, []);
Because the state update is not guaranteed to run synchronously, there's no way to extract the value from the setValue call and then call the setRotatingValue function, though. :( You could potentially call setRotatingValue inside of the setValue callback but that gives me the heebie geebies.
It's an interval and it may mess things up when you call setState directly by relying on the old state by the name rowState, try this:
setRowState(oldstate=> { ...rowState, value: oldstate.value+1 });

Resources