Why is it requiring two clicks to execute state change in react? - reactjs

I'm having a real hard time trying to wait for state change. I'm using react hooks and context.
Basically, I have a custom dropdown, no form elements used here. On click, it will run a function to change the state of two parameters:
// IdeaFilters.js
const organizationContext = useOrganization();
const { organization, filterIdeas, ideas, loading } = organizationContext;
const [filter, setFilter] = useState({
statusId: "",
orderBy: "voteCount"
});
// Build the parameters object to send to filterIdeas
const onClick = data => {
setFilter({ ...filter, ...data });
filterIdeas(filter);
};
So the onClick is attached to a dropdown in the render method:
First dropdown:
<Dropdown.Item onClick={() => onClick({ orderBy: "popularityScore" })}>Popularity</Dropdown.Item>
Second dropdown:
<Dropdown.Item key={status.id} onClick={() => onClick({ statusId: status.id })}>
{status.name}
</Dropdown.Item>
fetchIdeas() basically runs a function available in my context, that builds those parameters for my axios request. Every time I click, it requires two clicks for the idea to run. Sometimes it runs as expected, but most of the time it takes two clicks.
Any reason why I'm running into this issue?
Any help will be really appreciated!

Try this
const onClick = data => {
const newFilter = { ...filter, ...data }
setFilter(newFilter)
filterIdeas(newFilter)
}
Instead of
const onClick = data => {
setFilter({ ...filter, ...data })
filterIdeas(filter)
}
In React setState is asynchronous, I guess that was your problem. Try the code above, if it works and you don't fully understand, please comment below I'll edit the answer to explain more clearly. Read my other answer here (yet in the case your problem is asynchronous setState) about how to handle async setState.

Related

Make a query based on a state change

I am making a Pokemon team builder, and am trying to use react-query as I am trying to use the cache features to not make so many requests to the open and public PokeAPI.
I have a dropdown that allows a user to select the name of a Pokemon. When a Pokemon is selected, I want the 'Moves' dropdown to populate with a list of that Pokemons moves for selection.
My initial approach was to use a useEffect hook that depended on the Pokemon Dex Number.
useEffect(() => {
if (pokeDexNum) {
// fetch `https://pokeapi.co/api/v2/pokemon/${pokeDexNum}`
}
}, [pokeDexNum])
I have no clue however how to use this with React-Query. What I want to do is when a user selects a Pokemon, a fetch is made to the PokeAPI to fetch that pokemons available moves, and puts them into the options of the 'Moves' dropdown so that they can select moves.
From the code below you can see that when a Pokemon from the 'Pokemon' dropdown is selected, it updates the const [pokeDexNum, setPokeDexNum] = useState(null); state
<Form>
<Form.Group>
<Form.Dropdown
label="Pokemon"
search
selection
clearable
selectOnBlur={false}
value={pokeDexNum}
onChange={(e, data) => setPokeDexNum(data.value)}
options={[
{ text: "Tepig", value: 498 },
{ text: "Oshawott", value: 501 },
{ text: "Snivy", value: 495 },
]}
/>
<Form.Dropdown label="Moves" search selection options={[]} />
</Form.Group>
</Form>
How would I be able to use react query to fetch depending on whether pokeDexNum is updated
Example of query
const { isLoading, error, data } = useQuery('getPokeMoves', () =>
fetch(`https://pokeapi.co/api/v2/pokemon/${pokeDexNum}`).then(res =>
res.json()
)
)
The above query sometimes fetches even when pokeDexNum is null.
As stated in #Arjun's answer, you need to add the pokeDexNum state to the queryKey array. Think of it as a useEffect's dependency. Any time the pokeDexNum gets updated/changes, a refetch will be triggered for that query.
Since pokeDexNum's initial value is null, you don't want the query to fire before the state is updated. To avoid this, you can use the enabled option in useQuery:
const fetchPokemon = (pokeDexNum) => {
return fetch(`https://pokeapi.co/api/v2/pokemon/${pokeDexNum}`).then((res) =>
res.json()
)
}
const { isLoading, error, data } = useQuery(
['getPokeMoves', pokeDexNum],
() => fetchPokemon(pokeDexNum),
{
enabled: Boolean(pokeDexNum)
}
)
Also, it would be a good idea to add some error handling, I imagine you omitted this for brevity.
Solution: You need to add your pokeDexNum to the queryKey of useQuery.
Here is my suggestion,
define a function to call useQuery. adding your pokeDexNum to the queryKey.
const useGetPokeMoves = (pokeDexNum) => {
return useQuery(
["getPokeMoves", pokeDexNum],
() => {
return fetch(`https://pokeapi.co/api/v2/pokemon/${pokeDexNum}`).then(
(res) => res.json()
);
},
{ keepPreviousData: true }
);
};
Then use it in your component,
const { data } = useGetPokeMoves(pokeDexNum);
Whenever your state changes, the queryKey will also change and the query will be fetched.

How to cancel previous onClick handler that has two dispatch functions with useEffect()?

I saw this https://redux-toolkit.js.org/api/createAsyncThunk#canceling-while-running
but I have two dispatch functions inside an onClick handler.
How can I put them into useEffect()?
I want to achieve when user quickly clicks on another item, the previous fetchFileContentsHandler will be cancelled, and always return the result from the latest invoke.
I tried to wrap fetchFileContentsHandler in useCallback(),
and put fetchFileContentsHandler inside useEffect() with clean up return.
but it resulted in strange behavior.
const fetchFileContentsHandler = () => {
dispatch(fetchFileContents(props.params));
dispatch(
changeSelectedFile({ selectedFile: props.params, selected: true })
);
};
return (
<NavLink to="#" key={fileName}>
<ListItemButton
selected={props.selected}
onClick={fetchFileContentsHandler}
>
<ListItemText
primary={fileName}
/>
</ListItemButton>
</NavLink>
);
};
export default Items;
[update]
I tried to change fetchFileContents to createAsyncThunk and add the suggested useRef() and .abort(), but it didn't work.
If the first fetchFileContents takes longer than the second fetchFileContents, it still updates the htmlReport or textReport value once it's fulfilled.
Then once htmlReport or textReport value changes, I display its contents in another component. So I can still see the first fetched file contents display after the second fetched file contents.
slice.js
export const fetchFileContents = createAsyncThunk(
"ui/getFileContents",
async (fileName) => {
const fileContentsResponse = await fetch(...);
const response = await fileContentsResponse.json();
return {
fileType: fileName.includes("html") ? "text" : "html",
contents: data,
};
}
);
const Slice = createSlice({
name: "somename",
initialState: {
htmlReport: null,
textReport: null,
},
reducers: {
},
extraReducers: (builder) => {
builder
.addCase(fetchFileContents.fulfilled, (state, action) => {
if (action.payload.fileType === "text") {
state.textReport = action.payload.contents;
state.htmlReport = null;
} else if (action.payload.fileType === "html") {
state.textReport = null;
state.htmlReport = action.payload.contents;
}
})
.addCase(fetchFileContents.rejected, (state, action) => {
console.log("[REJECTED]: ");
const { fileName } = action.meta.arg;
const { message } = action.error;
console.log(fileName, message);
});
},
});
Component.js
const fetchFileContentsPromise = useRef();
const fetchFileContentsHandler = () => {
fetchFileContentsPromise.current?.abort();
fetchFileContentsPromise.current = dispatch(
fetchFileContents({ fileName: props.params })
);
dispatch(
changeSelectedFile({ selectedFile: props.params, selected: true })
);
};
[update]
I can't find any major differences between your codes and mine.
Your log was correctly recorded:
undefined
Fetching data with A...
PromiseĀ .....{fileName: 'A'.....
Fetching data with C...
[REJECTED]: A, I canceled it.
Promise resolved: CCC
[FULFILLED]: CCC
Promise resolved: AAA
But my log looks like this:
undefined
Fetching data with A...
undefined
Fetching data with C...
[FULFILLED]: CCC
[Violation] 'setTimeout' handler took 55ms
[FULFILLED]: AAA
or when I repeat:
PromiseĀ .....{fileName: 'A'.....
Fetching data with A...
PromiseĀ .....{fileName: 'C'.....
Fetching data with C...
[FULFILLED]: CCC
[Violation] 'setTimeout' handler took 55ms
[FULFILLED]: AAA
[update -- Solved]
I figured out that I had to declare const fetchFileContentsPromise = useRef(); in the parent component and use props to send it to the child then send it to child's child again.
I use a nested object and pick the necessary values to parent -> child(1) -> child(2).
This fetchFileContents is in child(2).
But I don't understand why.
I would be really appreciated it if anyone could explain.
You're pretty close!
If you scroll down a little bit in the documentation you found, you'll see that it says the dispatch(asyncThunk) will return a promise with an abort method.
In your use case, we only want previous requests to be canceled when the button is click.
useEffect could work, but we'll have to create some extra state to put it in the dependencies array, which is kind of weird.
Another possible option is to use useRef to store those promises, so that we don't have to deal with useEffect. I'm not sure if both of the actions are async thunk, if yes you can try:
const fetchFileContentsPromise = useRef()
const changeSelectedFilePromise = useRef()
const fetchFileContentsHandler = () => {
// Cancel previous requests
fetchFileContentsPromise.current?.abort();
changeSelectedFilePromise.current?.abort();
// Dispatch actions and send new requests
fetchFileContentsPromise.current = dispatch(fetchFileContents(props.params));
changeSelectedFilePromise.current = dispatch(
changeSelectedFile({ selectedFile: props.params, selected: true })
);
}
If changeSelectedFile is not an action created by createAsyncThunk, then it's really not possible to stop that because it's synchronous.
In that case I would suggest you to move the logic in changeSelectedFile into the extra reducer when fetchFileContents is fulfilled, so that we only execute those code when fetchFileContents is done.

Update server-side rendered props dynamically

I've got posts with comments and like to implement a post view that includes all comments to that post.
My getServerSideProps passes the post (including all comments) to my page. Whenever a new comment is written the comments should be dynamically updated, but I'm currently facing some problems with that.
My post view:
const PostView: NextPage = ({ post }) => {
return (
<Layout>
{post.title}
<CommentList initialComments={post.comments} postId={post.id} />
</Layout>
);
};
export default PostView;
export const getServerSideProps = () => {
const post = await getPost(); // returns the post and all its comments
return { props: { post } };
};
The CommentList component:
const CommentList = (initialComments, postId) => {
const { data: comments } = useQuery(["comments", postId], async () => getComments(), { initialData: initialComments);
return (
<>
Comments: {comments.length}
... new comment form ...
... list of comments ...
</>
);
}
The reason why I still want to query comments with react-query is simple: comments should be server-side rendered so that they become seo-relevant, while I want human users to get a dynamic list that can be updated.
When writing new comments I update the QueryClient of react-query by hand:
export const useCreateCommentMutation = (postId: string) => {
const queryClient = useQueryClient();
return useMutation(
["comments", postId],
async (values) =>
await axios.post("/api/comments", values),
{
onSuccess: async res => {
queryClient.setQueryData<CommentWithAuthor[]>(
["comments", postId],
prev => [...(prev || []), res.data],
);
},
},
);
};
This seems to work at first glance; when I check the DOM the comments are included and when writing new comments they dynamically appear.
Unfortunately, when I refresh the page I get the following error:
Text content did not match. Server: "3" Client: "4"
3 (or 4) in this case is the comments.length output.
What am I doing wrong in this case?
Thanks
Edit 1:
I've also tried fixing it by using useEffect:
const [usedComments, setUsedComments] = useState([]);
useEffect(() => {
setUsedComments(comments || initialComments);
}, [comments])
And render usedComments instead - but unfortunately now the comments are no longer part of the DOM.
Why don't you try using useState() hook to store that the Comment data . Every time useQuery runs it will update the state which will cause re-rendering of the comment and also.
I can't think of anything other then this. I don't know what your comment json/data look like to do the server side dynamic rendering.
And useQuery runs on user action like click on add new comment button or a time loop.
and your error seems like It is caused by some Server and client attribute of component.

Get Firestore Data based on User Input

I am creating a React App which will be used to display Dynamic Maps for DnD (vid files). Based on the User Input in a select field, a video player will get the specific video file from the firebase firestore, where I store the video files.
const Streamplayer = ({vidSelection}) => { //vidSelection is the event.target.value from the Select Input Field (basically a String)
const [links, setLinks] = useState([])
console.log(vidSelection);
useEffect(() => {
getLinks()
}, [])
useEffect(() => {
console.log(links)
}, [links])
function getLinks(){
const vidRef = collection(db, 'links');
getDocs(vidRef).then(response => {
console.log(response.docs)
const lnks = response.docs.map(doc => ({
data: doc.data(),
id: doc.id,
}))
setLinks(lnks)
}).catch(error => console.log(error.message));
}
function getVid(){
const toReturn = links.map((link) => link.data.vidSelection);//I want to change whatever gets returned based on input
console.log(toReturn)
return toReturn;
}
return (
<video
controls={false}
autoPlay
muted
loop
className="Video"
key={getVid()}
>
<source src={getVid()} type="video/mp4" />
</video>
)
}
export default Streamplayer
So in the method getVid() I request the data stored in the Firebase Firestore "Links" Collection and I want to change whichever I get based on the userInput. So if the User chooses the map "Grotto", I want to make the call "link.data.grotto". But since vidSelection is a String, I can't simply insert it in this call. Any Ideas how to solve this?
You can make useEffect get re-called when props change, by providing the prop in the dependency list for useEffect. Simply change your first useEffect call to:
useEffect(() => {
getLinks()
}, [vidSelection])
When the selection changes, useEffect will fire and call getLinks, which will update your state.
(Also, minor suggestion: remove the getVid() function, and just set a const videoSource = links.map((link) => link.data.vidSelection) in the function body. Right now you're calling the function twice which will cause the mapping to happen twice, and I think it's generally clearer to have consts flow directly from state, makes it easier to reason about state.)

How to properly implement "subscription"-like fetches with React useEffect

I have a question about the "proper" (or most idiomatic) way to implement network fetch behavior in React based on a single changing property.
A simplified example of the functionality I'm building is below: I am looking to build a multi-page form that "auto-saves" a draft of form inputs as the user navigates back/forth between pages.
TL;DR - I thought useEffect hooks would be the right way to save a draft to the backend every time a url slug prop changes, but I'm running into issues, and wondering about suggestions for the "right" tool for this type of behavior.
Here is my attempt so far. My code is technically working how I want it to, but violates React's recommended hook dependency pattern (and breaks the exhaustive-deps ESLint rule).
import React from 'react';
const ALL_SLUGS = [
'apple',
'banana',
'coconut',
];
function randomUrlSlug() {
return ALL_SLUGS[Math.floor((Math.random() * ALL_SLUGS.length))];
}
// just resovles the same object passed in
const dummySaveDraftToBackend = (input) => {
return new Promise((resolve, _reject) => {
setTimeout(() => {
resolve(input);
}, 1000);
});
};
export function App() {
const [urlSlug, setUrlSlug] = React.useState(randomUrlSlug());
return (
<MyComponent urlSlug={urlSlug} setUrlSlug={setUrlSlug} />
);
}
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
// useCallback memoization is technically unnecessary as written here,
// but if i follow the linter's advice (listing handleSave as a dependency of the useEffect below), it also suggests memoizing here.
// However, complexState is also technically a dependency of this callback memo, which causes the fetch to trigger every time state changes.
//
// Similarly, moving all of this inside the effect hook, makes the hook dependent on `complexState`, which means the call to the backend happens every time a user changes input data.
const handleSave = React.useCallback(() => {
console.log('*** : start fetch');
setLoading(true);
dummySaveDraftToBackend(complexState).then((resp) => {
console.log('fetch response: ', resp);
// to keep this example simple, here we are just updating
// a dummy "responseCount", but in the actual implementation,
// I'm using a state reducer, and want to make some updates to form state based on error handling, backend validation, etc.
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState]);
// I know this triggers on mount and am aware of strategies to prevent that.
// Just leaving that behavior as-is for the simplified example.
React.useEffect(() => {
if (urlSlug) {
handleSave();
}
}, [urlSlug]); // <- React wants me to also include my memoized handleSave function here, whose reference changes every time state changes. If I include it, the fetch fires every time state changes.
return (
<div className="App">
<h2>the current slug is:</h2>
<h3>{urlSlug}</h3>
<div>the current state is:</div>
<pre>{JSON.stringify(complexState, null, 2)}</pre>
<div>
<h2>edit foo</h2>
<input value={complexState.foo} onChange={(e) => setComplexState((s) => ({ ...s, foo: e.target.value }))} disabled={loading} />
</div>
<div>
<h2>edit baz</h2>
<input value={complexState.baz} onChange={(e) => setComplexState((s) => ({ ...s, baz: e.target.value }))} disabled={loading} />
</div>
<div>
<button
type="button"
onClick={() => setUrlSlug(randomUrlSlug())}
disabled={loading}
>
click to change to a random URL slug
</button>
</div>
</div>
);
}
As written, this does what I want it to do, but I had to omit my handleSave function as a dependency of my useEffect to get it to work. If I list handleSave as a dependency, the hook then relies on complexState, which changes (and thus fires the effect) every time the user modifies input.
I'm concerned about violating React's guidance for not including dependencies. As-is, I would also need to manually prevent the effect from running on mount. But because of the warning, I'm wondering if I should not use a useEffect pattern for this, and if there's a better way.
I believe I could also manually read/write state to a ref to accomplish this, but haven't explored that in much depth yet. I have also explored using event listeners on browser popstate events, which is leading me down another rabbit hole of bugginess.
I know that useEffect hooks are typically intended to be used for side effects based on event behavior (e.g. trigger a fetch on a button click). In my use case however, I can't rely solely on user interactions with elements on the page, since I also want to trigger autosave behavior when the user navigates with their browser back/forward controls (I'm using react-router; current version of react-router has hooks for this behavior, but I'm unfortunately locked in to an old version for the project I'm working on).
Through this process, I realized my understanding might be a bit off on proper usage of hook dependencies, and would love some clarity on what the pitfalls of this current implementation could be. Specifically:
In my snippet above, could somebody clarify to me why ignoring the ESLint rule could be "bad"? Specifically, why might ignoring a dependency on some complex state can be problematic, especially since I dont want to trigger an effect when that state changes?
Is there a better pattern I could use here - instead of relying on a useEffect hook - that is more idiomatic? I basically want to implement a subscriber pattern, i.e. "do something every time a prop changes, and ONLY when that prop changes"
If all the "state" that is updated after saving it to backend is only a call count, declare this as a separate chunk of state. This eliminates creating a render loop on complexState.
Use a React ref to cache the current state value and reference the ref in the useEffect callback. This is to separate the concerns of updating the local form state from the action of saving it in the backend on a different schedule.
Ideally each useState hook's "state" should be closely related properties/values. The complexState appears to be your form data that is being saved in the backend while the responseCount is completely unrelated to the actual form data, but rather it is related to how many times the data has been synchronized.
Example:
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow' });
const [responseCount, setResponseCount] = React.useState(0);
const complexStateRef = React.useRef();
React.useEffect(() => {
complexStateRef.current = complexState;
}, [complexState]);
React.useEffect(() => {
const handleSave = async (complexState) => {
console.log('*** : start fetch');
setLoading(true);
try {
const resp = await dummySaveDraftToBackend(complexState);
console.log('fetch response: ', resp);
setResponseCount(count => count + 1);
} catch(error) {
// handle any rejected Promises, errors, etc...
} finally {
setLoading(false);
}
};
if (urlSlug) {
handleSave(complexStateRef.current);
}
}, [urlSlug]);
return (
...
);
}
This feels like a move in the wrong direction (towards more complexity), but introducing an additional state to determine if the urlSlug has changed seems to work.
export function MyComponent({ urlSlug, setUrlSlug }) {
const [slug, setSlug] = React.useState(urlSlug);
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
const handleSave = React.useCallback(() => {
if (urlSlug === slug) return // only when slug changes and not on mount
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState, urlSlug, slug]);
React.useEffect(() => {
if (urlSlug) {
handleSave();
setSlug(urlSlug)
}
}, [urlSlug, handleSave]);
Or move handleSave inside the useEffect (with additional slug check)
Updated with better semantics
export function MyComponent({ urlSlug, setUrlSlug }) {
const [autoSave, setAutoSave] = React.useState(false); // false for not on mount
React.useEffect(() => {
setAutoSave(true)
}, [urlSlug])
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
React.useEffect(() => {
const handleSave = () => {
if(!autoSave) return
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}
if (urlSlug) {
handleSave();
setAutoSave(false)
}
}, [autoSave, complexState]);

Resources