I have a page where I display the data from the API based on an id. I am using React Query to manage the storage of the data. What I am trying to do is when the input with the id is changed I'd like to refetch the data for a different object. I tried to do the following:
const useData = (id: string) => useQuery(
['data', id],
() => axios.get(`/api/data/${id}`),
{
enabled: !!id,
},
);
const Page = () => {
const [id, setID] = useState('1');
const [result, setResult] = useState(useData(id));
useEffect(() => {
setResult(useData(id));
}, [id]);
return (
<div>
{result.data}
<input onChange={(e) => setID(e.target.value)} />
</div>
);
};
But you cannot call hooks inside the useEffect callback. What would be the correct approach for me to reset the result with the data from a new API call?
react-query will automatically refetch if parts of the query key change. So you are on the right track regarding your custom hook, and for your App, it also becomes much simpler:
const useData = (id: string) => useQuery(
['data', id],
() => axios.get(`/api/data/${id}`),
{
enabled: !!id,
},
);
const Page = () => {
const [id, setID] = useState('1');
const result = useData(id);
return (
<div>
{result.data}
<input onChange={(e) => setID(e.target.value)} />
</div>
);
};
that's it. that's all you need.
if id changes, the query key changes, thus giving you a new cache entry, which react-query will fetch for you.
Related
On Click I want to fetch onSnapshot instance of data which startAfter current data. But When the request is sent for the 1st time it returns an empty array. I have to click twice to get new Data.
export default function Page() {
const [data, setData] = useState([])
const [newData, setNewData] = useState([])
const [loading, setLoading] = useState(false)
const [postsEnd, setPostsEnd] = useState(false)
const [postLimit, setPostLimit] = useState(25)
const [lastVisible, setLastVisible] = useState(null)
useEffect(() => {
const collectionRef = collection(firestore, "Page")
const queryResult = query(collectionRef, orderBy("CreatedOn", "desc"), limit(postLimit));
const unsubscribe = onSnapshot(queryResult, (querySnapshot) => {
setLastVisible(querySnapshot.docs[querySnapshot.docs.length-1])
setData(querySnapshot.docs.map(doc => ({
...doc.data(),
id: doc.id,
CreatedOn : doc.data().CreatedOn?.toDate(),
UpdatedOn : doc.data().UpdatedOn?.toDate()
}))
)
});
return unsubscribe;
}, [postLimit])
const ths = (
<tr>
<th>Title</th>
<th>Created</th>
<th>Updated</th>
</tr>
);
const fetchMore = async () => {
setLoading(true)
const collectionRef = collection(firestore, "Page")
const queryResult = query(collectionRef, orderBy("CreatedOn", "desc"), startAfter(lastVisible), limit(postLimit));
onSnapshot(await queryResult, (querySnapshot) => {
setLastVisible(querySnapshot.docs[querySnapshot.docs.length-1])
setNewData(querySnapshot.docs.map(doc => ({
...doc.data(),
id: doc.id,
CreatedOn : doc.data().CreatedOn?.toDate(),
UpdatedOn : doc.data().UpdatedOn?.toDate()
}))
)
});
//Commented If Statement cause data is not fetched completely
//if (newData < postLimit ) { setPostsEnd(true) }
setData(data.concat(newData))
setLoading(false)
console.log(newData)
}
return (
<>
<Table highlightOnHover>
<thead>{ths}</thead>
<tbody>{
data.length > 0
? data.map((element) => (
<tr key={element.id}>
<td>{element.id}</td>
<td>{element.CreatedOn.toString()}</td>
<td>{element.UpdatedOn.toString()}</td>
</tr>
))
: <tr><td>Loading... / No Data to Display</td></tr>
}</tbody>
</Table>
{!postsEnd && !loading ? <Button fullWidth variant="outline" onClick={fetchMore}>Load More</Button> : <></>}
</>
)
}
Current Result
Expected Result
The problem seems to be rooted in React updating the state asynchronously. I saw the same behavior using your code in my Next.js project. When looking at it closely, the onSnapshot() function and your query are working as expected.
You cannot await onSnapshot() (code reference) since it's not an asynchronous function, so the async/await in fetchMore() does not have any effect. Regardless, React state update is asynchronous, so setData(data.concat(newData)) might run before newData is updated. By the second time you load more documents, newData will have the document data.
It looks like fetchMore() can be done without relying on a "newData" state, since it was only used to update the actual data and to hide the "Load More" button:
const fetchMore = () => {
setLoading(true)
const collectionRef = collection(firestore, "page")
const queryResult = query(collectionRef, orderBy("CreatedOn", "desc"), startAfter(lastVisible), limit(postLimit));
onSnapshot(queryResult, (querySnapshot) => {
setLastVisible(querySnapshot.docs[querySnapshot.docs.length - 1]);
//paginatedDocs simply holds the fetched documents
const paginatedDocs = querySnapshot.docs.map(doc => ({
...doc.data(),
id: doc.id,
CreatedOn: doc.data().CreatedOn?.toDate(),
UpdatedOn: doc.data().UpdatedOn?.toDate()
}));
//Replaced usage of newData here with the amount of documents in paginatedDocs
if (paginatedDocs.length < postLimit ) { setPostsEnd(true) }
//Updates the data used in the table directly, rebuilding the component
setData(data.concat(paginatedDocs));
});
setLoading(false);
}
With these changes, there is no problem updating the documents at the first try when clicking the button.
Do you have persistence on? It could be firing once with the local result and again with the server data.
My data from menProducts coming from store component just loops too many. Meaning it duplicates or renders too many when using my filter function. I've read using useEffect can render it only once but I don't know how to trigger its effect.
const [filter, setFilter] = useState('');
const menProducts = useSelector((state) => state.menProducts);
const SearchText = (event) => { <--- this function is for input search bar
setFilter(event.target.value);
}
useEffect(() => { <--- ??
}, []);
let dataSearch = menProducts.filter(id => { <-----Filter function
return Object.keys(id).some(key=>
id[key].toString().toLowerCase().includes(filter.toString().toLowerCase())
)
return (
<main>
{dataSearch.map((menproduct) => (
<ProductMen key={menproduct}/> <---imported <ProductMen/> component is a component that use useDispatch for store reducer and it also displayed products.
))}
</main>
)
}
You don't need to useEffect in this case, you just have to apply the filter at the right time like this:
const [filter, setFilter] = useState("");
const menProducts = useSelector((state) => state.menProducts);
const SearchText = (event) => {
setFilter(event.target.value);
};
return (
<main>
{menProducts
.filter((menproduct) =>
Object.keys(menproduct).some((key) =>
menproduct[key]
.toString()
.toLowerCase()
.includes(filter.toString().toLowerCase())
)
)
.map((menproduct) => (
<ProductMen key={menproduct} />
))}
</main>
);
Demo: https://stackblitz.com/edit/react-6lfqps?file=src/App.js
In the demo i've also included an alternative that use useEffect if you want to take a look at it
Try like this:
const [filter, setFilter] = useState("");
const menProducts = useSelector((state) => state.menProducts);
const searchText = (event) => {
setFilter(event.target.value);
};
useEffect(() => {
const dataSearch = (filter) =>
menProducts.filter((id) => {
// function
});
dataSearch(filter);
}, [filter]);
return (
<main>
{dataSearch.map((menproduct) => (
<ProductMen key={menproduct}/> <---imported <ProductMen/> component is a component that use useDispatch for store reducer and it also displayed products.
))}
</main>
)
To use the useEffect hook you have to pass a function and a dependencies array.
In this case I've used an anonymous function and inside of that I've defined the function dataSearch and on the dependencies array I've just included the filter so each time the filter value changes the useEffect gets executed.
I have a simple useEffect hook in my Task:
const TaskAD = ({ match }: TaskADProps) => {
const { taskName } = match.params;
const [task, setTask] = useState<TaskData | null>(null);
const [loading, setLoading] = useState(true);
const authCommunicator = authRequest();
useEffect(() => {
const getTask = async () => {
const taskData = await authCommunicator
.get(`/task/${taskName}`)
.then((response) => response.data);
setTask(taskData);
setLoading(false);
};
getTask();
}, []);
if (loading || task == null) {
return <Spinner centered />;
}
const updateDescription = async (content: string): Promise<boolean> => {
const r = await authCommunicator
.patch(`/task/${task.name}/`, {
description: content,
})
.then((response) => {
console.log("Setting Task data!");
setTask(response.data);
return true;
})
.catch(() => false);
return r;
};
return (
<ProjectEntity name={taskName}>
<Space direction="vertical" size="small" style={{ width: "100%" }}>
<StatusRow status="Open" />
<TaskDetails task={task} />
<Description content={task.description} onSubmit={updateDescription} />
<Title level={2}>Subtasks:</Title>
<Table dataSource={dataSource} columns={columns} />
</Space>
</ProjectEntity>
);
};
Task object contains a description. The description is another component with a text area. The idea is: when a user changes the description in the child component, the child component has a function (passed via props) to update the description.
So I pass updateDescription to my child component (Description) via props. Both useEffect and updateDescription are in my Task component, the Description component is basically stateless. What happens:
user updates a description
child component calls the function, it updates a record in my DB
it gets the response from the API and calls setTask
task variable is passed to Description via props in Task's render, so they both get updated since the state of parent Task has changed
I see updated description
The only problem is that although it work, but when I do this, I can see this in console:
Setting Task data!
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 a useEffect cleanup function.
(i've added the console.log just to see when it happens).
So I wanted to ask if this is a problem of me having async calls outside useEffect or maybe something else?
#Edit Description code (I removed all the unnecessary junk):
interface DescriptionProps {
content: string;
onSubmit?: (content: string) => Promise<boolean>;
title?: string;
rows?: number;
}
const Description = (props: DescriptionProps) => {
const { content, onSubmit, title, rows } = props;
const [descriptionContent, setDescriptionContent] = useState(content);
const [expanded, setExpanded] = useState(true);
const [editMode, setEditMode] = useState(false);
const [descriptionChanged, setDescriptionChanged] = useState(false);
const editable = onSubmit !== undefined;
const resetDescription = () => {
setDescriptionContent(content);
setDescriptionChanged(false);
};
const changeDescription = (value: string) => {
setDescriptionContent(value);
setDescriptionChanged(true);
};
const descriptionTitle = (
<>
<S.DescriptionTitle>{title}</S.DescriptionTitle>
</>
);
return (
<Collapse
defaultActiveKey={["desc"]}
expandIcon={S.ExpandIcon}
onChange={() => setExpanded(!expanded)}
>
<S.DescriptionHeader header={descriptionTitle} key="desc">
<S.DescriptionContent
onChange={(event): void => changeDescription(event.target.value)}
/>
{descriptionChanged && onSubmit !== undefined ? (
<S.DescriptionEditActions>
<Space size="middle">
<S.SaveIcon
onClick={async () => {
setDescriptionChanged(!(await onSubmit(descriptionContent)));
}}
/>
<S.CancelIcon onClick={() => resetDescription()} />
</Space>
</S.DescriptionEditActions>
) : null}
</S.DescriptionHeader>
</Collapse>
);
};
#Edit2
Funny thing, adding this to my Description solves the issue:
useEffect(
() => () => {
setDescriptionContent("");
},
[content]
);
Can anyone explain why?
Wy state is not updating in another state, I don't know how I can fix this problem.
I wanna do a multiplayer card game with Socket.io, but I run into this problem. Whenever the selectColor state is changing, it's not going to update in the other state. I tried to print the state whenever I click on the component, but the state is just equal as the initial state. Does anybody know how to solve this?
Thanks
const [cards, setCards] = useState([]);
const [id, setId] = useState();
const [color, setColor] = useState("?");
const [selectColor, setSelectColor] = useState(false);
useEffect(() => {
socket.on("id", (id) => setId(id));
socket.on("forceDisconnect", () => setId("Full"));
socket.on("cards", (data) => {
setKarten([
data.map((element) => {
return (
<div
onClick={() =>
console.log(selectColor)
}
key={element}
>
<Card card={element} />
</div>
);
}),
]);
});
socket.on("selectColor", () => {
setSelectColor(true);
console.log("selectColor");
});
}, []);
You have created a closure and placed the value of selectColor in it when your socket.on("cards", etc. ) callback was executed (which is therefore 'frozen in time').
It is no good to create your react elements when your data arrives and store them away in your state. You are supposed to create them when your render function is called. Something like so:
socket.on("cards", (data) => setCards(data));
return (
<>
{ cards.map(
card => (
<div onClick={() => console.log(selectColor)} key={card}>
<Card card={card} />
</div>
)
)}
</>
);
In my app I have profile section with a form. When the component mounts I want to fetch user data from firebase, and display it in the form, with the current values of the user profile. Either using the "value" prop or the "placeholder" prop.
When the user makes changes in the form inputs and submit the changes, I want the database to update and the form to update with the new data.
Currently I can make the database value appear in the form input field, or I can make the form input field empty, but update the database. But not both.
The following code makes the database data render in the form input, but it cant be changed.
I know it could be something with the second useEffect() and the getUserData() function, that I cant seem to figure out.
const UserEdit = (props) => {
const [currentUser, setCurrentUser] = useState('');
const [forening, setForening] = useState('');
useEffect(() => {
firebase_app.auth().onAuthStateChanged(setCurrentUser);
}, [])
const getUserData = async () => {
await dbRef.ref('/' + currentUser.uid + '/profil/' ).once('value', snapshot => {
const value = snapshot.val();
setForening(value)
})
}
useEffect(() => {
getUserData()
},[] )
const handleInput = (event) => {
setForening(event.target.value)
}
const updateUserData = () => {
dbRef.ref('/' + currentUser.uid + '/profil/' ).set({foreningsnavn: forening}, function(error) {
if(error) {
console.log("update failed")
} else {
alert(forening)
}
})
}
const handleClick = () => {
updateUserData()
}
return (
<>
<div className="card-body">
<div className="row">
<div className="col-md-5">
<div className="form-group">
<label className="form-label">{Forening}</label>
<input className="form-control" type="text" value={forening} onChange={handleInput}/>
</div>
</div>
</div>
</div>
</>
)
}
Your second useEffect will run only one time because the second argument array [] of dependencies is empty:
useEffect(() => {
getUserData()
},[] )
You can add foreign dependency to make useEffect run with input change
useEffect(() => {
getUserData()
},[foreign] )
or you can use polling to sync database state