React 18: Executing Code When Suspense Completes Promise - reactjs

I want to be able to make my own state change when when the return from useTransition completes. That is, currently, in the example below, isPending toggles from true to false, but that's not enough. I want to be able to execute code on that change.
That is, in the example below, I would like to possible do something similar to what useEffect does, and have the code that is returned from the startTransition execute when the pending event changes, but that does not work.
Any other options?
onClick={() => {
startTransition(() => {
const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId));
return () => { DO THIS CODE WHEN PENDING BECOMES FALSE }
});
}}
function App() {
const [resource, setResource] = useState(initialResource);
const [startTransition, isPending] = useTransition({
timeoutMs: 3000
});
return (
<>
<button
disabled={isPending}
onClick={() => {
startTransition(() => {
const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId));
});
}}
>
Next
</button>
{isPending ? " Loading..." : null}
<ProfilePage resource={resource} />
</>
);
}

It's is not possible within the startTransition call / useTransition config. You can use useEffect/useLayoutEffect to run some code after resource update.
useEffect/useLayoutEffect(() => {
// some code after resource state update
}, [resource])

Related

how to use useState like this? what does it mean?

Can anyone please explain me what const rerender = React.useState(0)[1] is this?
import React from 'react'
import axios from 'axios'
import {
useQuery,
useQueryClient,
QueryClient,
QueryClientProvider,
} from "#tanstack/react-query"
import { ReactQueryDevtools } from "#tanstack/react-query-devtools"
const getCharacters = async () => {
await new Promise((r) => setTimeout(r, 500))
const { data } = await axios.get('https://rickandmortyapi.com/api/character/')
return data
}
const getCharacter = async (selectedChar) => {
await new Promise((r) => setTimeout(r, 500))
const { data } = await axios.get(
`https://rickandmortyapi.com/api/character/${selectedChar}`,
)
return data
}
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
function Example() {
const queryClient = useQueryClient()
**const rerender = React.useState(0)[1]**
const [selectedChar, setSelectedChar] = React.useState(1)
const charactersQuery = useQuery(['characters'], getCharacters)
const characterQuery = useQuery(['character', selectedChar], () =>
getCharacter(selectedChar),
)
return (
<div className="App">
<p>
Hovering over a character will prefetch it, and when it's been
prefetched it will turn <strong>bold</strong>. Clicking on a prefetched
character will show their stats below immediately.
</p>
<h2>Characters</h2>
{charactersQuery.isLoading ? (
'Loading...'
) : (
<>
<ul>
{charactersQuery.data?.results.map((char) => (
<li
key={char.id}
onClick={() => {
setSelectedChar(char.id)
}}
onMouseEnter={async () => {
await queryClient.prefetchQuery(
['character', char.id],
() => getCharacter(char.id),
{
staleTime: 10 * 1000, // only prefetch if older than 10 seconds
},
)
setTimeout(() => {
**rerender({})**
}, 1)
}}
>
<div
style={
queryClient.getQueryData(['character', char.id])
? {
fontWeight: 'bold',
}
: {}
}
>
{char.id} - {char.name}
</div>
</li>
))}
</ul>
<h3>Selected Character</h3>
{characterQuery.isLoading ? (
'Loading...'
) : (
<>
<pre>{JSON.stringify(characterQuery.data, null, 2)}</pre>
</>
)}
<ReactQueryDevtools initialIsOpen />
</>
)}
</div>
)
}
I want to know what it means, i am unable to understand this useState syntax I never seen this type of syntax. Can anyone share something about this?
useState has two parts the value and a function to update the value.
Take a look at the below snippet which explains how the useState hook is assigning values normally.
var fruitStateVariable = useState('banana'); // Returns a pair
var fruit = fruitStateVariable[0]; // The value of the state
var setFruit = fruitStateVariable[1]; // A asynchronous function to update the state.
By accessing the item with index 1 you are assigning rerender a function to update the state, which will trigger a rerender, as React sees the value going from 0 -> {}.
As to why the code is doing this, it seems the author is trying to get around useState being asynchronous to update.
This is not a good pattern and should be avoided!. As you rightly have said you have not seen this syntax before, because it is not an appropriate way of triggering a function to rerender.
React Docs recommends this way if you really need to force the retrigger:
const forceUpdate = useReducer(x => x + 1, 0)[1]
But a key line from this resource is to Try to avoid this pattern if possible.

Why does my state affect the emitted events from a click handler?

I have (what seems to be) a very peculiar situation where I seem to be getting extra events emitted based on my Redux state.
I have narrowed the behavior down to whether or not I make a successful request to my /users endpoint and retrieve a list of users which is then stored in Redux.
If the commented code is not active (as it is currently shown), I am able to successfully render the modal(s) reliably and step between states.
If the commented code is active, the (which is what is behind the as well) emits an onDismiss call immediately. This has the result of closing the modal immediately.
If the commented code is active, but the response from the thunk is a 401 and the user data is not loaded (i.e., the state of the user key in redux is a failure, not success, then the modal works -- though of course, there are no users to select.
I have confirmed this behavior is consistent no matter where I seem to make this fetch request (initially it was in the App.tsx to be called immediately. I also tried it in an intermediate component).
Question(s):
Can you explain why I might be getting different behavior in my click handlers based on what is in my state?
Is there something I'm missing and I'm conflating my Redux state with the actual behavior?
I know I can solve this by adding a event.stopPropagation() call in strategic places (e.g., on the first button that opens the <ConfirmationBox> and then again on the button in the <ConfirmationBox> that transitions to the SelectUser modal), but are there other solutions?
//pinFlow.tsx
type States =
| { state: 'Confirm' }
| { state: 'SelectUser' }
| { state: 'SubmitPin'; user: User };
export function pinFlow<T extends ConfirmationBoxProps>(
ConfirmationBox: React.FC<T>,
authorization: Authorization,
) {
const [state, setState] = React.useState<States>({ state: 'Confirm' });
// const dispatch=useDispatch();
// initialize users
// const users = useSelector((state: InitialState) => state.pinAuth.users);
// const fetchUsers = useCallback(() => {
// dispatch(fetchUsersThunk());
// }, [dispatch]);
// useEffect(() => {
// if (users.state === RemoteDataState.NotStarted) {
// fetchUsers();
// }
// }, [fetchUsers, users.state]);
return (props: T) => {
const users = useSelector((state: InitialState) =>
mapRemoteData(state.pinAuth.users, users =>
users.filter(user => user.authorizations.includes(authorization)),
),
);
switch (state.state) {
case 'Confirm': {
return (
<ConfirmationBox
{...props}
onSubmit={(_event: React.MouseEvent) => {
setState({ state: 'SelectUser' });
}}
/>
);
}
case 'SelectUser': {
return (
<Modal
title={'PIN Required'}
canClickOutsideToDismiss={true}
onDismiss={() => {
setState({ state: 'Confirm' });
}}
>
<p className={style.selectProfileText}>Select your profile:</p>
<pre>
<code>{JSON.stringify(users, null, 4)}</code>
</pre>
{/*
<UserList users={users.data} /> */}
</Modal>
);
}
default: {
return <Modal title="others">all others</Modal>;
}
}
};
}
The code is used in another component like so:
function Comp(){
const [selected, setSelected] = useState();
const [mode, setMode] = useState();
const ConfirmationModal =
protected
? pinFlow(MenuItemModal, permission)
: MenuItemModal;
return(
<ConfirmationModal
item={selected}
mode={mode}
disabled={availability.state === RemoteDataState.Loading}
errorMessage={tryGetError(availability)}
onCancel={() => {
setMode(undefined);
dispatch(resetAvailability());
}}
onSubmit={(accessToken: string) => {
dispatch(findAction(selected, mode, accessToken));
}}
/>
)
}

How to avoid Can't perform a React state update error when i use ternary operator

I got an error :
index.js:1 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
And I have been trying to find what makes that error and I found the thing that makes an error.
so I tried to search how to avoid this error in this case. but I couldn't find it.
so The problem is when I upload the csv file and then the file contains info state.
so I show this file information on my website.
And when the file is uploaded then the component is changing
So I used it with the ternary operator. So I tried to remove the ternary operator then the error had disappeared I assumed that it made the error .
So I'm trying to fix it but I can't figure it out
here is my code :
const CsvShowData = ({ info, setInfo }) => {
return (
//
<>
{info.length !== 0 ? (
<DataTable>
{info.slice(0, 1).map(inf => (
<MainRow key={inf}>
{inf.map((d, index) => (
<Row key={index}>
<div className="titleRow">
<h3>{d}</h3>
</div>
</Row>
))}
</MainRow>
))}
{info.slice(1, 10).map((a, key) => (
<MainRow key={key}>
{a.map((b, idx) => (
<Row key={idx}>
<div className="sideRow">
<p>{b}</p>
</div>
</Row>
))}
</MainRow>
))}
</DataTable>
) : (
<CsvTable>
<CsvFileReader info={info} setInfo={setInfo} />
</CsvTable>
)}
</>
);
};
Thank you in advance!
CsvFileReader Component
const CsvFileReader = ({ setInfo }) => {
const handleOnDrop = data => {
const infos = data.map(item => item.data);
setTimeout(() => setInfo([...infos]), 1000); // save timeout ref
};
const handleOnError = (err, file, inputElem, reason) => {
console.log(err);
};
const handleOnRemoveFile = data => {
console.log(data);
};
return (
<>
<MainReader>
<CSVReader
onDrop={handleOnDrop}
onError={handleOnError}
config={
(({ fastMode: true }, { chunk: "LocalChunkSize" }),
{ header: false })
}
addRemoveButton
onRemoveFile={handleOnRemoveFile}
>
You should use a ref to save setTimeout and remove setInfo when component is unmounted.
const ref = useRef();
const handleOnDrop = (data) => {
const infos = data.map((item) => item.data);
ref.current = setTimeout(() => setInfo([...infos]), 1000); // save timeout ref
};
useEffect(() => {
return () => {
if (ref.current) {
clearTimeout(ref.current);
}
};
});

Mocking the fetching state of URQL in Jest

I'm trying to mock the fetching state in my tests. The state with data and state with an error were successfully mocked. Below you can find an example:
const createMenuWithGraphQL = (graphqlData: MockGraphQLResponse): [JSX.Element, MockGraphQLClient] => {
const mockGraphQLClient = {
executeQuery: jest.fn(() => fromValue(graphqlData)),
executeMutation: jest.fn(() => never),
executeSubscription: jest.fn(() => never),
};
return [
<GraphQLProvider key="provider" value={mockGraphQLClient}>
<Menu {...menuConfig} />
</GraphQLProvider>,
mockGraphQLClient,
];
};
it('displays submenu with section in loading state after clicking on an item with children', async () => {
const [Component, client] = createMenuWithGraphQL({
fetching: true,
error: false,
});
const { container } = render(Component);
const itemConfig = menuConfig.links[0];
fireEvent.click(screen.getByText(label));
await waitFor(() => {
const alertItem = screen.getByRole('alert');
expect(client.executeQuery).toBeCalledTimes(1);
expect(container).toContainElement(alertItem);
expect(alertItem).toHaveAttribute('aria-busy', 'false');
});
});
The component looks like the following:
export const Component: FC<Props> = ({ label, identifier }) => {
const [result] = useQuery({ query });
return (
<div role="group">
{result.fetching && (
<p role="alert" aria-busy={true}>
Loading
</p>
)}
{result.error && (
<p role="alert" aria-busy={false}>
Error
</p>
)}
{!result.fetching && !result.error && <p>Has data</p>}
</div>
);
};
I have no idea what am I doing wrong. When I run the test it says the fetching is false. Any help would be appreciated.
maintainer here,
I think the issue here is that you are synchronously returning in executeQuery. Let's look at what happens.
Your component mounts and checks whether or not there is synchronous state in cache.
This is the case since we only do executeQuery: jest.fn(() => fromValue(graphqlData)),
We can see that this actually comes down to the same result as if the result would just come out of cache (we never need to hit the API so we never hit fetching).
We could solve this by adding a setTimeout, ... but Wonka (the streaming library used by urql) has built-in solutions for this.
We can utilise the delay operator to simulate a fetching state:
const mockGraphQLClient = {
executeQuery: jest.fn(() => pipe(fromValue(data), delay(100)),
};
Now we can check the fetching state correctly in the first 100ms and after that we are able to utilise waitFor to check the subsequent state.

Why does my code in React JS goes to endless loop satisfying both conditions?

import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { Redirect } from "react-router-dom";
import firebase from '../../config/firebaseConfig'
import SingleEventSummary from './SingleEventSummary'
import { getEvent } from "./../../store/actions/eventActions";
import "./SingleEvent.css";
const SingleEvent = props => {
const id = props.match.params.id;
const [eventItem, seteventItem] = useState([]);
const [isFavourite, setIsFavourite] = useState("no");
//getting specific event
useEffect(() => {
const db = firebase.firestore().collection('newEvents').doc(id)
db.onSnapshot(snapshot => {
seteventItem(snapshot.data())
})
}, [id])
//checking if there is favourite
useEffect(() => {
const db = firebase.firestore().collection('users').doc(props.auth.uid)
db.get().then(snapshot => {
const data = snapshot.data()
const faves = data && snapshot.data().favorites || []
faves.includes(id) ? setIsFavourite("yes") : setIsFavourite("no")
},(error) => console.error(error))
},[isFavourite])
//setting as favourites
const favouriteClick = (uid, eid) => {
debugger;
let initFav = firebase.firestore().collection('users').doc(uid);
initFav.get().then(snapshot => {
const arrayUnion = firebase.firestore.FieldValue.arrayUnion(eid);
initFav.update({
favorites: arrayUnion,
});
setIsFavourite("yes")
},(error) => console.error(error))
}
//remove favourites
const removeFavourite = () => {
debugger;
const initFavo = firebase.firestore().collection('users').doc(props.auth.uid);
initFavo.get().then(snapshot => {
if (snapshot.data().favorites) {
if (snapshot.data().favorites.includes(id)) {
let data = snapshot.data().favorites.filter(el => el != id )
initFavo.update({
favorites: data,
});
setIsFavourite("no")
}
}
},(error) => console.error(error))
return () => initFavo()
}
console.log("wtf is this shit", isFavourite)
if (isFavourite == "no") {
return (
<main className="single-event_main">
<a className="waves-effect waves-light btn" onClick={favouriteClick(props.auth.uid, id)}>Add As Favourites!!</a>
</main>
);
}
else {
return (
<main className="single-event_main">
<div className="row">
<div className="col s6">
<a className="waves-effect waves-light btn" disabled>Favourite Event!!</a>
</div>
<div className="col s6">
<a className="waves-effect waves-light btn" onClick={removeFavourite}>Remove From Favourites!!</a>
</div>
</div>
</main>
);
}
};
export default SingleEvent;
I am trying to set the value in hook, comparing if the event id exists in the user's database(if he/she has set that event as a favourite).
....
const [isFavourite, setIsFavourite] = useState("no");
//checking if there is favourite
useEffect(() => {
debugger;
const db = firebase.firestore().collection('users').doc(props.auth.uid)
db.onSnapshot(snapshot => {
debugger;
if(snapshot.data()) {
if (snapshot.data().favorites) {
if (snapshot.data().favorites.includes(id)) {
setIsFavourite("yes")
}
else if(!snapshot.data().favorites.includes(id)){
setIsFavourite("no")
}
}
}
}, (error) => console.error(error));
return () => db()
},[])
....
The issue is, react goes inside both conditions endlessly setting the hook value both yes and no. Been stuck on this hours.
Any kind of help will be much appreciated!!!
jus offering a little refactor -> this is just a bit easier to read
useEffect(() => {
const db = firebase.firestore().collection('users').doc(props.auth.uid)
db.onSnapshot(snapshot => {
const data = snapshot.data()
const faves = data && snapshot.data().favorites || []
faves.includes(id) ? setIsFavourite("yes") : setIsFavourite("no")
},(error) => console.error(error))
return () => db()
},[])
I can't see why your code would be looping perhaps we need more code as the above commenter mentioned.
Ok now that you have shown us more code. I can say with a large degree of confidence it is because you are calling favouriteClick in the onClick of the "Add As Favourites" button.
Which is causing a weird loop.
Change
onClick={favouriteClick(props.auth.uid, id)
to
onClick={() => favouriteClick(props.auth.uid, id)
You are welcome!
you should have a stop condition for this hook, useEffect hook is triggered every time you render something, so you ending up changing props and rendering and then trigger useEffect which change props and trigger render lifecycle Hook.
You should have something like that
useEffect(() => {
// call database
},[setFavorite]) // here goes stop condition for useEffect
If setFavorite is still false it won't trigger trigger again, or if setFavorite is false and request from db is setting it to true then next time if you it's trigger useEffect again and setFavorite is still true then useEffect won't execute.
For more details read officials documentation https://reactjs.org/docs/hooks-effect.html
If you are updating firebase in setIsFavourite() then you are creating a change that will be picked up by the .onSnapshot listener. This will force an endless loop of : condition triggers change > listens for condition > condition triggers change > ad Infinitum.
You either need to switch from .onSnapshot to a one-off .get listener or you need to add a condition to prevent changes from propagating in this case. This custom condition will be specific to your implementation, not really something we can help with unless you show use more code and help us understand what you are trying to achieve (that should be a separate question though). So I suspect the former in this case.

Resources