I am using [async-mutex](https://github.com/DirtyHairy/async-mutex because there is a race condition in my code from concurrent requests returning. And upon all of the concurrent requests resolving, they each need to add something from the response into a state array.
I have included a codesandbox replicating this issue: https://codesandbox.io/s/runtime-haze-2407uy
I will also post the code here for reference:
import Uppy from "#uppy/core";
import XHRUpload from "#uppy/xhr-upload";
import { DragDrop, ProgressBar } from "#uppy/react";
import { Mutex } from "async-mutex";
import { useEffect, useMemo, useRef, useState } from "react";
export default function App() {
const [stuff, setStuff] = useState([]);
const uppy = new Uppy({
meta: { type: "file" },
autoProceed: true
});
uppy.use(XHRUpload, {
endpoint: `google.com/upload`
});
const mutex = new Mutex();
uppy.on("upload-error", (_, response) => {
mutex.acquire().then((release) => {
let joined = stuff.concat("test");
setStuff(joined);
console.log(stuff);
release();
});
});
return (
<div className="App">
<DragDrop
uppy={uppy}
locale={{
strings: {
// Text to show on the droppable area.
// `%{browse}` is replaced with a link that opens the system file selection dialog.
dropHereOr: "Drop here or %{browse}",
// Used as the label for the link that opens the system file selection dialog.
browse: "browse"
}
}}
/>
</div>
);
}
I expect, when uploading two files (the upload server is bogus but that is intended, because all requests (one per file) will trigger the upload-error event) that the stuff array will end up like ['test', 'test']. However, that does not happen:
The reason for this is because the "state" (no pun intended) of the stuff state is uncertain at the time of running setState, due to the nature of setStuff and state setters in general being asynchronous.
The solution is to
a) use await because in any case the mutex lock acquisition is a promise
b) pass a lambda function into setStuff that guarantees the state of stuff will be up to date, as opposed to just assuming stuff will be up to date (which it won't be)
uppy.on('upload-error', async (_, response) => {
await mutex.acquire().then((release) => {
setStuff((prevState => {
return prevState.concat('test');
}));
release();
});
})
For more information, check out https://stackoverflow.com/a/44726537/8652920
Related
I am trying to refetch a specific data, which should update after a post request in another API. It's worth mentioning that based on that request two APIs should update and one of them updates, another one not.
I've tried multiple ways to refetch the API based on successful post request, but nothing seems to work.
I tried to also add 2nd option like this, but without success.
{
refetchInactive: true,
refetchActive: true,
}
As it mentioned in this discussion,
maybe "keys are not matching, so you are trying to invalidate something that doesn't exist in the cache.", but not sure that it's my case.
What is more interesting is that clicking the invalidate button in the devtools, the invalidation per se works.
Mutation function:
import { BASE_URL, product1Url, product2Url } from './services/api'
import axios from 'axios'
import { useMutation, useQueryClient } from 'react-query'
export const usePostProduct3 = () => {
const queryClient = useQueryClient()
const mutation = useMutation(
async (data) => {
const url = `${BASE_URL}/product3`
return axios.post(url, data).then((response) => response)
},
{
onSuccess: async (data) => {
return (
await queryClient.invalidateQueries(['prodKey', product1Url]), //this one doesn't work
queryClient.invalidateQueries(['prodKey', product2Url]) //this api refetch/update works
) },
},
)
return mutation
}
What am I doing wrong?
You shold do it like this
onSuccess:(response)=>{
queryClient.setQueryData("your query key",response.data)
}
Or if you just want to invalidate queries
import {querCache} from "react-query"
...
onSuccess:(response)=>{
queryClient.invalidateQueries("your query key")
queryClient.invalidateQueries("your query key")
//you also can refetch data like this
queryCache.refetchQueries("your query key")
}
I am still not as fluent in ReactJS, Ant Design, Sagas and Reducers as I thought I am: I am trying to maintain/ debug a single-page app and I am not sure which files I have to adjust and how.
The goal is to add the following <select> in their render() function within component.tsx:
<Select showSearch
style={{ width: 150 }}
placeholder="Select entity"
// onChange={(e) => { this.handleSelectorChange("Entity", e) }} // to tackle later
// value={this.props.selectedEntity.toString()} // ------ " ------
>
<Option value="0">Option 0</Option>
{this.props.availableEntities.map((myEntity) =>
(<Option key={myEntity.Id.toString()}
value={myEntity.Id.toString()}>{myEntity.Name}</Option>))}
</Select>
In the respective container.tsx, I added:
const mapStateToProps = (state ) => ({
availableEntities: GeneralSelectors.getAvailableEntities(state),
});
What I get back is the following is only Option 0 instead of all entities (and even on a wrong place), see screenshot
How do I get my selection options dynamically generated using data from an API? Since I do not see backend being called (using the Network tab of my Chrome debugger), I assume something is wrong with the sagas, but that is just an hypothesis.
Background info
For another single-page-app (SPA) of the same project which already has that <select> inside, I found a urls.ts which looks as follows
import UrlMap from "../UrlMap"
import * as Actions from "./actions"
import * as GeneralActions from "../general/actions"
export const urls = [
new UrlMap("myWorkingSPA",
() => [
Actions.load(),
GeneralActions.loadEntities(),
Actions.loadDifferencePositions()
])
];
I don't know where this file is actually called, and what I have to modify where in order to include it.
In the respective actions.ts of the running SPA I find
export function loadEntities() : Action {
return {
type: LOAD_ENTITIES
}
}
I am not sure whether my SPA is also taking this actions.ts or not.
The sagas.ts is the same for the running SPA and for my component:
function* loadEntities() {
const url = config.apiBaseUrl + "/api/Entities";
try {
const response: Response = yield fetch(url);
const json: Interfaces.Entity[] = yield response.json();
yield put(Actions.setEntities(json));
} catch (e) {
notification.error({ message: "Error", description: "Error loading Entities" });
}
}
function* watchLoadEntities() {
yield takeEvery(Actions.LOAD_ENTITIES, loadEntities);
}
Hope that was not too much information, but I guess it is all related to the problem.
References
populate select option in reactjs
Mapping checkboxes inside checkboxes ReactJS
Eventually I also found another sagas.ts which was also in effect:
function* parseUrl({ payload }) {
const pathName = payload.location.pathname;
const myComponentURI: match<{}> = matchPath(pathName, { path: config.uiBaseUrl + "myComponent" });
if (myComponentURI) {
yield put(GeneralActions.loadEntities()); // <---
}
// ... here are many more const. for various different pages
}
The GeneralActions refers to an actions.ts in a directory general.
I solved the issue by putting the marked line. Warning: I believe, this is not the way things should be implemented, I am just saying that it hot-fixed my issue.
As clafou pointed out in a comment, the clean way would be to trigger the yield on startup.
<script lang="ts">
import { createComponent } from "#vue/composition-api";
import { SplashPage } from "../../lib/vue-viewmodels";
export default createComponent({
async setup(props, context) {
await SplashPage.init(2000, context.root.$router, "plan", "login");
}
});
</script>
ERROR: "setup" must return a "Object" or a "Function", got "Promise"
The setup function must be synchronous can be async using Suspense.
How to avoid using async setup (obsolete answer)
An onMounted hook can be used with an async callback:
import { onMounted } from "#vue/composition-api";
// …
export default createComponent({
setup(props, context) {
onMounted(async () => {
await SplashPage.init(2000, context.root.$router, "plan", "login");
)};
}
});
Or, it is always possible to call an asynchronous function without to await it:
SplashPage.init(2000, context.root.$router, "plan", "login")
.catch(console.log);
In both cases, you'll have to take into account that the component will be rendered before the execution of the asynchronous function. A simple way to not display something that would depends on it is to use v-if in your template.
I have another solution that works in my use-case. Maybe it will help. It is a bit like the lifestyle hook approach, but without needing it. It also doesn't need the <Suspense> tag which was "overkill" in my use-case.
The idea is to return a default value (the empty array in this case, but could be a "Loading..." splash page). Then, after the async has resolved, update the reactive prop (menuItems array here, but it could be the actual splash page contents or html or whatever).
I know this might not suit all use-cases but it is another approach that works.
Simplified code:
setup () {
const menuItems = ref([])
const buildMenuItems = async () => {
// eg get items from server, return an array of formatted items...
}
/* setTimeout(async () => {
menuItems.value = await buildMenuItems()
}, 0) */
// or..
;(async () => {
menuItems.value = await buildMenuItems()
})()
return {
menuItems
}
}
I tested it by making buildMenuItems() take 2 seconds and it all works fine.
EDIT: And then I discovered other ways (even for non TypeScript): How can I use async/await in the Vue 3.0 setup() function using Typescript
Cheers,
Murray
On my ReactJS App I used setTimeout to defer some Redux action:
export const doLockSide = (lockObject) => (dispatch) => {
const timerId = setTimeout(() => {
dispatch({
type: CONSTANTS.TOPICS_SET_CURRENT_TOPIC_LOCKED_SIDE,
payload: { id: lockObject.topicId, side: lockObject.side, locked: false }
});
}, lockObject.unlockTimeout);
dispatch({
type: CONSTANTS.TOPICS_SET_CURRENT_TOPIC_LOCKED_SIDE,
payload: { id: lockObject.topicId, side: lockObject.side, timerId, locked: true }
});
};
The lockObject comes from the server, so this code is a part of async Redux actions chain. It worked fine, but when I tried to make this functionality to be a part of server side rendering process, it broke the App. I understand the difference between Browser and NodeJS runtime environments and the difference between its implementations of setTimeout. Specifically my timerId could not be processed by Node due to it's an object, while my Redux reducer treats it as an integer. But the main problem is that during server side rendering Node fires setTimeout callback on the server side...
The question. I have some redux-based proccess that should be deferred in some cases including the App start. How can I do it satisfying the requirement of server-side rendering?
After some research I was able to apply the following approach.
1) Push the deferred action data into some special storage in case of server-side rendering, and run it "as is" in case of Browser:
import { _postRender } from '../utils/misc';
const doLockSideUI = (dispatch, lockObject) => {
// the body of previous version of doLockSide inner function
const timerId = setTimeout(() => {/*...*/}, lockObject.unlockTimeout);
dispatch(/*...*/);
};
export const doLockSide = (lockObject) => (dispatch) => {
if(typeof window === 'undefined') { // server-side rendering case
_postRender.actions.push({
name: 'doLockSide',
params: lockObject
});
}
else { // Browser case
doLockSideUI(dispatch, lockObject);
}
};
Where utils/misc.js has the following entity:
// to run actions on the Client after server-side rendering
export const _postRender = { actions: [] };
2) On the server I've imported that _postRender object form utils/misc.js and pushed it to render parameters when all redux-store data dependencies had been resolved:
const markup = renderToString(/*...*/);
const finalState = store.getState();
const params = { markup, finalState, postRender: { ..._postRender } };
_postRender.actions = []; // need to reset post-render actions
return res.status(status).render('index', params);
_postRender.actions has to be cleaned up, otherwise _postRender.actions.push from p.1 will populate it again and again each time the Client had been reloaded.
3) Then I provided my post-render actions the same way as it is done for preloaded state. In my case it is index.ejs template:
<div id="main"><%- markup %></div>
<script>
var __PRELOADED_STATE__ = <%- JSON.stringify(finalState) %>;
var __POST_RENDER__ = <%- JSON.stringify(postRender) %>;
</script>
4) Now I need to call my __POST_RENDER__ actions with given params. For this purpose I updated my root component's did-mount hook and dispatch an additional action which handles the post-render action list:
componentDidMount() {
console.log('The App has been run successfully');
if(window.__POST_RENDER__ && window.__POST_RENDER__.actions.length) {
this.props.dispatch(runAfterRender(window.__POST_RENDER__.actions));
}
}
Where runAfterRender is a new action that is being imported from ../actions/render:
import { doLockSide } from './topic'
export const runAfterRender = (list) => (dispatch) => {
list.forEach(action => {
if(action.name === 'doLockSide') {
dispatch(doLockSide(action.params));
}
// other actions?
});
};
As you can see, it's just a draft and I was forced to import doLockSide action from p.1 and call it explicitly. I guess there may be a list of possible actions that could be called on the Client after server-side rendering, but this approach already works. I wonder if there is a better way...
I would like to add a loading animation to my website since it's loading quite a bit when entering the website. It is built in ReactJS & NodeJS, so I need to know specifically with ReactJS how to add a loading animation when initially entering the site and also when there is any loading time when rendering a new component.
So is there a way to let people on my website already, although it's not fully loaded, so I can add a loading page with some CSS3 animation as a loading screen.
The question is not really how to make a loading animation. It's more about how to integrate it into ReactJS.
Thank you very much.
Since ReactJS virtual DOM is pretty fast, I assume the biggest load time is due to asynchronous calls. You might be running async code in one of the React lifecycle event (e.g. componentWillMount).
Your application looks empty in the time that it takes for the HTTP call. To create a loader you need to keep the state of your async code.
Example without using Redux
We will have three different states in our app:
REQUEST: while the data is requested but has not loaded yet.
SUCCESS: The data returned successfully. No error occurred.
FAILURE: The async code failed with an error.
While we are in the request state we need to render the spinner. Once the data is back from the server, we change the state of the app to SUCCESS which trigger the component re-render, in which we render the listings.
import React from 'react'
import axios from 'axios'
const REQUEST = 'REQUEST'
const SUCCESS = 'SUCCESS'
const FAILURE = 'FAILURE'
export default class Listings extends React.Component {
constructor(props) {
super(props)
this.state = {status: REQUEST, listings: []}
}
componentDidMount() {
axios.get('/api/listing/12345')
.then(function (response) {
this.setState({listing: response.payload, status: SUCCESS})
})
.catch(function (error) {
this.setState({listing: [], status: FAILURE})
})
}
renderSpinner() {
return ('Loading...')
}
renderListing(listing, idx) {
return (
<div key={idx}>
{listing.name}
</div>
)
}
renderListings() {
return this.state.listing.map(this.renderListing)
}
render() {
return this.state.status == REQUEST ? this.renderSpinner() : this.renderListings()
}
}
Example using Redux
You can pretty much do the similar thing using Redux and Thunk middleware.
Thunk middleware allows us to send actions that are functions. Therefore, it allows us to run an async code. Here we are doing the same thing that we did in the previous example: we keep track of the state of asynchronous code.
export default function promiseMiddleware() {
return (next) => (action) => {
const {promise, type, ...rest} = action
if (!promise) return next(action)
const REQUEST = type + '_REQUEST'
const SUCCESS = type + '_SUCCESS'
const FAILURE = type + '_FAILURE'
next({...rest, type: REQUEST})
return promise
.then(result => {
next({...rest, result, type: SUCCESS})
return true
})
.catch(error => {
if (DEBUG) {
console.error(error)
console.log(error.stack)
}
next({...rest, error, type: FAILURE})
return false
})
}
}