Multiple state changes in event listener, how to NOT batch the DOM updates? - reactjs

I'm building a component to test the performance of different algorithms. The algorithms return the ms they took to run and this is want I want to display. The "fastAlgorithm" takes about half a second, and the "slowAlgorithm" takes around 5 seconds.
My problem is that the UI is not re-rendered with the result until both algorithms have finished. I would like to display the result for the fast algorithm as soon as it finishes, and the slow algorithm when that one finishes.
I've read about how React batches updates before re-rendering, but is there someway to change this behavior? Or is there a better way to organize my component/s to achieve what I want?
I'm using react 16.13.1
Here is my component:
import { useState } from 'react'
import { fastAlgorithm, slowAlgorithm } from '../utils/algorithms'
const PerformanceTest = () => {
const [slowResult, setSlowResult] = useState(false)
const [fastResult, setFastResult] = useState(false)
const testPerformance = async () => {
fastAlgorithm().then(result => {
setFastResult(result)
})
slowAlgorithm().then(result => {
setSlowResult(result)
})
}
return (
<div>
<button onClick={testPerformance}>Run test!</button>
<div>{fastResult}</div>
<div>{slowResult}</div>
</div>
)
}
export default PerformanceTest
I read somewhere that ReactDOM.flushSync() would trigger the re-rendering on each state change, but it did not make any difference. This is what I tried:
const testPerformance = async () => {
ReactDOM.flushSync(() =>
fastAlgorithm().then(result => {
setFastResult(result)
})
)
ReactDOM.flushSync(() =>
slowAlgorithm().then(result => {
setSlowResult(result)
})
)
}
And also this:
const testPerformance = async () => {
fastAlgorithm().then(result => {
ReactDOM.flushSync(() =>
setFastResult(result)
)
})
slowAlgorithm().then(result => {
ReactDOM.flushSync(() =>
setSlowResult(result)
)
})
}
I also tried restructuring the algorithms so they didn't use Promises and tried this, with no luck:
const testPerformance = () => {
setFastResult(fastAlgorithm())
setSlowResult(slowAlgorithm())
}
Edit
As Sujoy Saha suggested in a comment below, I replaced my algorithms with simple ones using setTimeout(), and everything works as expected. "Fast" is displayed first and then two seconds later "Slow" is displayed.
However, if I do something like the code below it doesn't work. Both "Fast" and "Slow" shows up when the slower function finishes... Does anyone know exactly when/how the batch rendering in React happens, and how to avoid it?
export const slowAlgorithm = () => {
return new Promise((resolve, reject) => {
const array = []
for(let i = 0; i < 9000; i++) {
for(let y = 0; y < 9000; y++) {
array.push(y);
}
}
resolve('slow')
})
}

Your initial PerfomanceTest component is correct. The component will re-render for the each state change. I think issue is in your algorithm. Please let us know how did you returned promise there.
Follow below code snippet for your reference.
export const fastAlgorithm = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('fast')
}, 1000)
})
}
export const slowAlgorithm = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('slow')
}, 3000)
})
}

Are you running your algorithms synchronously on the main thread? If so, that's probably what's blocking React from re-rendering. You may need to move them to worker threads.
The below is loosely based on this answer, minus all the compatibility stuff (assuming you don't need IE support):
// `args` must contain all dependencies for the function.
const asyncify = (fn) => {
return (...args) => {
const workerStr =
`const fn = ${fn.toString()}
self.onmessage = ({ data: args }) => {
self.postMessage(fn(...args))
}`
const blob = new Blob([workerStr], { type: 'application/javascript' })
const worker = new Worker(URL.createObjectURL(blob))
let abort = () => {}
const promise = new Promise((resolve, reject) => {
worker.onmessage = (result) => {
resolve(result.data)
worker.terminate()
}
worker.onerror = (err) => {
reject(err)
worker.terminate()
}
// In case we need it for cleanup later.
// Provide either a default value to resolve to
// or an Error object to throw
abort = (value) => {
if (value instanceof Error) reject(value)
else resolve(value)
worker.terminate()
}
})
worker.postMessage(args)
return Object.assign(promise, { abort })
}
}
const multiplySlowly = (x, y) => {
const start = Date.now()
const arr = [...new Array(x)].fill([...new Array(y)])
return {
x,
y,
result: arr.flat().length,
timeElapsed: Date.now() - start,
}
}
const multiplySlowlyAsync = asyncify(multiplySlowly)
// rendering not blocked - just pretend this is React
const render = (x) => document.write(`<pre>${JSON.stringify(x, null, 4)}</pre>`)
multiplySlowlyAsync(999, 9999).then(render)
multiplySlowlyAsync(15, 25).then(render)
Note that fn is effectively being evaled in the context of the worker thread here, so you need to make sure the code is trusted. Presumably it is, given that you're already happy to run it on the main thread.
For completeness, here's a TypeScript version:
type AbortFn<T> = (value: T | Error) => void
export type AbortablePromise<T> = Promise<T> & {
abort: AbortFn<T>
}
// `args` must contain all dependencies for the function.
export const asyncify = <T extends (...args: any[]) => any>(fn: T) => {
return (...args: Parameters<T>) => {
const workerStr =
`const fn = ${fn.toString()}
self.onmessage = ({ data: args }) => {
self.postMessage(fn(...args))
}`
const blob = new Blob([workerStr], { type: 'application/javascript' })
const worker = new Worker(URL.createObjectURL(blob))
let abort = (() => {}) as AbortFn<ReturnType<T>>
const promise = new Promise<ReturnType<T>>((resolve, reject) => {
worker.onmessage = (result) => {
resolve(result.data)
worker.terminate()
}
worker.onerror = (err) => {
reject(err)
worker.terminate()
}
// In case we need it for cleanup later.
// Provide either a default value to resolve to
// or an Error object to throw
abort = (value: ReturnType<T> | Error) => {
if (value instanceof Error) reject(value)
else resolve(value)
worker.terminate()
}
})
worker.postMessage(args)
return Object.assign(promise, { abort }) as AbortablePromise<
ReturnType<T>
>
}
}

Related

Cannot setstate in nested axios post request in react

I am trying to access the res.data.id from a nested axios.post call and assign it to 'activeId' variable. I am calling the handleSaveAll() function on a button Click event. When the button is clicked, When I console the 'res.data.Id', its returning the value properly, but when I console the 'activeId', it's returning null, which means the 'res.data.id' cannot be assigned. Does anyone have a solution? Thanks in advance
const [activeId, setActiveId] = useState(null);
useEffect(() => {}, [activeId]);
const save1 = () => {
axios.get(api1, getDefaultHeaders())
.then(() => {
const data = {item1: item1,};
axios.post(api2, data, getDefaultHeaders()).then((res) => {
setActiveId(res.data.id);
console.log(res.data.id); // result: e.g. 10
});
});
};
const save2 = () => {
console.log(activeId); // result: null
};
const handleSaveAll = () => {
save1();
save2();
console.log(activeId); // result: again its still null
};
return (
<button type='submit' onClick={handleSaveAll}>Save</button>
);
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, like in your example, the console.log function runs before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
console.log(activeId);
}, [activeId);
The callback will run every time the state value changes and only after it has finished changing and a render has occurred.
Edit:
Based on the discussion in the comments.
const handleSaveSections = () => {
// ... Your logic with the `setState` at the end.
}
useEffect(() => {
if (activeId === null) {
return;
}
save2(); // ( or any other function / logic you need )
}, [activeId]);
return (
<button onClick={handleSaveSections}>Click me!</button>
)
As the setState is a async task, you will not see the changes directly.
If you want to see the changes after the axios call, you can use the following code :
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
setTimeout(()=>console.log(activeId),0);
})
useEffect(() => {
}, [activeId]);
const [activeId, setActiveId] = useState(null);
const save1 = () => {
const handleSaveSections = async () => {
activeMetric &&
axios.get(api1, getDefaultHeaders()).then(res => {
if (res.data.length > 0) {
Swal.fire({
text: 'Record already exists',
icon: 'error',
});
return false;
}
else {
const data = {
item1: item1,
item2: item2
}
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
})
}
});
}
handleSaveSections()
}
const save2 = () => {
console.log(activeId); //correct result would be shown here
}
const handleSaveAll = () => {
save1();
save2();
}
return (
<button type="submit" onClick={handleSaveAll}>Save</button>
)

useEffect function inside context unaware of state changes inside itself

I am building a messaging feature using socket.io and react context;
I created a context to hold the conversations that are initially loaded from the server as the user passes authentication.
export const ConversationsContext = createContext();
export const ConversationsContextProvider = ({ children }) => {
const { user } = useUser();
const [conversations, setConversations] = useState([]);
const { socket } = useContext(MessagesSocketContext);
useEffect(() => {
console.log(conversations);
}, [conversations]);
useEffect(() => {
if (!socket) return;
socket.on("userConversations", (uc) => {
let ucc = uc.map((c) => ({
...c,
participant: c.participants.filter((p) => p._id != user._id)[0],
}));
setConversations([...ucc]);
});
socket.on("receive-message", (message) => {
console.log([...conversations]);
console.log(message);
setConversations((convs) => {
let convIndex = convs.findIndex(
(c) => c._id === message.conversation._id
);
let conv = convs[convIndex];
convs.splice(convIndex, 1);
conv.messages.unshift(message);
return [conv, ...convs];
});
});
}, [socket]);
return (
<ConversationsContext.Provider
value={{
conversations,
setConversations,
}}
>
{children}
</ConversationsContext.Provider>
);
};
The conversations state is updated with the values that come from the server, and I have confirmed that on the first render, the values are indeed there.
Whenever i am geting a message, when the socket.on("receive-message", ...) function is called, the conversations state always return as []. When checking devTools if that is the case I see the values present, meaning the the socket.on is not updated with the conversations state.
I would appreciate any advice on this as I`m dealing with this for the past 3 days.
Thanks.
You can take "receive-message" function outside of the useEffect hook and use thr reference as so:
const onReceiveMessageRef = useRef();
onReceiveMessageRef.current = (message) => {
console.log([...conversations]);
console.log(message);
setConversations((convs) => {
let convIndex = convs.findIndex(
(c) => c._id === message.conversation._id
);
let conv = convs[convIndex];
convs.splice(convIndex, 1);
conv.messages.unshift(message);
return [conv, ...convs];
});
};
useEffect(() => {
if (!socket) return;
socket.on("userConversations", (uc) => {
let ucc = uc.map((c) => ({
...c,
participant: c.participants.filter((p) => p._id != user._id)[0],
}));
setConversations([...ucc]);
});
socket.on("receive-message", (...r) => onReceiveMessageRef.current(...r));
}, [socket]);
let me know if this solves your problem

Functions in a jest test only work when launched alone, but not at the same time

I have a custom hook that updates a state. The state is made with immer thanks to useImmer().
I have written the tests with Jest & "testing-library" - which allows to test hooks -.
All the functions work when launched alone. But when I launch them all in the same time, only the first one succeed. How so?
Here is the hook: (simplified for the sake of clarity):
export default function useSettingsModaleEditor(draftPage) {
const [settings, setSettings] = useImmer(draftPage);
const enablePeriodSelector = (enable: boolean) => {
return setSettings((draftSettings) => {
draftSettings.periodSelector = enable;
});
};
const enableDynamicFilter = (enable: boolean) => {
return setSettings((draftSettings) => {
draftSettings.filters.dynamic = enable;
});
};
const resetState = () => {
return setSettings((draftSettings) => {
draftSettings.filters.dynamic = draftPage.filters.dynamic;
draftSettings.periodSelector = draftPage.periodSelector;
draftSettings.filters.static = draftPage.filters.static;
});
};
return {
settings,
enablePeriodSelector,
enableDynamicFilter,
resetState,
};
}
And the test:
describe("enablePeriodSelector", () => {
const { result } = useHook(() => useSettingsModaleEditor(page));
it("switches period selector", () => {
act(() => result.current.enablePeriodSelector(true));
expect(result.current.settings.periodSelector).toBeTruthy();
act(() => result.current.enablePeriodSelector(false));
expect(result.current.settings.periodSelector).toBeFalsy();
});
});
describe("enableDynamicFilter", () => {
const { result } = useHook(() => useSettingsModaleEditor(page));
it("switches dynamic filter selector", () => {
act(() => result.current.enableDynamicFilter(true));
expect(result.current.settings.filters.dynamic).toBeTruthy();
act(() => result.current.enableDynamicFilter(false));
expect(result.current.settings.filters.dynamic).toBeFalsy();
});
});
describe("resetState", () => {
const { result } = useHook(() => useSettingsModaleEditor(page));
it("switches dynamic filter selector", () => {
act(() => result.current.enableDynamicFilter(true));
act(() => result.current.enablePeriodSelector(true));
act(() => result.current.addShortcut(Facet.Focuses));
act(() => result.current.resetState());
expect(result.current.settings.periodSelector).toBeFalsy();
expect(result.current.settings.filters.dynamic).toBeFalsy();
expect(result.current.settings.filters.static).toEqual([]);
});
});
All functions works in real life. How to fix this? Thanks!
use beforeEach and reset all mocks(functions has stale closure data) or make common logic to test differently and use that logic to test specific cases.
The answer was: useHook is called before "it". It must be called below.

useState not updating Array after calling API

I have my code as follows:
const ManageModules: React.FC < IProps > = ({
history
}) => {
const [CurrentlyRegisteredModules, setCurrentlyRegisteredModules] = useState(null as unknown as any[])
const [SelectedModule, setSelectedModule] = useState(null as unknown as any);
useEffect(() => {
userModuleService.addUserModule(SelectedModule.id)
.subscribe(res => {
setCurrentlyRegisteredModules(x => {
x.push(new RegisteredModules(res.data));
return x;
});
});
})
},
[SelectedModule]);
useEffect(() => {
console.log('Current Reg Change');
}, [CurrentlyRegisteredModules]);
}
Everytime SelectedModule changes, the effect listening to it runs fine and returns a value. I then push the result to array CurrentlyRegisteredModules using setCurrentlyRegisteredModules.
However, the problem I am facing is that the effect listening to CurrentlyRegisteredModules is not triggered but when i inspect CurrentlyRegisteredModules in console, it shows that the result was added to the area.
Can anyone identify what I could be doing wrong.
Change following code
setCurrentlyRegisteredModules(x => {
x.push(new RegisteredModules(res.data));
return x;
});
to
setCurrentlyRegisteredModules(x => {
let newX = [...x]
newX.push(new RegisteredModules(res.data));
return newX;
});
Do not mutate x.

how to handle race conditions in class components?

Suppose there is a component where ask server to do some search and response will be rendered. How to ensure most recent request's response is rendered even if server side for any reason answers in different ordering? I'm not asking about cancelling previous request since it's not always possible with reasonable efforts.
onClick = () => {
apiCall(this.state.searchQuery).then(items => this.setState({ items }));
};
Is there elegant way to handle that? By now I know few approaches:
disabling button till request comes(provides bad experiences in large amount of cases - say for searching while typing)
checking inside then() if request's params matches this.props/this.state data(does not handle case when we intentionally forced new search with same query - say by pressing Enter/clicking "Search" button)
onClick = () => {
const searchQuery = this.state.searchQuery;
apiCall(searchQuery)
.then(items =>
this.state.searchQuery === searchQuery
&& this.setState({ items })
);
};
marking requests somehow and checking if it's latest(works, but looks too verboose especially if there are few requests we need to check)
searchQueryIndex = 0;
onClick = () => {
this.searchQueryIndex++;
const index = this.searchQueryIndex;
apiCall(this.state.searchQuery)
.then(items =>
this.searchQueryIndex === searchQueryIndex
&& this.setState({ items })
);
};
I'd call that trio "ugly, broken and messy".
Is there something such clear way as hooks allow:
useEffect(() => {
const isCanceled = false;
apiCall(searchQuery).then(items => !isCanceled && setItems(items));
return () => {isCanceled = true;};
}, [searchQuery])
Your onClick handler suggest a class component since you use this and this.setState:
onClick = () => {
apiCall(this.state.searchQuery).then(items =>
this.setState({ items })
);
};
I adjusted onlyLastRequestedPromise to take a function that will return something (you can return Promise.reject('cancelled') or anything).
const onlyLastRequestedPromise = (promiseIds => {
const whenResolve = (
promise,
id,
promiseID,
resolveValue,
whenCancelled = () => Promise.reject('cancelled')
) => {
if (promise !== undefined) {
//called by user adding a promise
promiseIds[id] = {};
} else {
//called because promise is resolved
return promiseID === promiseIds[id]
? Promise.resolve(resolveValue)
: whenCancelled(resolveValue);
}
return (function(currentPromiseID) {
return promise.then(function(result) {
return whenResolve(
undefined,
id,
currentPromiseID,
result
);
});
})(promiseIds[id]);
};
return (id = 'general', whenCancelled) => promise =>
whenResolve(
promise,
id,
undefined,
undefined,
whenCancelled
);
})({});
A class example on how to use it:
class Component extends React.Component {
CANCELLED = {};
last = onlyLastRequestedPromise(
'search',
() => this.CANCELLED
);
onSearch = () => {
this.last(apiCall(this.state.searchQuery)).then(
items =>
items !== this.CANCELLED && this.setState({ items })
);
};
changeAndSearch = e => {
this.setState(
{}, //state with new value
() => this.onSearch() //onSearch after state update
);
};
render() {
return (
<div>
<SearchButton onClick={this.onSearch} />
<Other onChange={this.changeAndSearch} />
</div>
);
}
}
I agree it's a lot of code but since you put most of the implementation in the lib it should not clutter your components.
If you had a functional component you could create the last function with useRef:
//
function ComponentContainer(props) {
const CANCELLED = useRef({});
const last = useRef(
onlyLastRequestedPromise('search', () => CANCELLED)
);
const [searchQuery,setSearchQuery] = useState({});
const mounted = useIsMounted();
const onSearch = useCallback(
last(apiCall(searchQuery)).then(
items =>
items !== CANCELLED &&
mounted.current &&
//do something with items
)
);
}
Finally figured out how to utilize closure to mimic "just ignore that" approach from hooks' world:
class MyComponent extends React.Component {
const ignorePrevRequest = () => {}; // empty function by default
loadSomeData() {
this.ignorePrevRequest();
let cancelled = false;
this.ignorePrevRequest = () => { cancelled = true; }; // closure comes to play
doSomeCall().then(data => !cancelled && this.setState({ data }))
}
}

Resources